What Is a Race Condition?

SecurityForEveryone

S4E.io

14/Oct/25

1. Introduction

In modern software systems, the use of multiple threads or processes offers major performance benefits, but improper management can lead to unexpected security vulnerabilities.

One of the most well-known examples of such an issue is the race condition vulnerability.

A race condition occurs when multiple threads or processes try to access the same shared resource concurrently, causing the system to behave unpredictably.

The problem arises when two operations depend on each other’s results but are executed without proper synchronization.

As a result, the system may enter an inconsistent state, data may be lost, or attackers may exploit the timing gap to perform unauthorized actions.

2. How Does a Race Condition Occur?

A race condition occurs when a system tries to access the same resource (e.g., a file, database record, or API operation) from multiple points at the same time.

The difference in timing between these accesses becomes security-critical.

3. Vulnerable Code Example

In the following Python example, two threads attempt to withdraw money from the same account simultaneously.

Because there is no locking mechanism, a race condition occurs.

import threading
import time

balance = 1000  # Account balance

def withdraw(amount):
    global balance
    if balance >= amount:
        print(f"[{threading.current_thread().name}] Sufficient balance, withdrawing...")
        time.sleep(1)  # Artificial delay (triggers race condition)
        balance -= amount
        print(f"[{threading.current_thread().name}] New balance: {balance}")
    else:
        print(f"[{threading.current_thread().name}] Insufficient funds!")

t1 = threading.Thread(target=withdraw, args=(1000,), name="Thread-1")
t2 = threading.Thread(target=withdraw, args=(1000,), name="Thread-2")

t1.start()
t2.start()
t1.join()
t2.join()

Expected Result

In theory, only one thread should successfully complete the withdrawal.

However, since both threads pass the “sufficient balance” check simultaneously, the result may end up being balance = -1000, indicating a race condition vulnerability.

4. Secure Code Example

To fix this issue, we must lock the critical section of code (the part inside the withdraw() function).

By using threading.Lock(), only one thread is allowed to access that section at a time.

import threading
import time

balance = 1000
lock = threading.Lock()  # Create a lock

def withdraw(amount):
    global balance
    with lock:  # Critical section: only one thread can enter
        if balance >= amount:
            print(f"[{threading.current_thread().name}] Sufficient balance, withdrawing...")
            time.sleep(1)
            balance -= amount
            print(f"[{threading.current_thread().name}] New balance: {balance}")
        else:
            print(f"[{threading.current_thread().name}] Insufficient funds!")

t1 = threading.Thread(target=withdraw, args=(1000,), name="Thread-1")
t2 = threading.Thread(target=withdraw, args=(1000,), name="Thread-2")

t1.start()
t2.start()
t1.join()
t2.join()

Result

The with lock: statement ensures that only one thread can access the critical section at a time.

This eliminates the race condition and preserves data integrity.

5. Real-World Example

Consider an e-commerce platform handling product sales for limited stock items.

When there is only one item left, and two users attempt to purchase it at the same time, the following can happen:

  1. User A clicks “Buy Now” — the system reads stock = 1.
  2. Simultaneously, User B clicks “Buy Now” — the system also reads stock = 1.
  3. Both transactions proceed to the “decrease stock by 1” step.
  4. The result: stock becomes -1, or both users receive a “purchase successful” message.

This leads to overselling, data inconsistencies, and financial mismatches.

The root cause is that the stock update operation is not atomic.

Recommended Mitigations

  • Use transactions and row-level locking in the database.
  • Implement optimistic concurrency control for version checks.
  • Design APIs to be idempotent (safe to call multiple times).
  • Use distributed locking mechanisms (e.g., Redis locks) for critical operations.

6. Race Condition Scanning with S4E

S4E’s Create with AI feature can be used to automatically generate dynamic testing scenarios.

This capability allows you to easily simulate and detect timing-based vulnerabilities like race conditions.

The following example prompt can be used to detect a potential race condition in a coupon redemption workflow:

Send concurrent requests to https://example.com/apply-coupon using the DECEMBER50 coupon code. Attempt to exploit a race condition that allows the same coupon to be applied twice. Report success if the coupon is used more than once (e.g., total discount applied > 50%).

This prompt sends concurrent requests to test whether the same coupon can be applied multiple times.

S4E automatically manages the process and generates a finding if a vulnerability is detected.

Example Python code using S4E Create with AI

Python code snippet demonstrating an AI-based scan generator form from S4E that tests race condition vulnerabilities using concurrent HTTP requests.

7. Conclusion

Race conditions often appear sporadically and may not surface during testing, but they can cause severe issues in production environments.

Therefore:

  • Any function involving concurrent access should be treated as a critical section,
  • Concurrency controls should be reviewed in every code audit,
  • Performance tuning should never come at the cost of security.

In short, preventing race conditions requires continuous diligence. Proper synchronization, comprehensive testing, and regular code reviews are essential to minimize the risk.

cyber security services for everyone one. Free security tools, continuous vulnerability scanning and many more.
Try it yourself,
control security posture