How a Race Condition Vulnerability Could Cast Multiple Votes
This blog was originally posted on Medium by Dane Sherrets.
Race condition vulnerabilities make up less than 0.3% of reports on the HackerOne platform. However, researchers have recently been particularly interested in experimenting with race condition vulnerabilities as a result of James Kettle’s “State Machine” research that he presented at BlackHat in 2023.
This blog tells the story of how I applied this research to the World ID Software Development Kit (SDK) and identified a race condition that would have enabled an attacker to bypass the preset verification limit on a cloud-based implementation. The exploitation of this vulnerability would impact a project that relied on World ID for restricting the number of actions a user could take (for example, if a project was using the SDK to ensure a user should only be able to cast one vote, an attacker could exploit the vulnerability to cast 20 votes).
What Is A Race Condition?
A race condition occurs when the behavior of software is contingent upon the sequence or timing of uncontrollable events. This flaw arises in environments where multiple processes or threads access and modify shared resources concurrently without appropriate synchronization, leading to unpredictable outcomes.
The introduction of race conditions is often attributed to insufficient design consideration regarding concurrency (e.g. - lack of adequate safeguards when processing requests concurrently) and flawed implementation of access controls to shared data. A common scenario involves two threads that both read and write to a shared variable without ensuring the operations are atomic. Without proper locking mechanisms or atomic operations, the final state of the shared resource becomes dependent on the order of execution, leading to inconsistencies and potentially compromising the integrity and security of the system.
These vulnerabilities can be exploited to cause unauthorized actions, data corruption, and even system crashes. In worst-case scenarios, attackers could leverage race conditions to bypass security mechanisms, gain unauthorized access to sensitive data, or execute arbitrary code, posing significant risks to organizational security and data integrity. Addressing race conditions requires a meticulous approach to design and testing, emphasizing concurrency control and ensuring that critical operations on shared resources are performed atomically.
Background on Worldcoin
I have been interested in Worldcoin ever since it hit the Blockchain scene back in 2019. Worldcoin is a cryptocurrency project that aims to build a foundation for a) biometrically verifying unique humans and b) a distribution mechanism for Universal Basic Income (UBI). Most people probably just know about it as the company that builds an orb that will give you cryptocurrency if you stare into it.
The idea of humans scanning their eyeballs and giving biometric data to a centralized entity is certainly controversial, however, Worldcoin’s approach delivers Sybil resistance, one of the most difficult problems the internet faces today (A Sybil attack is a security threat where an attacker creates multiple fake identities to gain a disproportionately large influence on a network.) As we approach a world where anyone can create hundreds of AI-powered bots, Sybil resistance will become a critical problem to solve (it is worth noting that Sam Altman is one of the founders of Worldcoin) and Worldcoin is one of the only projects that is working on this issue in a meaningful way.
Before I started looking into Worldcoin, I had assumed that it was purely a cryptocurrency distribution mechanism but, as I dug deeper, I found that they were building a Software Development Kit (SDK) around biometric authentication. This means that other apps could use the Worldcoin SDK to confirm that one user = one human being. That might not sound like a big deal, but it is a game changer for apps with use cases including voting, airdropping, and bot protection. For example, if I wanted to make an app that made it so a unique human could only vote once on a given issue, then I could make all of my users authenticate via the Worldcoin SDK, which would essentially act as an Identity Provider (IDP) and enforcer for the limitation I preconfigured (i.e vote once).
As a cryptocurrency, Worldcoin has an SDK implementation that works for (EVM-compatible) blockchains and a cloud-based version that does not require touching any blockchain. Both versions use some really cool Merkle-tree-based cryptography to create a tamper-proof log of transactions that is worth reading up on if you are not familiar.
Testing on Worldcoin
To help developers test the Worldcoin workflow, Worldcoin offers a simulator to test transactions and verifications (link here). If I create a dev account, I can create “actions” (e.g. “vote on this proposal”) and set a maximum number of “verifications” (e.g. “number of times a human can vote on a proposal). To look under the hood of how this all works, I did most of my testing on this simulator and ran the requests through Burp Suite.
While I could change things like the “credential type” (either “orb” or “device”) or “chain”, the requests were all very similar. I spent many hours trying to break the flow and signatures but was unsuccessful in achieving anything interesting, so I gave up for a few weeks.
Flash forward to DEF CON 31, and James Kettle presented his research on Smashing The State Machine. As soon as I read his blog, I realized it was something I could try on Worldcoin. I opened up the Burp Repeater tab, made sure I downloaded the Burp update that had the “send in parallel” feature, and gave it a shot on the Worldcoin flow I described earlier.
Specifically, the steps were:
- Capture the request and send it to the repeater
- Make a new “tab” for the request
- Duplicate the original request ~20 times in the new “tab”
- Send them all “in parallel”
When I looked at the requests it worked! Where I should have only seen two verifications of “unique humans,” I saw 20!
I only tested this on the “cloud” version of the implementation as I was pretty sure it wouldn’t work on the “blockchain” version since that would require a transaction to be finalized.
Since the Worldcoin project is open source, I reviewed the code and saw a red flag in the `canVerifyForAction` util:
export const canVerifyForAction = (
nullifiers:
| Array<{
nullifier_hash: string;
}>
| undefined,
max_verifications_per_person: number
): boolean => {
if (!nullifiers?.length) {
// Person has not verified before, can always verify for the first time
return true;
} else if (max_verifications_per_person <= 0) {
// `0` or `-1` means unlimited verifications
return true;
}
// Else, can only verify if the max number of verifications has not been met
return (nullifiers?.length ?? 0) < max_verifications_per_person;
}
Specifically, the way `nullifier` is referenced in other parts of the codebase here allowed for a race condition as it just added to an array without a “lock,” which means parallel requests would all be treated equally.
The Mitigation
The Worldcoin team addressed this by using the database to create an artificial lock so that even if two requests come in at the same time, they will be treated properly. In this pull request, you can see they now have new database tables for `nullifiers`, and they have updated the `canVerifyForAction` code to reference the `nullifier` number of uses instead of just the length:
export const canVerifyForAction = (
nullifier:
| {
uses: number;
nullifier_hash: string;
}
| undefined,
max_verifications_per_person: number
): boolean => {
if (!nullifier) {
// Person has not verified before, can always verify for the first time
return true;
} else if (max_verifications_per_person <= 0) {
@@ -146,7 +147,7 @@ export const canVerifyForAction = (
}
// Else, can only verify if the max number of verifications has not been met
return nullifier.uses < max_verifications_per_person;
};
When people think of a “blockchain” or “Web3” project, they often assume that any vulnerabilities will be limited to the network or smart contract — this is often not the case. Today, Web3 still relies heavily on Web2, and this opens up a wide range of creative attack vectors. If you are a Javascript expert and you haven’t tried bug hunting on Web3 projects, then you are leaving money on the table!
I would like to thank the World team for diligently fixing this issue and giving me permission to disclose it on Hacktivity.
Addressing Race Conditions:
To prevent or mitigate race condition vulnerabilities, developers and architects can employ several strategies, such as:
- Implementing proper synchronization techniques to manage access to shared resources.
- Designing systems with concurrency in mind, ensuring that operations on shared resources are atomic.
- Using high-level abstractions and programming constructs designed to handle concurrency safely.
- Conducting thorough testing, including stress tests and concurrency tests, to uncover and fix potential race conditions.
The 8th Annual Hacker-Powered Security Report