HIGH race conditionexpresscockroachdb

Race Condition in Express with Cockroachdb

Race Condition in Express with Cockroachdb — how this specific combination creates or exposes the vulnerability

A race condition in an Express service using CockroachDB typically arises when multiple concurrent requests read and write shared data without strict serialization, violating linearizable consistency expectations for certain operations. CockroachDB provides strong ACID guarantees and serializable isolation by default, but application-level logic can still introduce races if correctness depends on read-modify-write cycles without explicit synchronization.

Consider an Express endpoint that reads a row, computes a new value, and writes it back. Between the read and the write, another request can observe and modify the same row, resulting in lost updates or inconsistent state. This pattern is common in inventory or balance adjustments, where a non-atomic increment/decrement is expressed as SELECT followed by UPDATE. CockroachDB’s serializable isolation detects write conflicts and aborts one transaction, but from an API perspective this may manifest as a 409 or transient error if retry logic is absent, or it may silently produce incorrect results if the application incorrectly assumes the first write is final.

An example vulnerable flow: an endpoint /api/account/:id/deposit reads balance, adds amount, and writes back. Two simultaneous deposits can both read the same balance, each add their amount, and write back, causing one update to be overwritten. CockroachDB will not corrupt data due to its transaction model, but the business logic becomes nondeterministic under concurrency because the transaction does not isolate the read–modify–write as a single atomic step.

SSRF and input validation findings from middleBrick can highlight endpoints that perform complex read–write workflows on user-supplied identifiers, increasing the chance of contention paths that expose race conditions. In combination with BOLA/IDOR issues, an attacker may force targeted account IDs to amplify collision likelihood. The 12 checks running in parallel do not alter runtime behavior, but they help identify endpoints with unsafe consumption patterns and missing authorization that can make race conditions easier to trigger and observe.

Remediation focuses on making the read-modify-write atomic at the database level, using explicit isolation or conditional writes, and ensuring the Express transaction lifecycle aligns with CockroachDB’s serializable guarantees. Relying on application-level locks or simple retries without correct transaction boundaries is insufficient.

Cockroachdb-Specific Remediation in Express — concrete code fixes

To eliminate race conditions in Express with CockroachDB, structure transactions so that all read–modify–write steps occur within a single serializable transaction, and use conditional writes when possible. The following patterns are recommended and include real, syntactically correct code examples.

1) Atomic increment/decrement in a transaction

Perform the arithmetic inside CockroachDB so the operation is atomic. This avoids read–modify-write races entirely.

const { Client } = require('pg');
const client = new Client({ connectionString: process.env.DATABASE_URL });

async function depositAtomic(accountId, amount) {
  const client = new Client({ connectionString: process.env.DATABASE_URL });
  await client.connect();
  try {
    await client.query('BEGIN TRANSACTION');
    const res = await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2 RETURNING balance',
      [amount, accountId]
    );
    await client.query('COMMIT');
    return res.rows[0].balance;
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.end();
  }
}

// Express route
app.post('/api/account/:id/deposit', async (req, res) => {
  const { amount } = req.body;
  if (typeof amount !== 'number' || amount <= 0) {
    return res.status(400).json({ error: 'Invalid amount' });
  }
  try {
    const updated = await depositAtomic(req.params.id, amount);
    res.json({ balance: updated });
  } catch (err) {
    if (err.code === '40001') {
      // serialization failure, safe to retry
      return res.status(409).json({ error: 'Conflict, retry recommended' });
    }
    res.status(500).json({ error: 'Internal error' });
  }
});

2) Conditional write with expected state

When an update must depend on a previously observed value, include the expected condition in the WHERE clause. This ensures the write fails if the state changed concurrently, allowing the caller to detect and retry.

async function transferConditional(fromId, toId, amount, expectedFromBalance) {
  const client = new Client({ connectionString: process.env.DATABASE_URL });
  await client.connect();
  try {
    await client.query('BEGIN TRANSACTION');
    const fromRes = await client.query(
      'UPDATE transfers SET balance = balance - $1 WHERE id = $2 AND balance = $3 RETURNING balance',
      [amount, fromId, expectedFromBalance]
    );
    if (fromRes.rowCount === 0) {
      await client.query('ROLLBACK');
      return { conflict: true };
    }
    await client.query(
      'UPDATE transfers SET balance = balance + $1 WHERE id = $2',
      [amount, toId]
    );
    await client.query('COMMIT');
    return { conflict: false };
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.end();
  }
}

If the balance changed between read and write, rowCount will be 0, and the Express handler can decide to retry with a fresh read or report a conflict. This pattern aligns with CockroachDB’s serializable semantics and avoids lost updates.

3) Retry on serialization errors

Under high concurrency, serializable transactions may abort with a 40001 error. Implement exponential backoff and idempotency keys in Express to safely retry without side effects.

async function withRetry(fn, retries = 5) {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      if (err.code === '40001' && attempt < retries) {
        attempt++;
        const delay = Math.pow(2, attempt) * 100 + Math.random() * 100;
        await new Promise(r => setTimeout(r, delay));
        continue;
      }
      throw err;
    }
  }
}

app.post('/api/account/:id/deposit', async (req, res) => {
  const { amount } = req.body;
  if (typeof amount !== 'number' || amount <= 0) {
    return res.status(400).json({ error: 'Invalid amount' });
  }
  try {
    const result = await withRetry(() => depositAtomic(req.params.id, amount));
    res.json({ balance: result });
  } catch (err) {
    res.status(500).json({ error: 'Internal error' });
  }
});

These patterns ensure that concurrent operations on the same account are serialized correctly by CockroachDB, and the Express layer handles conflicts gracefully. For broader coverage, combine this with middleBrick scans to detect endpoints that perform unsafe consumption or lack proper authorization, which can exacerbate contention paths and make race conditions more observable.

Frequently Asked Questions

Can CockroachDB’s serializable isolation prevent all race conditions in Express APIs?
CockroachDB’s serializable isolation prevents database-level anomalies such as dirty reads and write skew, but it does not automatically make application logic race-free. Races can still occur if the Express code performs read–modify–write cycles without atomic updates or conditional writes. You must structure transactions to perform all necessary work within a single serializable transaction or use conditional WHERE clauses; the database cannot fix incorrect application-level sequencing.
How does middleBrick help identify endpoints prone to race conditions?
middleBrick does not test for race conditions directly; it runs 12 parallel security checks including input validation, unsafe consumption, and authorization. By surfacing endpoints with unsafe consumption patterns, missing authorization, or complex read–write workflows, these findings can help you locate code paths where concurrent access may lead to race conditions. Follow up with database-level fixes and retries as shown in the remediation examples.