Race Condition in Feathersjs with Cockroachdb
Race Condition in Feathersjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
A race condition in a Feathersjs service backed by Cockroachdb typically occurs when multiple concurrent requests read and write the same entity without effective synchronization, leading to lost updates or inconsistent state. Feathersjs services often rely on generic CRUD hooks and external adapters; if optimistic concurrency controls (like version fields) are absent or not enforced, concurrent operations can interleave in problematic ways. Cockroachdb, while strongly consistent for single-statement reads and writes, does not prevent application-level races when transactions span multiple statements or when the application layer does not use explicit isolation or conditional writes.
Consider a Feathersjs service for updating a ticket’s remaining seats. Two clients may concurrently read the same seat count (e.g., 1), both decide they can proceed, and each writes back a decremented value. If the read–modify–write is not atomic, the final count can become incorrect (e.g., 0 instead of -1). This pattern maps to the OWASP API Top 10:2023 A5 (Broken Function Level Authorization) when overprivileged endpoints allow such logic to run unchecked, and it can be surfaced by middleBrick’s BOLA/IDOR and Property Authorization checks, which highlight missing per-instance ownership and missing state validation.
With Cockroachdb, explicit serializable transactions can mitigate this, but developers must opt in. If a Feathersjs adapter opens short transactions that only contain a single statement, Cockroachdb’s strong consistency often masks races; races become likely when a transaction includes a SELECT followed by a conditional UPDATE without locking. For example, a find followed by an update in separate adapter calls gives an unbounded window for interleaving. Cockroachdb will commit both transactions under snapshot isolation, but the second commit will fail at commit time if a write-write conflict is detected (serializable isolation). If the Feathersjs service does not retry on serialization failures, users see errors or silent data loss. middleBrick’s Input Validation and Rate Limiting checks can reduce noisy, abusive concurrency, but they do not replace correct transaction design.
Another scenario involves inventory decrement with insufficient checks. A service may read an inventory row, validate business rules, and then write back a new quantity. Between read and write, another request can modify the same row. Cockroachdb’s distributed SQL nature does not inherently serialize these multi-step workflows; the application must use SELECT FOR UPDATE or conditional UPDATE to lock the relevant rows or ensure uniqueness constraints are checked at write time. Without this, the service is vulnerable to timing-based exploits, and findings from middleBrick’s BFLA/Privilege Escalation and Property Authorization checks will point to missing safeguards.
Instrumentation and logging can reveal symptoms, but root causes are in transaction design and hook logic. A Feathersjs hook that does not return a promise chain that enforces ordering, or that uses async hooks without proper transaction scoping, can exacerbate races. Cockroachdb’s serializable isolation will raise retry errors; if these are not surfaced or handled, clients may receive 500-level errors. Use middleware to standardize error handling and map serialization failures to idempotent retries. middleBrick’s scan can identify unguarded endpoints and missing idempotency guidance in findings, helping you prioritize fixes.
Cockroachdb-Specific Remediation in Feathersjs — concrete code fixes
To eliminate race conditions, make state transitions atomic in the database and ensure Feathersjs hooks use explicit transactions with serializable isolation. The following examples show a robust pattern using the pg client compatible with Cockroachdb and the Feathersjs hooks API.
1. Atomic decrement with conditional update
Instead of read–modify–write, push the decision into a single UPDATE with a WHERE condition. This avoids races and reduces round-trips.
// In a Feathersjs service mixin or hook
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: {
rejectUnauthorized: false // adjust for your Cockroachdb setup
}
});
async function decrementSeats(ticketId, seatsToBuy) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Conditional update: only succeed if enough seats remain
const res = await client.query(
'UPDATE tickets SET remaining_seats = remaining_seats - $1 WHERE id = $2 AND remaining_seats >= $1 RETURNING remaining_seats',
[seatsToBuy, ticketId]
);
if (res.rowCount === 0) {
throw new Error('Not enough seats or ticket not found');
}
await client.query('COMMIT');
return res.rows[0].remaining_seats;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
// Feathersjs hook example
const decrementHook = async context => {
const { id, data } = context.result; // after CREATE/UPDATE
if (context.method === 'patch' && typeof data.seats === 'number') {
const newRemaining = await decrementSeats(id, data.seats);
// Optionally patch the result for the caller
context.result.remaining_seats = newRemaining;
}
return context;
};
2. Serializable transaction for multi-step workflows
When you must execute multiple statements, use an explicit serializable transaction and retry on serialization failures.
async function reserveInventoryWithTransaction(items) {
const client = await pool.connect();
let retries = 0;
const maxRetries = 5;
while (retries <= maxRetries) {
try {
await client.query('BEGIN');
for (const item of items) {
const res = await client.query(
'UPDATE inventory SET reserved = reserved + $1 WHERE product_id = $2 AND available >= $1',
[item.qty, item.productId]
);
if (res.rowCount === 0) {
throw new Error(`Insufficient inventory for product ${item.productId}`);
}
}
await client.query('COMMIT');
return { success: true };
} catch (err) {
await client.query('ROLLBACK');
if (err.code === '40001' && retries < maxRetries) { // serialization_failure
retries += 1;
continue;
}
throw err;
} finally {
client.release();
}
}
throw new Error('Max retries exceeded for serializable transaction');
}
3. Use optimistic concurrency with a version column
Add a version (or updated_at) column and include it in WHERE clauses so updates fail if the version mismatches. This pattern works well with Feathersjs hooks to provide clear errors rather than silent overwrites.
// Migration (Cockroachdb SQL)
-- ALTER TABLE tickets ADD COLUMN version INT NOT NULL DEFAULT 0;
// Update with version check
async function updateTicketWithVersion(ticketId, patch, expectedVersion) {
const client = await pool.connect();
try {
const res = await client.query(
'UPDATE tickets SET seat_count = $1, version = version + 1 WHERE id = $2 AND version = $3 RETURNING version',
[patch.seatCount, ticketId, expectedVersion]
);
if (res.rowCount === 0) {
throw new Error('Concurrency conflict: ticket was modified by another request');
}
return res.rows[0].version;
} finally {
client.release();
}
}
4. Hook integration and error mapping
In Feathersjs, implement these patterns inside hooks so services remain unaware of the underlying transaction mechanics. Map Cockroachdb serialization errors to appropriate HTTP statuses and provide idempotency guidance in responses. middleBrick’s findings around missing state checks and weak authorization will be reduced once these atomic patterns are in place.
Finally, ensure your service’s validation and authorization logic runs within the transaction boundary where necessary, and prefer database constraints (unique, foreign key, check) to enforce invariants. This aligns with best practices flagged by middleBrick’s Compliance checks for frameworks such as OWASP API Top 10 and SOC2 control considerations.