Note: This article was written to increase developer awareness to different attack vectors, not as a how-to for attacking existing systems.
In 2012 delfi.ee (an Estonian media outlet) organized a “Hot male student” competition. Visitors of the website could cast their votes to five candidates of different universities. The code was poorly written and so the winner of the competition wasn’t necessarily the best looking candidate, but the one who had the support of tech-savvy fellow students.
Online voting systems are often implemented with naive security considerations. “Why would anyone ever delete their cookies?”, thinks the blue-eyed developer who identifies visitors by cookies. I’ll tell you why: because they can.
Think about how people could exploit your system and be the person in the team who’s most enthusiastic about tearing apart your own code. You ship the code to the real world - don’t assume all visitors will play nice with the system.
Attack vectors to online voting systems
Below is a list of different vulnerabilities most commonly found in voting implementations.
The website displays a voting area. The user presses “submit” and is shown a thank-you message. The website saves no information about the vote being processed, not to its own database, not into the browser.
Vulnerability: No checks whatsoever
The website has no idea whether the visitor has already voted, hence the voting form is shown and processed for each request.
Attack: Repeat the request
No special knowlege is needed to attack this system. You can refresh the browser and vote again (spam F5) or write a short script to automate it, just sending curl requests from bash.
Forget the cookiejar
The website sets a special cookie into the visitors browser to mark the user as ‘you have voted’. This is the most common strategy for letting visitors vote only once. The server trusts the visitors browser to store the ‘already voted’ flag.
Vulnerability: Trusting user input
User input should never be trusted. Everything sent by the user to the server can be tampered with, especially if the only check is the presence of a cookie.
Attack: Forget the cookie
It is trivial for a user to remove the cookie from his browser or to write a script that just doesn’t save and return cookies.
IP based memory
The server saves the voters IP into a database of IPs who have voted. Before processing the vote, the visitors IP is checked against this database and if a match is found, an error is shown (you have already voted).
Vulnerability: Failing to identify voters
The assumption here is that IP equals person. This is not the case:
- Many internet connections are set up with a dynamic IP
- A person has more than one devices (phone, laptop, tablet)
- Visiting different hotspots (coffees) yields different WAN IPs
- VPNs provide different IPs
- Oftentimes, several devices share a single external IP
Attack: Use different IPs
Take your laptop and visit twenty different coffee shops: you don’t even have to sit down, just connect from outside the coffee house, agree to the ToS, open the browser, vote, disconnect and move to the next coffee (hint: automate).
Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into otherwise benign and trusted web sites
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated
Vulnerability: Failure to protect against XSS and CSRF
Attack: XSS and CSRF
There is no magic bullet that works for every use case. How you protect your system depends on your needs. Requiring all voters to be signed in with their ID card guarantees that everyone votes exactly once, but will lower poll participation rate drastically. Oftentimes it’s just a compromise on ‘reasonable’ level of protection.
Here are some things you can do.
Don’t allow multiple votes from the same source. Select a good enough authentication scheme (“how do I identify a unique user?”) and don’t allow the same source to vote twice.
Use dynamic values for submission
If your voting system has a
voteId (the ID of the current poll) and
selectionId (the choice that the user wants to vote for), don’t make their values easily guessable and/or static.
Using static values enables the attacker to just copy them into their attack script.
Either use rolling codes (dynamic, unique
selectionId) for each page rendering or include an invisible form field with a dynamic, unique value (a nonce) which is verified on form submit by the server.
This technique can be defeated with web scraping: the attacker would first request the poll page HTML and then submit the vote with the unique token parsed out of the original response and into the vote request.
Require your voters to log in before voting. This loses the anonymity of the poll - you will get less results -, but increases the certainty that the results won’t be tampered with. A user can only vote once, since the state is tied with his user account. The effectiveness of this approach is dependant on your new user signup policy: how difficult is it for a bot to create a new, temporary, fake account?
Protect against XSS and CSRF
Filter user input for XSS. Implement same-origin policy and protect against CSRF.
Add a Turing test
Add a CAPTCHA to the vote submission. This will annoy legitimate users, but will cause problems to attackers. This method of protection is not bulletproof, but will probably deter many script kiddies.
Don’t trust user input
vikerraadio.err.ee stores the voter information in a cookie:
URL decoding the value of the cookie gives this:
We can see that the cookie contains the vote timestamp and option value. What might happen if I delete this cookie? Can I vote again?
Implement IP blacklist
Compare voter IP against a database of known VPN providers. This will reduce the amount of requests that can come in from a proxy-IP, but is a dangerous ground to tread on: discrimination of privacy-aware individuals can blow up in your face. You’re basically saying that since the visitor values their privacy, they can not use your site.
Identifying automated votes
Let’s say that an attacker writes a script that makes 100 requests to the target voting server and adds 100 votes to his preferred option. As the developer of the system, how do you distinguish fake votes from the real ones?
Look at the request content
A script kiddie might be ‘clever’ enough to write a PHP script, but does he actually understand how HTTP or the library he’s using works? For example, the
User-Agent HTTP header of a popular PHP HTTP client library Guzzle looks like this:
Guzzle/<Guzzle_Version> curl/<curl_version> PHP/<PHP_VERSION>
while Chrome typically sends something like this:
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36
This information is sent to the server and can be stored in logs. The server might automatically reject suspicious requests based on the request headers or notify administrators.
So you want your script to add 100 votes? Will you send 100 requests as quickly as possible or will you space them out evenly inside 24 hours? As with the request contents, it’s easy to identify requests by their arrival time. Seeing 100 incoming votes in the space of a few seconds raises some questions.
This example demonstrates the adding of votes to a vulnerable system. The target system identifies voters by IP (no IP can vote twice).
The attack script adds three votes to a specific option and works as follows:
- A connection to the local Tor service is established (HTTP requests will be proxied through Tor)
- A HTTP POST request to the target voting system is sent. The system rejects the request, since the current IP has already voted
- The script requests a new IP address from Tor
- A HTTP POST request to the target voting system is sent. The system accepts the vote
Be aware that your voting implementation can be hacked - but you can make it more time-consuming if you implement elementary safeguards.