September 12, 2026
The Server Was Already Logged In - Race Conditions Part 2: Hidden Windows
Hidden sub-states, multi-endpoint attacks and single-endpoint password reset races - the deeper side of race conditions.

By // l1m1nal_3ntr0py
6 min read
By // l1m1nal_3ntr0py
โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ
This is Part 2 of a 3-part series on Race Conditions. Read Part 1 here.
In Part 1 we learned about limit overrun โ using parallel requests to exceed a single-use limit before the server marks it as used. Simple concept. Devastating impact.
But that was just the surface.
Some race conditions do not involve limits at all. They involve something more subtle โ hidden states that exist for milliseconds inside a single request. States the developer never intended anyone to see. States that briefly make the impossible possible.
This is where race conditions get interesting.
Hidden Multi-Step Sequences
When you send a request to a web application, you assume it does one thing. In reality, a single request often triggers an entire chain of operations behind the scenes โ each one happening in sequence, each one creating a brief window where the application is in an in-between state.
These are called sub-states.
Think of a light switch. It is either on or off. But for a fraction of a second during the flick โ it is neither. That fraction of a second is the sub-state. And in web security, that fraction of a second can be everything.
A login with MFA โ the hidden sub-state:
python
# Step 1: Set user as logged in
session['userid'] = user.userid โ logged in here!
# Step 2: Check if MFA needed
if user.mfa_enabled:
session['enforce_mfa'] = True โ MFA enforced here!
# redirect to MFA page# Step 1: Set user as logged in
session['userid'] = user.userid โ logged in here!
# Step 2: Check if MFA needed
if user.mfa_enabled:
session['enforce_mfa'] = True โ MFA enforced here!
# redirect to MFA pageBetween Step 1 and Step 2 there is a window where the user is logged in โ but MFA has not yet been enforced. For that brief moment the user has a valid authenticated session with no second factor required.
The race attack:
Normal flow (sequential):
Login โ userid set โ MFA enforced โ redirect to MFA page
You try /dashboard โ BLOCKED
Race attack:
Login request โโโโโโโโโโโโโโโบ userid set โโโโโโโ
/dashboard request โโโโโโโโโโโบ hits here! โ
โ
MFA not set yet! โ
ACCESS GRANTED! โ
MFA enforced โโโโโโโ
(too late!)Normal flow (sequential):
Login โ userid set โ MFA enforced โ redirect to MFA page
You try /dashboard โ BLOCKED
Race attack:
Login request โโโโโโโโโโโโโโโบ userid set โโโโโโโ
/dashboard request โโโโโโโโโโโบ hits here! โ
โ
MFA not set yet! โ
ACCESS GRANTED! โ
MFA enforced โโโโโโโ
(too late!)Send the login request and a request to a sensitive authenticated endpoint simultaneously. The dashboard request hits the server during the window when the session has a valid user ID but MFA has not yet been enabled.
How to Spot Hidden Sub-States
You cannot see sub-states directly โ you have to infer them. Here is how:
Trigger different errors on the same endpoint:
Send requests with intentionally wrong data to force the server through different code paths. Watch for responses that suggest the server is in an unexpected state โ errors that should not exist if the request was processed completely.
Look for multi-step processes in a single request:
โ Login that also sets session variables
โ Registration that creates AND initialises a user
โ Payment that validates AND confirms in one request
โ Email change that sends AND records simultaneouslyโ Login that also sets session variables
โ Registration that creates AND initialises a user
โ Payment that validates AND confirms in one request
โ Email change that sends AND records simultaneouslyAny endpoint that does multiple things is a candidate.
Watch for second-order effects:
โ Emails arriving with wrong content
โ Session state changing unexpectedly
โ Responses that reference data from another request
โ Confirmation of actions you did not completeโ Emails arriving with wrong content
โ Session state changing unexpectedly
โ Responses that reference data from another request
โ Confirmation of actions you did not completeMulti-Endpoint Race Conditions
The most intuitive form of multi-step race condition involves sending requests to multiple different endpoints simultaneously.
Classic example โ the online shop basket trick:
Normal flow:
Add item to basket โ Pay โ Receive order confirmation
Race attack:
POST /payment โโโโโโโโโโโโโโโโโโโบ validates payment
POST /cart/add โโโโโโโโโโโโโโโโโโโบ adds expensive item
โ sent simultaneously!
Timeline:
Payment validated โโโโโโโโโโโโโโโโโโโ
Add item hits the window! ๐ฅ โโโโโโโโค
Order confirmed (includes โ
your expensive item!) โโโโโโโโโโโโโโโโNormal flow:
Add item to basket โ Pay โ Receive order confirmation
Race attack:
POST /payment โโโโโโโโโโโโโโโโโโโบ validates payment
POST /cart/add โโโโโโโโโโโโโโโโโโโบ adds expensive item
โ sent simultaneously!
Timeline:
Payment validated โโโโโโโโโโโโโโโโโโโ
Add item hits the window! ๐ฅ โโโโโโโโค
Order confirmed (includes โ
your expensive item!) โโโโโโโโโโโโโโโโThe payment is validated against the basket at the time of payment. But if you can add an item to the basket during the window between validation and confirmation โ that item gets included in the confirmed order without being paid for.
Aligning Multi-Endpoint Windows
The challenge with multi-endpoint attacks is that different endpoints process at different speeds. Your /cart/add might complete in 50ms but /payment takes 200ms. By the time payment is ready to confirm, /cart/add has long finished โ the window has closed.
Two solutions:
Connection warming:
Before your attack, send a dummy request on the same connection to warm up the back-end connection. This eliminates the initial connection delay that makes the first request slower than subsequent ones.
Step 1: Send harmless GET / to warm connection
Step 2: Notice first request is slower โ connection warming working
Step 3: Remove warming request โ subsequent requests now fast
Step 4: Run actual attack with aligned timingStep 1: Send harmless GET / to warm connection
Step 2: Notice first request is slower โ connection warming working
Step 3: Remove warming request โ subsequent requests now fast
Step 4: Run actual attack with aligned timingIn Burp Repeater: Group your requests โ Send group in sequence (single connection) to warm โ then Send group in parallel for the attack.
Abusing rate limits for server-side delay:
Flood the server with dummy requests to trigger its rate or resource limit. This causes the server to queue all incoming requests and process them at similar speeds โ slowing down the fast endpoint to match the slow one.
Without rate limit abuse:
/cart/add โโโโโโโบ done (50ms) โ already finished!
/payment โโโโโโโโโโโโโโโโโโโโโโโบ done (200ms)
Window: โ too early โ
With rate limit abuse:
Dummy GET ร 50 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ
Server overwhelmed โ queuing requests
Now attack requests join queue:
/cart/add โโโโโโโโโโโโโโโโโโโโโโโบ done (200ms)
/payment โโโโโโโโโโโโโโโโโโโโโโโบ done (200ms)
Both aligned! โ
Window open!Without rate limit abuse:
/cart/add โโโโโโโบ done (50ms) โ already finished!
/payment โโโโโโโโโโโโโโโโโโโโโโโบ done (200ms)
Window: โ too early โ
With rate limit abuse:
Dummy GET ร 50 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ
Server overwhelmed โ queuing requests
Now attack requests join queue:
/cart/add โโโโโโโโโโโโโโโโโโโโโโโบ done (200ms)
/payment โโโโโโโโโโโโโโโโโโโโโโโบ done (200ms)
Both aligned! โ
Window open!Single-Endpoint Race Conditions
Not all race conditions require multiple endpoints. Some of the most elegant attacks happen on a single endpoint โ by sending parallel requests with different values and letting them interfere with each other.
Password reset โ the collision:
Consider a password reset mechanism that stores the user ID and reset token in the session:
python
# What the server does on each reset request:
session['reset-user'] = username โ stored in session
session['reset-token'] = generate_token()
send_email(username, token)# What the server does on each reset request:
session['reset-user'] = username โ stored in session
session['reset-token'] = generate_token()
send_email(username, token)Normal sequential behaviour:
Request 1 (wiener): reset-user=wiener, token=5678 โ email sent to wiener
Request 2 (carlos): reset-user=carlos, token=1234 โ email sent to carlos
No mixing. Safe. โ
Request 1 (wiener): reset-user=wiener, token=5678 โ email sent to wiener
Request 2 (carlos): reset-user=carlos, token=1234 โ email sent to carlos
No mixing. Safe. โ
Race attack โ parallel requests:
Request 1 (wiener) โโโโโโโโโโโโโโโโโโโโ token=5678
Request 2 (carlos) โโโโโ token=1234
โ MIX HAPPENS HERE!
Final state:
reset-user = carlos โ from request 2
reset-token = 5678 โ from request 1 (overwrote!)
Email sent to wiener with token 5678
But token 5678 is now linked to carlos!
Wiener uses token โ resets CARLOS's password! ๐Request 1 (wiener) โโโโโโโโโโโโโโโโโโโโ token=5678
Request 2 (carlos) โโโโโ token=1234
โ MIX HAPPENS HERE!
Final state:
reset-user = carlos โ from request 2
reset-token = 5678 โ from request 1 (overwrote!)
Email sent to wiener with token 5678
But token 5678 is now linked to carlos!
Wiener uses token โ resets CARLOS's password! ๐How to execute in Burp:
Step 1: Login as wiener โ get session cookie
Step 2: Go to password reset โ enter wiener โ intercept in Burp
Step 3: Send to Repeater
Step 4: Duplicate tab โ change username to carlos in copy
Step 5: Group both tabs
Step 6: Send group in parallel
Step 7: Check wiener's email โ did you receive a token?
Step 8: Use that token on the reset URL โ it resets carlos!Step 1: Login as wiener โ get session cookie
Step 2: Go to password reset โ enter wiener โ intercept in Burp
Step 3: Send to Repeater
Step 4: Duplicate tab โ change username to carlos in copy
Step 5: Group both tabs
Step 6: Send group in parallel
Step 7: Check wiener's email โ did you receive a token?
Step 8: Use that token on the reset URL โ it resets carlos!Why this works:
The vulnerability is in the storage pattern:
Vulnerable โ session storage:
session['reset-user'] = carlos โ can be overwritten!
session['reset-token'] = 1234 โ can be overwritten!
Parallel requests write to SAME session โ collision!
Safe โ database storage:
DB.save(user=carlos, token=1234) โ atomic operation
Each token linked directly to user in DB
No shared session โ no collision!Vulnerable โ session storage:
session['reset-user'] = carlos โ can be overwritten!
session['reset-token'] = 1234 โ can be overwritten!
Parallel requests write to SAME session โ collision!
Safe โ database storage:
DB.save(user=carlos, token=1234) โ atomic operation
Each token linked directly to user in DB
No shared session โ no collision!Email-based operations are especially vulnerable:
Emails are often sent in a background thread after the HTTP response is returned. This means the race window is larger than usual โ the server has already responded to your request before the email is even sent. More time for collision.
Session-Based Locking โ Detecting and Bypassing
Some frameworks prevent race conditions by only processing one request per session at a time. PHP's native session handler does this by default.
How to detect session locking:
No locking (good for attacks):
Request 1 โ responds at 200ms
Request 2 โ responds at 201ms โ almost same time!
Session locking (bad for attacks):
Request 1 โ responds at 200ms
Request 2 โ responds at 400ms โ waited for request 1!
Request 3 โ responds at 600ms โ waited for both!
Sequential timing = session locking detected โNo locking (good for attacks):
Request 1 โ responds at 200ms
Request 2 โ responds at 201ms โ almost same time!
Session locking (bad for attacks):
Request 1 โ responds at 200ms
Request 2 โ responds at 400ms โ waited for request 1!
Request 3 โ responds at 600ms โ waited for both!
Sequential timing = session locking detected โThe bypass โ use different session tokens:
Instead of:
Request 1 โ session=ABC (locked!)
Request 2 โ session=ABC (waiting!)
Use:
Request 1 โ session=ABC โ different sessions!
Request 2 โ session=DEF โ process simultaneously!Instead of:
Request 1 โ session=ABC (locked!)
Request 2 โ session=ABC (waiting!)
Use:
Request 1 โ session=ABC โ different sessions!
Request 2 โ session=DEF โ process simultaneously!Important caveat:
Session locking only stops attacks that use session storage. If the vulnerable data is stored in a database โ different sessions still write to the same database record. Session locking is irrelevant.
Session storage โ session locking stops the attack โ
Database storage โ different sessions โ race still works โ
Session storage โ session locking stops the attack โ
Database storage โ different sessions โ race still works โ
Summary
HIDDEN SUB-STATES
โ Single requests trigger multiple backend operations
โ Gap between steps = sub-state = race window
โ Find by triggering errors and watching for
unexpected responses
MULTI-ENDPOINT ATTACKS
โ Send requests to different endpoints simultaneously
โ Classic: add item during payment validation window
โ Fix timing with connection warming or rate limit abuse
SINGLE-ENDPOINT ATTACKS
โ Parallel requests with different values interfere
โ Session storage is vulnerable โ overwrites possible
โ Email-based operations have larger windows
โ Detect session locking by checking response timing
โ Bypass by using different session tokens
WHAT TO LOOK FOR
โ Endpoints that store data in sessions
โ Multi-step processes in single requests
โ Email operations (sent in background threads)
โ Payment + order confirmation flows
โ Any shared mutable stateHIDDEN SUB-STATES
โ Single requests trigger multiple backend operations
โ Gap between steps = sub-state = race window
โ Find by triggering errors and watching for
unexpected responses
MULTI-ENDPOINT ATTACKS
โ Send requests to different endpoints simultaneously
โ Classic: add item during payment validation window
โ Fix timing with connection warming or rate limit abuse
SINGLE-ENDPOINT ATTACKS
โ Parallel requests with different values interfere
โ Session storage is vulnerable โ overwrites possible
โ Email-based operations have larger windows
โ Detect session locking by checking response timing
โ Bypass by using different session tokens
WHAT TO LOOK FOR
โ Endpoints that store data in sessions
โ Multi-step processes in single requests
โ Email operations (sent in background threads)
โ Payment + order confirmation flows
โ Any shared mutable statePart 3 coming soon โ Partial construction race conditions, time-sensitive attacks, and how to prevent all of this.
Written by // l1m1nal_3ntr0py โ documenting my cybersecurity journey from scratch
Follow my journey:
- ๐ฆ Twitter: @l1m1nal_3ntr0py
- ๐ GitHub: l1m1nal-3ntr0py
- ๐ HackerOne: nithig