What Are Race Conditions?#
Race conditions are relatively common and are closely related to business logic flaws.
A collision occurs when multiple distinct threads try to interact with the same data at the same time. Race condition attacks are when attackers use carefully timed requests to cause intentional collisions to exploit an application.
- The term thread is used to describe a sequence of instructions to be computed. A single process (like redeeming a discount code) might be made up of multiple threads that do different operations.
The race window is the period in time where a collision is possible. Usually a very short time window but still exploitable in the right context.
So far this is a very vague description but let’s get into more details.
Limit Overrun Race Conditions#
The most well-known type of race condition enables you to exceed some kind of limit imposed by the business logic of the application.
Imagine an online store that allows you to apply a gift card to get five dollars added to your account’s balance. The application might need to check a few things before allowing you to use the gift card:
- Check if the gift card has been used
- Apply the gift card to the user’s balance
- Update the record in the database that indicated the user used the gift card
This way, trying to re-use the gift card won’t work.

But imagine if we tried to submit both of these requests at the exact same time:

This diagram is meant to represent how sending the two requests at the same time could expose a race window. The race window is between when the application checks if the gift was already used and when the application adds the gift to the user’s account.
Because those operations are editing the same piece of data (setting the gift to used) at the same time, the gift card is able to be added twice.
The applications of this technique are pretty wide in terms of where you can find them. Redeeming codes and gift cards, leaving reviews or likes on forum sites, withdrawing or transferring funds, bypassing brute-force limits, and so on.
Detecting and exploiting limit overruns with the Burp Repeater#
In order to exploit these kinds of vulnerabilities, it is pretty simple. You need two things:
- Find a single-use or rate-limited endpoint that has some security impact or useful purpose
- Send multiple requests in quick succession to the endpoint
The hard part is making sure that the requests are timed correctly, as the race window is usually only a few milliseconds.
Even if you send the requests at the exact same time, there are factors like network latency, and jitter that you need to worry about.
- Latency is the time it takes for data to arrive from one endpoint to another.
- Jitter is the difference in delay between two packets.
Burp Suite does have features that help you send requests in parallel that can reduce jitter, but the way it does this depends on which version of HTTP is being used by the target.
- Last byte synchronization is used for HTTP 1 and single-packet attacks are used for HTTP 2.
Single packet attacks effectively neutralize jitter by sending only one TCP packet to send 20 or so requests at the same time.
Even though in theory, two requests is enough to trigger an exploit, larger numbers help mitigate latency and jitter problems.
Hidden Multi-Step Sequences#
In practice, a single request might initiate more operations behind the scenes for a web application. This sequence might move the application into multiple different hidden states before completing the process. These are called sub-states.
If we are able to find multiple HTTP requests that cause an interaction with the same data, we might be able to abuse the sub-states to reveal time-sensitive variations of the workflow. This lets race conditions move into realms other than the basic limit overruns.
We can talk about this same topic in the context of MFA bypass similar to the one we went over in the authentication module. Let’s say that we are able to navigate to an endpoint and login where we should get redirected to a page to input out MFA code, the pseudocode might look like this:
session[userId] = gabe.userId
if gabe.mfa_enabled:
session[enforceMFA] = True
# send reset code to gabe
# redirect browser to MFA code input screen
This would produce a multi-step sequence initialized by one request. The user logs in and has their account set in a temporary logged in state so that the application can check if they have MFA enabled, and redirects the user to the code input page if it is.
This can be potentially exploited if there is an adequate race window between the time we are given the temporary login session and the time the application has to enforce MFA. As long as we make a valid request to an authenticated endpoint in that race window, theoretically it should work.
Methodology#
This methodology is from a talk by James Kettle, called Smashing the state machine: the true potential of web race conditions. There is also an amazing video version of this talk that was given at DEF CON 31 here.
There are three essential steps to the methodology: predict, probe, and prove.
Predict potential collisions#
We need some way to determine where these collisions might be, so try to limit the high effort investigating to endpoints that answer these questions:
- Is there a security impact? If the potential impact of a race condition doesn’t impact the security of the application, it isn’t worth testing.
- Is there room for a collision? You typically need two or more requests that trigger some operation on the same record.
Imagine a password reset implementation where the goal is to give the user a token to navigate to the reset page.
- Version A: When a user with an active session tries to reset their password, the application uses the user ID as a lookup key to find and return a reset token that is stored in a database. This would not produce a collision because if we requested two user IDs with the same session token, we would be retrieving two separate values.
- Version B: When a user with an active session tries to reset their password, the session token is used as a lookup key where the user ID and reset token are returned in the request. This would work because if two different users try to reset their password using the same session token, the threads will try to return two user IDs and two reset tokens.
Probe for clues#
To recognize clues, you need to properly benchmark the expected behavior of the application. Knowing the default behavior helps you understand when things are going wrong.
You can do this by collecting the request that you are investigating and copying it 20 or so times in the repeater. Afterwards you can submit them in sequence to determine the expected behavior.
If you send the requests in parallel instead and notice some deviations from the benchmarking behavior, you might be able to prove the exploit.
Proving the concept#
Try to refine the exploit you found by removing unnecessary requests and understanding how it works. For example you might find something that works but is later invalidated because of certain requests included in the probing attempt.
Multi-Endpoint Race Conditions#
This is the most intuitive type to exploit, like if you tried to checkout your cheap cart from an online store but exploit a race condition to add an expensive item at the same time, keeping the price small but fulfilling the order.
This might happen if payment validation and order confirmation are done in the same request.
Aligning multi-endpoint race windows#
Even if you send these requests at the same time, you might run into issues because of network delays between back-end architecture or differences in the rate that different endpoints process their data.
The workarounds for this context are:
- Connection warming: If you see some strange delays between your requests, try adding a request at the beginning of the sequence to warm the request. This is just meant to see if you can determine why a delay might be present. If you notice that the warming request has a long delay but the requests after don’t, then you can keep testing in confidence.
- Abusing rate limits: If connection warming doesn’t seem to normalize the delays, it likely has something to do with a back-end delay of some kind that is being introduced. Thankfully, some web servers will delay the processing of requests if too many are sent too quickly. So if we send a bunch of dummy requests to trigger an internal rate or resource limit, we might be able to get the server-side delay to fit our conditions.
Single-Endpoint Race Conditions#
Consider a password reset mechanism that stores the user ID and reset token in the user’s session. In this case sending two different password reset requests with two different user IDs, but the same session, could cause a collision.
Let me try and take it apart a bit more:
- User A tries reset their password, setting the session user attribute to
A. The session should reflectuser-a&. - User B tries to reset their password, setting the session user attribute to
B. The session should reflectuser-b&. - User B is assigned a reset code
222by the application and it is applied to the current session. The session should reflectuser-b&reset-222. The code is emailed to User B. - User A is then assigned a reset code
777and it is applied to the current session. This session should reflectuser-b&reset-777. The code is emailed to User A.
This required the application to perform the requests in the perfect wrong order, but it is possible if you attempt enough times.
The issue is that the session at the end reflects that User B has a reset code that has in reality been send to User A, which will allow User A to reset the password for User B.
PortSwigger Lab Example#
The only place I found some great examples of this is from the PortSwigger labs. HTB does have a challenge called Diogenes’ Rage, but it was really inconsistent to exploit.
For this exercise, we are tasked with exploiting a feature on the web application that allows us to change our email address.
Specifically, we need get the web application to send an email intended for carlos@ginandjuice.shop to the email address associated with the account that we were given. This is because the admin role was granted to Carlos based on his email address, but he hasn’t created his account yet. Meaning that any user who can verify that this is their email address will be granted admin privileges.
So we need to find where this race window is, get the web application to think our email address is carlos@ginandjuice.shop, and then log in to the admin panel.
We are given the credentials wiener:peter and we also have access to an email client where we can receive emails through Burp Suite’s lab.
We can begin by looking for where the collision might be taking place. We can map out the process like this:
- We can enter a new email and select
Update email - The application sends an email to that address, containing a link that will update that user’s email when navigated to.
Now that we have a basic idea of how it works, we can look for quirks about it. I will try to update the email twice and see if the older of the two reset links work.
The older of the two reset links did not work, and this tells us that the website is only keeping track of one pending email address at a time. Specifically, by submitting a new email address we are having the application edit an entry in a database for this new email address.
Well, let’s break down the process into more pieces:
- We ask to change our email
- The application writes the email into a database
- The application then puts this email address from the database into an email template
- The email template is then sent to the email from the database
So there might be a race window between the time the application fills out the email template and when the application sends the email.
I made a drawing to try and illustrate what I mean:

We can first benchmark the behavior by capturing 20 email update requests that each have a different email. If we send these requests in sequence, we should get 20 emails all to different email addresses as expected.
Then, we can try to send them in parallel and will notice that some of the emails were send to an address that does not match the body of the email.
Now we can try to exploit this by sending a request to change to our email and another request to change to the carlos@ginandjuice.shop email in parallel. This should after some time send the reset email for Carlos to our email.
We can then navigate to our account where the admin page is available.
Session-Based Locking Mechanisms#
Some frameworks will try to prevent data corruption of this type by only processing one request per session at a time. You can try sending the requests with different session tokens to see if that changes the outcome.
Preventions#
When a single request can transition an application through invisible sub-states, understanding and predicting its behavior is extremely difficult. This makes defending them impractical so it is probably easier to avoid using sub-states from sensitive endpoints.
- Avoid mixing data from different places.
- Try to maintain atomic state changes on sensitive endpoints.
- Take advantage of column uniqueness constraints.
- Don’t use one data storage layer to secure another, like trying to use sessions to prevent database corruption.
- Ensure that your sessions are internally consistent when being handled.
- In some architectures, it may be appropriate to avoid server-side state entirely. Instead, you could use encryption to push the state client-side, for example, using JWTs.