Race Conditions

TOCTOUConcurrency Bugs

Race Conditions at a glance

What it is: Two or more operations run concurrently and interleave in a way that violates assumptions, for example read-modify-write on shared state.
Why it happens: Race conditions occur when concurrent actions like inventory updates, coupon use, payments, or file access lack proper synchronization, especially in microservices with retries or eventual consistency.
How to fix: Use atomic updates with constraints or locks, wrap operations in transactions, and apply idempotency keys and deduplication to prevent duplicate or conflicting actions.

Overview

Race conditions happen when code reads state, makes a decision, and writes new state while another concurrent request does the same. Time-of-check to time-of-use bugs allow bypassing authorization and business limits. Without atomic operations, locks, or idempotency, systems can double charge, oversell inventory, or leak files.

sequenceDiagram participant Browser participant App as App Server participant DB as Database Browser->>App: POST /buy {cost:50} (x2 in parallel) App->>DB: READ user.credits App->>DB: UPDATE user.credits = credits - 50 (twice) DB-->>App: Both updates succeed App-->>Browser: Two 200 OK responses note over App,DB: Read-modify-write without atomic predicate
A potential flow for a Race Conditions exploit

Where it occurs

It occurs in operations like inventory updates, coupon use, payments, or file downloads that lack proper concurrency controls, especially in microservices using retries or eventual consistency.

Impact

Financial loss from double charges or oversells, privilege escalation when checks and uses are split, data corruption, and confusing customer states that are hard to recover.

Prevention

Prevent race conditions by using atomic or transactional updates with proper isolation, idempotency keys, and deduplication, keeping checks and actions in the same critical section and binding operations to resolved resource handles.

Examples

Switch tabs to view language/framework variants.

Concurrent purchases allow credit balance to go negative

Two requests read the same balance and both succeed because update is non-atomic.

Vulnerable
JavaScript • Express + Mongoose — Bad
app.post('/buy', async (req,res)=>{
  const user = await User.findById(req.user._id); // stale
  const cost = Number(req.body.cost||0);
  if (user.credits >= cost) {
    user.credits -= cost; // race window
    await user.save();
    return res.json({ok:true});
  }
  res.status(402).json({ok:false});
});
  • Line 2: Separate read and write create a race window

Two concurrent handlers read the same balance and both decrement, causing an overdraft.

Secure
JavaScript • Express + Mongoose — Good
app.post('/buy', async (req,res)=>{
  const cost = Number(req.body.cost||0);
  const upd = await User.updateOne({ _id:req.user._id, credits: { $gte: cost } }, { $inc: { credits: -cost } });
  if (upd.modifiedCount === 1) return res.json({ok:true});
  return res.status(402).json({ok:false});
});
  • Line 2: Atomic conditional update ensures only one succeeds

Use a single atomic compare-and-update (findAndModify, update with predicate, or a transaction with proper isolation).

Engineer Checklist

  • Replace read-then-write with atomic updates or transactions

  • Use SELECT ... FOR UPDATE or document-level locks where supported

  • Introduce idempotency keys and dedupe for external actions

  • Keep authorization check and resource use inside the same critical section

  • Test with parallel clients and chaos tooling to surface races

End-to-End Example

A wallet service decrements credits after a separate read of the balance. Attackers send two purchase requests simultaneously. Both succeed and the balance goes negative.

Vulnerable
JAVASCRIPT
// Node.js/Express + MongoDB - Vulnerable race condition

app.post('/api/purchase', authenticateToken, async (req, res) => {
  try {
    const userId = req.user.id;
    const itemCost = parseFloat(req.body.cost);
    
    // VULNERABLE: Read-then-write without atomic operation
    // Step 1: Read current balance
    const user = await User.findById(userId);
    const currentBalance = user.credits;
    
    // Step 2: Check if user has enough credits
    if (currentBalance < itemCost) {
      return res.status(400).json({ error: 'Insufficient credits' });
    }
    
    // TIME GAP: Another request can execute here!
    // If two requests run in parallel, both pass the check
    // even if balance is only enough for one purchase
    
    // Step 3: Deduct credits
    const newBalance = currentBalance - itemCost;
    await User.findByIdAndUpdate(userId, { credits: newBalance });
    
    // Create purchase record
    await Purchase.create({ userId, cost: itemCost });
    
    res.json({ success: true, newBalance });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

// ALSO VULNERABLE: Coupon redemption without atomic check
app.post('/api/redeem-coupon', authenticateToken, async (req, res) => {
  const code = req.body.code;
  const userId = req.user.id;
  
  // VULNERABLE: Check and use in separate operations
  const coupon = await Coupon.findOne({ code });
  
  if (!coupon || coupon.used) {
    return res.status(400).json({ error: 'Invalid coupon' });
  }
  
  // RACE WINDOW: Multiple requests can all pass the check!
  // before any of them marks it as used
  
  await Coupon.findByIdAndUpdate(coupon._id, { used: true, usedBy: userId });
  await User.findByIdAndUpdate(userId, { $inc: { credits: coupon.value } });
  
  res.json({ success: true, creditsAdded: coupon.value });
});
Secure
JAVASCRIPT
// Node.js/Express + MongoDB - SECURE with atomic operations

app.post('/api/purchase', authenticateToken, async (req, res) => {
  try {
    const userId = req.user.id;
    const itemCost = parseFloat(req.body.cost);
    
    // SECURE: Atomic update with condition
    // MongoDB's findOneAndUpdate with condition ensures atomicity
    const result = await User.findOneAndUpdate(
      {
        _id: userId,
        credits: { $gte: itemCost }  // Condition: only update if enough credits
      },
      {
        $inc: { credits: -itemCost }  // Atomic decrement
      },
      {
        new: true  // Return updated document
      }
    );
    
    // If result is null, condition failed (insufficient credits or race)
    if (!result) {
      return res.status(400).json({ error: 'Insufficient credits' });
    }
    
    // Create purchase record
    await Purchase.create({ userId, cost: itemCost });
    
    res.json({ success: true, newBalance: result.credits });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

// SECURE: Coupon redemption with atomic check-and-set
app.post('/api/redeem-coupon', authenticateToken, async (req, res) => {
  const code = req.body.code;
  const userId = req.user.id;
  
  // SECURE: Atomic update - only marks as used if currently unused
  const coupon = await Coupon.findOneAndUpdate(
    {
      code: code,
      used: false  // Condition: only update if not already used
    },
    {
      $set: { used: true, usedBy: userId, usedAt: new Date() }
    },
    { new: true }
  );
  
  // If coupon is null, it was already used (race lost) or doesn't exist
  if (!coupon) {
    return res.status(400).json({ error: 'Invalid or already used coupon' });
  }
  
  // Atomic credit addition
  await User.findByIdAndUpdate(userId, { $inc: { credits: coupon.value } });
  
  res.json({ success: true, creditsAdded: coupon.value });
});

// ALTERNATIVE: Using transactions for multiple operations
app.post('/api/transfer-credits', authenticateToken, async (req, res) => {
  const session = await mongoose.startSession();
  session.startTransaction();
  
  try {
    const fromUserId = req.user.id;
    const toUserId = req.body.toUserId;
    const amount = parseFloat(req.body.amount);
    
    // Atomic debit
    const fromUser = await User.findOneAndUpdate(
      { _id: fromUserId, credits: { $gte: amount } },
      { $inc: { credits: -amount } },
      { session, new: true }
    );
    
    if (!fromUser) {
      await session.abortTransaction();
      return res.status(400).json({ error: 'Insufficient credits' });
    }
    
    // Atomic credit
    await User.findByIdAndUpdate(
      toUserId,
      { $inc: { credits: amount } },
      { session }
    );
    
    await session.commitTransaction();
    res.json({ success: true });
  } catch (err) {
    await session.abortTransaction();
    res.status(500).json({ error: 'Transfer failed' });
  } finally {
    session.endSession();
  }
});

Discovery

This vulnerability is discovered by sending concurrent requests to endpoints that modify shared state (like balance transfers, coupon redemptions, or resource creation) and observing inconsistent results that indicate lack of proper synchronization.

  1. 1. Baseline sequential request test

    http

    Action

    Perform normal sequential operations to establish expected state transitions

    Request

    POST https://app.example.com/api/purchase
    Headers:
    Content-Type: application/json
    Authorization: Bearer user_token_abc
    Body:
    {
      "item_id": "123",
      "quantity": 1,
      "cost": 50
    }

    Response

    Status: 200
    Body:
    {
      "message": "Purchase successful",
      "balance_before": 100,
      "balance_after": 50,
      "transaction_id": "txn_001",
      "note": "Sequential operation correctly decrements balance by 50"
    }

    Artifacts

    http_response_status account_balance transaction_record
  2. 2. Concurrent request race detection

    http

    Action

    Send multiple identical requests simultaneously to test for race conditions

    Request

    POST https://app.example.com/api/purchase
    Headers:
    Content-Type: application/json
    Authorization: Bearer user_token_abc
    Body:
    {
      "item_id": "456",
      "quantity": 1,
      "cost": 50,
      "note": "Send 3 identical requests in parallel with balance=100"
    }

    Response

    Status: 200
    Body:
    {
      "parallel_results": [
        {
          "request": 1,
          "status": "success",
          "balance_read": 100,
          "balance_written": 50
        },
        {
          "request": 2,
          "status": "success",
          "balance_read": 100,
          "balance_written": 50
        },
        {
          "request": 3,
          "status": "success",
          "balance_read": 100,
          "balance_written": 50
        }
      ],
      "final_balance": 50,
      "expected_balance": -50,
      "note": "Race condition! All 3 requests read balance=100 and wrote 50. Final balance should be -50 but is 50 - lost 2 updates!"
    }

    Artifacts

    http_response_bodies final_balance transaction_count database_audit_log
  3. 3. Coupon/voucher reuse test

    http

    Action

    Attempt to redeem same coupon code in parallel requests

    Request

    POST https://app.example.com/api/redeem-coupon
    Headers:
    Content-Type: application/json
    Authorization: Bearer user_token_xyz
    Body:
    {
      "coupon_code": "SAVE20",
      "order_id": "order_789",
      "note": "Send 5 parallel redemption requests"
    }

    Response

    Status: 200
    Body:
    {
      "redemption_results": {
        "parallel_requests": 5,
        "successful_redemptions": 5,
        "expected_redemptions": 1,
        "total_discount_applied": "$100",
        "expected_discount": "$20",
        "revenue_loss": "$80"
      },
      "note": "Single-use coupon redeemed 5 times due to TOCTOU race in validation!"
    }

    Artifacts

    redemption_records discount_applied_count database_state
  4. 4. TOCTOU file access race

    http

    Action

    Test time-of-check to time-of-use vulnerability in file access controls

    Request

    GET https://app.example.com/api/download?file_id=sensitive_doc_456

    Response

    Status: 200
    Body:
    {
      "file_access_sequence": [
        {
          "step": 1,
          "action": "permission_check",
          "timestamp": "2024-01-15T10:00:00.100Z",
          "result": "access_granted",
          "file_owner": "attacker_user"
        },
        {
          "step": 2,
          "action": "symlink_swap",
          "timestamp": "2024-01-15T10:00:00.150Z",
          "note": "Attacker replaces file_id=456 with symlink to /etc/passwd during 50ms gap"
        },
        {
          "step": 3,
          "action": "file_read",
          "timestamp": "2024-01-15T10:00:00.200Z",
          "content": "root:x:0:0:root:/root:/bin/bash\npostgres:x:999:999::/var/lib/postgresql:/bin/sh",
          "note": "Read sensitive system file due to TOCTOU race"
        }
      ],
      "vulnerability": "Permission checked on attacker-owned file, but content read from swapped target"
    }

    Artifacts

    file_access_logs permission_check_timing file_content

Exploit steps

An attacker exploits this by sending parallel requests that exploit timing windows between check-and-use operations, allowing actions like redeeming the same coupon multiple times, withdrawing more funds than available, or bypassing rate limits.

  1. 1. Double-spend attack via concurrent purchases

    Exploit balance race condition

    http

    Action

    Send parallel purchase requests to spend same balance multiple times

    Request

    POST https://app.example.com/api/purchase
    Headers:
    Content-Type: application/json
    Authorization: Bearer attacker_token
    Body:
    {
      "item_id": "premium_item",
      "cost": 100,
      "note": "Send 3 parallel requests with balance=100"
    }

    Response

    Status: 200
    Body:
    {
      "attack_results": {
        "starting_balance": 100,
        "parallel_purchases": 3,
        "items_obtained": [
          "premium_item_copy_1",
          "premium_item_copy_2",
          "premium_item_copy_3"
        ],
        "total_cost": 300,
        "final_balance": -100,
        "fraud_value": "$200"
      },
      "note": "Successfully purchased $300 worth of items with only $100 balance via race condition"
    }

    Artifacts

    transaction_records negative_balance purchased_items
  2. 2. Coupon abuse through parallel redemption

    Redeem single-use coupon multiple times

    http

    Action

    Exploit TOCTOU in coupon validation to reuse promotional codes

    Request

    POST https://app.example.com/api/apply-discount
    Headers:
    Content-Type: application/json
    Authorization: Bearer attacker_token
    Body:
    {
      "coupon": "ONETIME50",
      "order_id": "order_12345",
      "note": "Send 10 parallel requests"
    }

    Response

    Status: 200
    Body:
    {
      "coupon_abuse_results": {
        "coupon_code": "ONETIME50",
        "intended_discount": "$50",
        "redemptions": 10,
        "total_discount": "$500",
        "original_order_value": "$1000",
        "final_order_value": "$500",
        "revenue_loss": "$450"
      },
      "note": "Single-use $50 coupon redeemed 10 times, saving $500 instead of $50"
    }

    Artifacts

    discount_applications final_price coupon_usage_count
  3. 3. Inventory overselling via concurrent orders

    Purchase limited inventory concurrently

    http

    Action

    Exploit race in inventory checks to oversell limited stock items

    Request

    POST https://app.example.com/api/order
    Headers:
    Content-Type: application/json
    Body:
    {
      "product_id": "limited_edition",
      "quantity": 1,
      "note": "Product has stock=1, send 10 parallel orders"
    }

    Response

    Status: 200
    Body:
    {
      "overselling_results": {
        "product": "limited_edition",
        "available_stock": 1,
        "successful_orders": 10,
        "oversold_quantity": 9,
        "confirmed_shipments": [
          "order_001",
          "order_002",
          "order_003",
          "order_004",
          "order_005",
          "order_006",
          "order_007",
          "order_008",
          "order_009",
          "order_010"
        ],
        "business_impact": "9 customers will not receive items, support tickets incoming"
      },
      "note": "Race condition allowed 10 orders for 1 item in stock"
    }

    Artifacts

    order_confirmations inventory_count oversold_quantity
  4. 4. Privilege escalation via role assignment race

    Exploit concurrent permission checks

    http

    Action

    Race between permission check and role assignment to escalate privileges

    Request

    POST https://app.example.com/api/grant-role
    Headers:
    Content-Type: application/json
    Authorization: Bearer attacker_token
    Body:
    {
      "user_id": "attacker_123",
      "role": "admin",
      "note": "Send 5 parallel role grant requests"
    }

    Response

    Status: 200
    Body:
    {
      "privilege_escalation": {
        "original_role": "user",
        "requested_role": "admin",
        "parallel_requests": 5,
        "race_window": "15ms between permission check and role write",
        "final_role": "admin",
        "admin_panel_access": "https://app.example.com/admin",
        "note": "TOCTOU in authorization check allowed unprivileged user to grant admin role"
      }
    }

    Artifacts

    user_roles permission_escalation audit_logs

Specific Impact

Customers obtain goods or services without being charged correctly. Accounting and reconciliation are wrong and support volume increases.

Fixing negative balances requires manual adjustments and undermines trust in the billing system.

Fix

Perform a single conditional update or hold row locks during the check and decrement. Add idempotency keys to external actions and keep checks and mutations in one transaction.

Detect This Vulnerability in Your Code

Sourcery automatically identifies race conditions vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free