High Severity CWE-362 OWASP A04:2021

Race Conditions: How Async Code Creates Security Holes AI Misses

Timing attacks that let attackers bypass limits and duplicate transactions

Quick Answer

Race conditions let attackers exploit timing gaps between checking a condition and acting on it. Send 10 requests simultaneously to redeem a gift card 10 times. AI generates async code without proper locking, making this common in vibe coded apps. Fix: use database transactions and atomic operations. Classified as CWE-362.

CWE-362
Classification
High
Severity
100+
CVEs Per Year
Hard
Detection Difficulty

What is a Race Condition?

A race condition occurs when two or more operations access shared resources concurrently, and the final result depends on timing. In web applications, this creates security vulnerabilities when attackers can exploit the gap between checking a condition and acting on it - known as the CHECK-USE gap.

Think of it like two people trying to book the last airline seat simultaneously. Both see "1 seat available," both click "Book," and both succeed - even though only one seat exists. In code, this manifests when you check a value (is balance sufficient?), then act on it (deduct balance) - but another request slips in between.

Race Window Example
Normal single-user flow:
1. CHECK: Is coupon valid? → Yes
2. USE: Apply discount
3. UPDATE: Mark coupon as used

Race condition attack (two simultaneous requests):
Request A: CHECK (valid) → USE → UPDATE
Request B: CHECK (still valid!) → USE → UPDATE
                    ↑
              Gap exploited - coupon used twice

Race conditions fall under OWASP A04:2021 Insecure Design. A specific variant called TOCTOU (Time-of-check Time-of-use), documented as CWE-367, is particularly common in file operations and web applications.

Real-World Attack Examples

Race conditions enable financial fraud, privilege escalation, and security bypass. These attacks are effective because the vulnerability only appears under concurrent load - normal testing misses them entirely.

Gift Card Double Redemption

  1. Attacker has a $100 gift card code
  2. Opens 10 browser tabs to the redemption page
  3. Clicks "Redeem" simultaneously in all 10 tabs
  4. All 10 requests pass the "is valid" check before any marks it used
  5. Result: $100 gift card redeemed 10 times = $1,000 store credit

Rate Limit Bypass

  1. API allows 5 login attempts per minute
  2. Attacker sends 100 login requests simultaneously
  3. All requests hit the server before the rate counter updates
  4. 100 password guesses processed instead of 5
  5. This enables brute-force attacks on access control

Notable CVEs

  • CVE-2024-50379 - Apache Tomcat RCE: TOCTOU race condition allowing remote code execution via file handling
  • CVE-2025-68146 - Python filelock: Symlink attack via TOCTOU race condition in file locking library
  • CVE-2021-0920 - Android Kernel: Use-after-free via race condition, actively exploited in the wild

Why AI Tools Generate Vulnerable Code

AI coding tools like Cursor and Claude Code generate async code that works perfectly for single users but breaks under concurrent load. The issue: AI focuses on functional correctness without considering concurrent access patterns.

AI-Generated (Vulnerable)
// "Redeem coupon" - AI's typical output
app.post('/redeem', async (req, res) => {
  const coupon = await Coupon.findOne({
    code: req.body.code
  })

  if (!coupon || coupon.used) {
    return res.status(400).json({
      error: 'Invalid coupon'
    })
  }

  // RACE WINDOW: Another request passes
  // the check above while we're here

  await applyDiscount(req.user, coupon.discount)
  coupon.used = true
  await coupon.save()

  res.json({ success: true })
})
Secure Pattern
// Atomic operation with database transaction
app.post('/redeem', async (req, res) => {
  const result = await db.transaction(async (tx) => {
    // Atomic check-and-update in one query
    const coupon = await tx.coupon.update({
      where: {
        code: req.body.code,
        used: false  // Condition in the update
      },
      data: { used: true }
    })

    if (!coupon) {
      throw new Error('Invalid or already used')
    }

    await tx.discount.create({
      data: {
        userId: req.user.id,
        amount: coupon.discount
      }
    })

    return coupon
  })

  res.json({ success: true })
})

Why This Happens

  • Training data: Most code samples are single-user scenarios
  • await creates gaps: Every await point is a potential race window
  • No concurrency model: AI doesn't think about parallel requests
  • "It works" testing: Single-user tests always pass

Secure Patterns

The solution is always the same principle: eliminate the gap between check and use. Here are concrete patterns for common scenarios.

Fix 1: Database Transactions

Wrap related operations in a transaction. With Prisma:

// SECURE: Atomic transaction
async function withdrawFunds(userId, amount) {
  await prisma.$transaction(async (tx) => {
    const user = await tx.user.findUnique({
      where: { id: userId }
    })

    if (user.balance < amount) {
      throw new Error('Insufficient balance')
    }

    await tx.user.update({
      where: { id: userId },
      data: { balance: { decrement: amount } }
    })

    await tx.transaction.create({
      data: { userId, amount, type: 'withdrawal' }
    })
  })
}

Fix 2: Atomic Update with Condition

Combine check and update into single query. With MongoDB:

// SECURE: Single atomic operation
async function withdrawFunds(userId, amount) {
  const result = await User.updateOne(
    {
      _id: userId,
      balance: { $gte: amount }  // Condition in query
    },
    {
      $inc: { balance: -amount }  // Atomic decrement
    }
  )

  if (result.modifiedCount === 0) {
    throw new Error('Insufficient balance or user not found')
  }
}

Fix 3: Database Unique Constraints

Let the database enforce uniqueness - don't check in application code:

// SECURE: Database enforces uniqueness
async function redeemCoupon(code, userId) {
  try {
    await Redemption.create({
      couponCode: code,
      userId
    })
  } catch (error) {
    if (error.code === 11000) { // MongoDB duplicate key
      throw new Error('Coupon already redeemed')
    }
    throw error
  }
}

Testing for Race Conditions

Race conditions are notoriously hard to find because they depend on timing. Normal testing with a single user will never reveal them. The PortSwigger race conditions guide covers testing techniques in depth.

Simple Test Script

// Simple race condition test
async function testRaceCondition() {
  const requests = Array(20).fill().map(() =>
    fetch('/api/redeem', {
      method: 'POST',
      body: JSON.stringify({ code: 'GIFT100' }),
      headers: { 'Content-Type': 'application/json' }
    })
  )

  const results = await Promise.all(requests)
  const successes = results.filter(r => r.ok).length

  console.log(`${successes} successful redemptions`)
  // If > 1, race condition exists
}

Code Review Patterns

Search your codebase for these patterns:

  • if (await ...check...) {...await ...update...} - check-then-act
  • findById followed by save() - read-modify-write
  • Rate limiting without atomic counters
  • Any code that checks availability, then reserves

AI Fix Prompt for Race Conditions

Copy this prompt to your AI coding tool to audit your codebase for race condition vulnerabilities:

Race Condition Audit Prompt
Review my codebase for Race Condition vulnerabilities (CWE-362): ## Check 1: Check-Then-Act Patterns Search for patterns where validation precedes modification: - if (condition) { ...await update... } - Finding a record, checking a value, then updating - Balance/inventory/limit checks before modifications - Coupon/discount validation before applying Flag: Any gap between validation and state change ## Check 2: Non-Atomic Operations Look for read-modify-write patterns: - const value = await Model.findById(id) - value.field += 1 (or -= 1) - await value.save() Flag: Any increment/decrement that isn't atomic ($inc, increment, etc.) ## Check 3: Missing Transactions For operations that must be atomic: - Are they wrapped in database transactions? - Do they use atomic update operators ($inc, increment)? - Is optimistic locking (version field) implemented? - Is pessimistic locking (SELECT FOR UPDATE) used? ## Check 4: Rate Limiting Implementation If rate limiting exists: - Is the counter updated atomically? - Can concurrent requests bypass the limit? - Is Redis INCR or atomic database operation used? ## Check 5: Resource Reservation For booking/reservation systems: - Is availability checked and updated atomically? - Can two users book the same resource simultaneously? - Are unique constraints enforced at database level? ## Secure Patterns to Apply For balance/inventory operations: ```javascript // Atomic decrement with condition const result = await User.updateOne( { _id: id, balance: { $gte: amount } }, { $inc: { balance: -amount } } ) if (result.modifiedCount === 0) throw new Error('Insufficient') ``` For unique operations (coupon redemption): ```javascript // Use unique constraint - let database enforce try { await Redemption.create({ couponCode, userId }) } catch (e) { if (e.code === 11000) throw new Error('Already redeemed') } ``` For multi-step operations: ```javascript // Wrap in transaction await prisma.$transaction(async (tx) => { const item = await tx.item.findUnique({ where: { id } }) if (item.quantity < 1) throw new Error('Out of stock') await tx.item.update({ where: { id }, data: { quantity: { decrement: 1 } } }) await tx.order.create({ data: { itemId: id, userId } }) }) ``` For PostgreSQL pessimistic locking: ```sql BEGIN; SELECT * FROM items WHERE id = $1 FOR UPDATE; UPDATE items SET quantity = quantity - 1 WHERE id = $1; COMMIT; ``` For each vulnerability found: 1. Identify the race window (gap between check and use) 2. Explain the attack scenario (e.g., 10 simultaneous requests) 3. Show how concurrent requests could exploit it 4. Provide atomic replacement pattern

This prompt guides Cursor, Claude Code, or other AI tools through systematic detection of check-then-act patterns, non-atomic operations, and missing transaction boundaries.

Frequently Asked Questions

What is a race condition vulnerability?

A race condition vulnerability occurs when two or more operations access shared data concurrently, and the final result depends on timing. In web applications, this means attackers can send multiple requests simultaneously to exploit the gap between checking a condition and acting on it. For example, redeeming a gift card 10 times by clicking simultaneously in 10 browser tabs.

How do attackers exploit race conditions?

Attackers send multiple requests simultaneously to hit the vulnerable window before state updates. Tools like Burp Suite's Turbo Intruder can send hundreds of requests in parallel. The single-packet attack technique bundles multiple HTTP requests into one TCP packet, ensuring they arrive at the same instant. Common targets: gift card redemption, discount codes, rate limits, and balance transfers.

How do I prevent race conditions in Node.js?

Use database transactions and atomic operations. With Prisma: prisma.$transaction(). With MongoDB: use $inc operator and conditions in queries. For critical operations, use database-level unique constraints to prevent duplicates. Never separate check from update - combine them into single atomic operations like UPDATE WHERE balance >= amount.

What is TOCTOU?

TOCTOU (Time-of-check Time-of-use) is a specific type of race condition (CWE-367) where a resource is checked and then used, but changes between those two steps. Example: checking if a file exists, then reading it - an attacker could swap the file between check and read. In web apps, this happens when validating data in one query and using it in another.

Are race conditions hard to detect?

Yes, race conditions are notoriously difficult to detect because they depend on timing and may not appear in normal testing. Single-user testing almost never reveals them. You need concurrent load testing or specialized tools. Code review patterns to look for: check-then-act sequences, non-atomic increments, and any gap between reading and writing shared state.

Related Security Topics

Scan Your Code for Race Conditions

vibeship scanner detects check-then-act patterns, non-atomic operations, and other race condition vulnerabilities in your AI-generated code.

Scan Your Repository