Race Condition in Express with Api Keys
Race Condition in Express with Api Keys — how this specific combination creates or exposes the vulnerability
A race condition in an Express API that uses API keys often arises when key validation and related state changes are not performed as a single, atomic operation. For example, consider an endpoint that checks whether an API key is valid and then decrements a remaining quota or updates a last-used timestamp. If two concurrent requests arrive with the same key, both may pass the initial validation before either updates the shared quota state. This can result in the key being used more times than allowed, bypassing intended rate limits or quota controls.
In Express, this typically manifests when developers implement key checks in middleware but defer quota or state updates to later in the request chain without locking or transactional guarantees. An attacker can send many rapid, parallel requests that all see the key as valid and available, effectively racing through a rate limit or quota that should have been exhausted earlier. Because API keys are often bearer tokens with broad access, the outcome may be unauthorized data access or elevated usage at the expense of the key owner.
Another scenario involves key rotation or revocation. If an Express route first validates the key and then checks a revocation list or database entry that may have changed after validation, a race condition exists. An attacker whose key is revoked could still succeed if the revocation occurs after the validation step but before the business logic completes. This is common in systems where key state is cached for performance and eventual consistency is relied upon without additional safeguards.
The vulnerability is specific to the combination of Express’s asynchronous request handling and the way API key state is managed. Because Express does not provide built-in concurrency controls, developers must ensure that validation and state mutation are designed to be safe under concurrent access. Without such design, the API key mechanism becomes susceptible to overuse, quota bypass, or inconsistent enforcement, which middleBrick’s BFLA/Privilege Escalation and Rate Limiting checks are designed to detect.
Api Keys-Specific Remediation in Express — concrete code fixes
To mitigate race conditions with API keys in Express, make validation and state updates atomic or effectively serializing for the same key. Below are concrete, realistic examples that you can adapt.
Example 1: In-memory atomic check-and-update with a Map
Use a JavaScript Map to store per-key counters and update them within a synchronous or Promise-based block to reduce concurrency issues. Note that this works for single-process setups; for distributed systems, use a shared store with atomic operations.
const express = require('express');
const app = express();
// Simple in-memory store for remaining quota per key
const keyQuota = new Map([
['abc123', 100],
['def456', 50]
]);
function apiKeyQuotaMiddleware(req, res, next) {
const key = req.headers['x-api-key'];
if (!key) {
return res.status(401).json({ error: 'API key missing' });
}
const remaining = keyQuota.get(key);
if (remaining == null || remaining <= 0) {
return res.status(403).json({ error: 'Quota exhausted' });
}
// Simulate atomic update for this request by decrementing immediately
keyQuota.set(key, remaining - 1);
next();
}
app.use(apiKeyQuotaMiddleware);
app.get('/data', (req, res) => {
res.json({ message: 'Success', key: req.headers['x-api-key'] });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Example 2: Using an external store with atomic decrement (e.g., Redis)
For distributed or clustered environments, use a data store that supports atomic decrement operations. This ensures that concurrent requests for the same key are handled safely.
const express = require('express');
const redis = require('redis');
const app = express();
const client = redis.createClient();
client.connect().catch(console.error);
async function checkAndDecrement(key) {
// Redis DECR is atomic; we set initial quota via SET key 100 EX 86400 as needed
const remaining = await client.decr(key);
return remaining;
}
async function apiKeyQuotaMiddleware(req, res, next) {
const key = req.headers['x-api-key'];
if (!key) {
return res.status(401).json({ error: 'API key missing' });
}
try {
const remaining = await checkAndDecrement(`quota:${key}`);
if (remaining < 0) {
// Rollback one increment if you prefer to keep non-negative; here we treat < 0 as exhausted
await client.incr(`quota:${key}`); // revert the decrement
return res.status(403).json({ error: 'Quota exhausted' });
}
next();
} catch (err) {
next(err);
}
}
app.use(apiKeyQuotaMiddleware);
app.get('/data', (req, res) => {
res.json({ message: 'Success', key: req.headers['x-api-key'] });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Example 3: Serializing access per key with an async queue for critical sections
If you must perform multi-step validation and updates, serialize operations per key to avoid races. This example uses a simple promise queue per key to ensure ordered execution.
const express = require('express');
const app = express();
const queues = new Map();
function getQueueFor(key) {
if (!queues.has(key)) {
queues.set(key, Promise.resolve());
}
return queues.get(key);
}
const keyState = new Map([
['abc123', { remaining: 10 }]
]);
app.use((req, res, next) => {
const key = req.headers['x-api-key'];
if (!key) {
return res.status(401).json({ error: 'API key missing' });
}
const queue = getQueueFor(key);
const newQueue = queue.then(async () => {
const state = keyState.get(key);
if (!state || state.remaining <= 0) {
res.status(403).json({ error: 'Quota exhausted' });
return;
}
state.remaining -= 1;
keyState.set(key, state);
// proceed to route handler via a flag
req._quotaOk = true;
});
queues.set(key, newQueue);
next();
});
app.get('/data', (req, res) => {
if (req._quotaOk) {
res.json({ message: 'Success', key: req.headers['x-api-key'] });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
These examples demonstrate how to align validation and state changes to reduce race windows. For production, prefer an external store with atomic operations and instrument your system so that middleBrick’s BFLA/Privilege Escalation and Rate Limiting checks can surface any remaining inconsistencies.