HIGH time of check time of useexpresscockroachdb

Time Of Check Time Of Use in Express with Cockroachdb

Time Of Check Time Of Use in Express with Cockroachdb — how this specific combination creates or exposes the vulnerability

Time Of Check Time Of Use (TOCTOU) occurs in an Express route when data is read (checked) and later used in a way that can be changed between the read and the use. With CockroachDB, this commonly appears when an authorization check runs in a separate transaction or query from the data mutation, and the underlying state changes between them. Even with serializable isolation, the application’s own read-then-write logic can create a window where the checked condition no longer holds at write time.

Consider an Express endpoint that first verifies a user’s ownership of a resource and then performs an update. If the ownership check and the update are separate statements, and the resource row can be modified by another concurrent request (or by the same user via another path), the authorization decision made during the check may be invalid at use time. For example, an attacker could alter the resource’s owner between the SELECT and the UPDATE, leading to privilege escalation or unauthorized data modification.

A concrete pattern that is vulnerable looks like:

// Vulnerable: check then use without locking or single-statement write
app.put('/resources/:id', async (req, res) => {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const { rows } = await client.query(
      'SELECT owner_id FROM resources WHERE id = $1',
      [req.params.id]
    );
    if (!rows.length || rows[0].owner_id !== req.user.id) {
      await client.query('ROLLBACK');
      return res.status(403).send('Forbidden');
    }
    // TOCTOU window: resource could be transferred between SELECT and UPDATE
    await client.query(
      'UPDATE resources SET data = $1 WHERE id = $2',
      [req.body.data, req.params.id]
    );
    await client.query('COMMIT');
    res.send('Updated');
  } catch (e) {
    await client.query('ROLLBACK');
    res.status(500).send('Server error');
  } finally {
    client.release();
  }
});

In this pattern, the SELECT checks ownership, but between that check and the UPDATE, another transaction could change the owner_id (e.g., via an account merge or an admin action). The check passes, but the update proceeds on a resource the user no longer owns. CockroachDB’s serializable isolation prevents some classes of anomalies, but it does not prevent the application logic from acting on stale reads when writes depend on earlier reads across separate statements.

Another common scenario is when authorization is inferred from list results that are not re-validated at the row level during mutation. For example, a GET /projects/:id may list accessible projects, and the client sends :id back on PATCH. If the server uses the earlier list to authorize (instead of querying again or locking), the effective check becomes stale.

To reduce TOCTOU risk with CockroachDB in Express, prefer approaches that perform authorization as part of the write operation, use explicit locking when necessary, and keep the check and use within a single, well-structured statement or consistent snapshot. Relying solely on application-level checks without coupling them tightly to the write increases the risk window.

Cockroachdb-Specific Remediation in Express — concrete code fixes

Remediation focuses on ensuring that the authorization condition is evaluated at the moment of write, ideally in a single statement or within a serializable transaction with minimal read–write separation. Below are concrete, CockroachDB-aware patterns for Express.

1) Combine check and update in one statement

For ownership or policy checks, encode the condition directly in the WHERE clause and inspect the row count after the update. This eliminates the TOCTOU window because the authorization decision is made inside the database at write time.

// Secure: authorization enforced in UPDATE WHERE
app.put('/resources/:id', async (req, res) => {
  const client = await pool.connect();
  try {
    const { rowCount } = await client.query(
      'UPDATE resources SET data = $1 WHERE id = $2 AND owner_id = $3',
      [req.body.data, req.params.id, req.user.id]
    );
    if (rowCount === 0) {
      return res.status(403).send('Forbidden or resource not found');
    }
    res.send('Updated');
  } catch (e) {
    res.status(500).send('Server error');
  } finally {
    client.release();
  }
});

This approach leverages CockroachDB’s ability to filter rows by primary key and additional columns atomically. If the row does not match the owner_id, zero rows are updated, and the request is denied without a prior SELECT.

2) Use SELECT FOR UPDATE when a prior read is required

If business logic requires reading the row first, immediately lock it with SELECT FOR UPDATE within the same serializable transaction. This prevents concurrent modifications between the read and the subsequent write.

// Secure: lock the row after reading
app.put('/resources/:id', async (req, res) => {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const { rows } = await client.query(
      'SELECT data, owner_id FROM resources WHERE id = $1 FOR UPDATE',
      [req.params.id]
    );
    if (!rows.length || rows[0].owner_id !== req.user.id) {
      await client.query('ROLLBACK');
      return res.status(403).send('Forbidden');
    }
    await client.query(
      'UPDATE resources SET data = $1 WHERE id = $2',
      [req.body.data, req.params.id]
    );
    await client.query('COMMIT');
    res.send('Updated');
  } catch (e) {
    await client.query('ROLLBACK');
    res.status(500).send('Server error');
  } finally {
    client.release();
  }
});

SELECT FOR UPDATE places a lock on the row for the transaction, closing the TOCTOU gap by ensuring no other transaction can modify the row until the current transaction commits or aborts. This is appropriate when additional read-side logic is required before the update.

3) Use conditional writes with optimistic concurrency control

Include a version or updated_at column and check it in the WHERE clause to detect conflicts. If the version changed between read and write, the update affects zero rows, and the request can be retried or rejected.

// Secure: optimistic concurrency control
app.put('/resources/:id', async (req, res) => {
  const client = await pool.connect();
  try {
    const { rowCount } = await client.query(
      'UPDATE resources SET data = $1, version = version + 1 WHERE id = $2 AND owner_id = $3 AND version = $4',
      [req.body.data, req.params.id, req.user.id, req.body.version]
    );
    if (rowCount === 0) {
      return res.status(409).send('Conflict or authorization failed');
    }
    res.send('Updated');
  } catch (e) {
    res.status(500).send('Server error');
  } finally {
    client.release();
  }
});

These patterns ensure that authorization is enforced at the point of use, minimizing the TOCTOU window. For most cases, combining check and update in a single statement is the simplest and safest approach.

Frequently Asked Questions

Why does a SELECT followed by an UPDATE in separate statements create a TOCTOU risk even on CockroachDB?
Because the state can change between the SELECT and the UPDATE. CockroachDB’s serializable isolation prevents certain anomalies but does not automatically re-validate authorization assumptions; the application must couple the check and use or lock the row to avoid a window.
Is SELECT FOR UPDATE always necessary to prevent TOCTOU in Express with CockroachDB?
Not always. When authorization can be expressed in the UPDATE WHERE clause, a single statement is sufficient and avoids extra locking. Use SELECT FOR UPDATE only when you need to read and apply additional logic before writing.