Race Condition in Express with Hmac Signatures
Race Condition in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A race condition occurs when the outcome depends on the unpredictable timing of events. In Express applications that use HMAC signatures for request integrity, a race condition can arise when signature validation and the side-effect of processing the same logical operation are not performed atomically. Consider an endpoint that accepts a JSON payload containing an account balance and a signature over the payload. The server first verifies the HMAC, then reads the current balance from a data store, computes a new balance, and writes it back. If two identical or related requests arrive concurrently, the following sequence can occur:
- Request A validates its HMAC successfully.
- Request B validates its HMAC successfully.
- Request A reads the balance (e.g., 100).
- Request B reads the same balance (100).
- Request A computes a new balance (e.g., 150) and writes it.
- Request B computes a new balance based on the stale read (100) and writes it (150), overwriting A’s update.
Although each request was authenticated with a valid HMAC, the lack of atomicity between verification and state mutation leads to lost updates and integrity violations. This pattern maps to common OWASP API Top 10 categories such as Broken Object Level Authorization (BOLA) and Mass Assignment when the signature covers mutable state that is not properly isolated per request. An attacker does not need to break the HMAC; they exploit timing and concurrency to cause inconsistent state. In distributed or horizontally scaled environments, the race condition can be more pronounced if the data store’s read and write operations are not properly serialized or if caching layers introduce additional asynchronicity.
Real-world examples include financial transfer endpoints and inventory deduction APIs. For instance, an endpoint that decrements a stock count after verifying a signature can oversell if two orders are processed concurrently. Similarly, an administrative action that changes a user’s role can be triggered multiple times in an unintended order, leading to privilege escalation or unauthorized state changes. Because HMACs ensure integrity and authenticity of a single request, they do not prevent logical flaws when the application’s workflow involves non-atomic state transitions. Detecting this class of issue requires observing behavior under concurrency, not just the validity of individual signatures.
Hmac Signatures-Specific Remediation in Express — concrete code fixes
Remediation focuses on making the verification and state update atomic and idempotent. In Express, this typically involves using database transactions, optimistic locking, or unique request identifiers to ensure that once a signature is validated, the associated state change cannot be interleaved with conflicting updates. Below are two concrete patterns with valid Express code examples.
1. Database transaction with atomic update
Use your data store’s transaction mechanism to read, compute, and write within a single atomic operation. This ensures that concurrent requests are serialized by the database.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.HMAC_SECRET;
function verifyHmac(req, res, next) {
const received = req.headers['x-hmac-signature'];
const payload = JSON.stringify(req.body);
const expected = crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
app.post('/account/deposit', verifyHmac, async (req, res) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
const { amount } = req.body;
// Atomic update: read and write in one transaction
const result = await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2 RETURNING balance',
[amount, req.userAccountId]
);
await client.query('COMMIT');
res.json({ newBalance: result.rows[0].balance });
} catch (err) {
await client.query('ROLLBACK');
res.status(500).json({ error: 'Internal error' });
} finally {
client.release();
}
});
2. Optimistic locking with versioning
Include a version or timestamp in the signed payload and verify it on update. If the version in the database does not match, reject the request to prevent overwriting concurrent changes.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.HMAC_SECRET;
function signPayload(payload) {
return crypto.createHmac('sha256', SHARED_SECRET).update(JSON.stringify(payload)).digest('hex');
}
app.post('/resource/update', async (req, res) => {
const { data, version, signature } = req.body;
const payload = { data, version };
const expected = signPayload(payload);
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await client.query(
'UPDATE resources SET data = $1, version = version + 1 WHERE id = $2 AND version = $3 RETURNING version',
[data, req.resourceId, version]
);
if (result.rowCount === 0) {
await client.query('ROLLBACK');
return res.status(409).json({ error: 'Conflict: resource was modified concurrently' });
}
await client.query('COMMIT');
res.json({ version: version + 1 });
} catch (err) {
await client.query('ROLLBACK');
res.status(500).json({ error: 'Internal error' });
} finally {
client.release();
}
});
Both approaches ensure that the integrity check (HMAC) and the state change are treated as a single logical operation. The first pattern relies on the database to serialize writes; the second uses versioning to detect and reject interleaved updates. These methods prevent race conditions while preserving the authenticity guarantees provided by HMAC signatures.