Race Condition in Express
How Race Condition Manifests in Express
Race conditions in Express applications occur when multiple requests access and modify shared state concurrently, leading to unpredictable results. These vulnerabilities are particularly dangerous in Express because of its single-threaded, event-driven architecture where multiple requests can interleave operations on shared resources.
A classic Express race condition appears in user balance updates. Consider this flawed implementation:
app.post('/transfer', (req, res) => {
const { from, to, amount } = req.body;
// Read current balances
const fromBalance = getBalanceFromDB(from);
const toBalance = getBalanceFromDB(to);
// Check if sufficient funds
if (fromBalance >= amount) {
// Update balances
updateBalanceFromDB(from, fromBalance - amount);
updateBalanceFromDB(to, toBalance + amount);
res.json({ success: true });
} else {
res.status(400).json({ error: 'Insufficient funds' });
}
});Two concurrent requests can interleave between the balance read and write operations:
Request 1: Read fromBalance = 100
Request 2: Read fromBalance = 100
Request 1: Check 100 >= 50 (true)
Request 2: Check 100 >= 50 (true)
Request 1: Write fromBalance = 50
Request 1: Write toBalance = 150
Request 2: Write fromBalance = 50 // Overwrites Request 1's write!
Request 2: Write toBalance = 150
The result: $100 transferred instead of $50, with the second operation completely overwriting the first.
Another Express-specific race condition occurs with in-memory counters used for rate limiting:
let requestCount = {};
app.use((req, res, next) => {
const ip = req.ip;
if (!requestCount[ip]) requestCount[ip] = 0;
if (requestCount[ip] >= 100) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
requestCount[ip]++;
next();
});Multiple requests from the same IP can read the same count value before any increment completes, allowing more requests than intended.
Express middleware order creates additional race condition surfaces. If authentication middleware and authorization middleware access shared session data without proper synchronization, concurrent requests can cause authorization bypass:
app.use(sessionMiddleware);
app.use(authMiddleware);
app.use(authzMiddleware);If authzMiddleware reads session data while authMiddleware is writing it (due to concurrent requests), the authorization check might see stale or inconsistent data.
Express-Specific Detection
Detecting race conditions in Express requires both static analysis and runtime testing. middleBrick's scanning engine specifically targets Express patterns that commonly harbor race conditions.
The scanner examines your Express application for shared state access patterns:
// middleBrick detects this vulnerable pattern
const userCache = {};
app.get('/user/:id', (req, res) => {
const id = req.params.id;
if (!userCache[id]) {
// Race condition: multiple requests can enter here simultaneously
userCache[id] = db.getUser(id);
}
res.json(userCache[id]);
});middleBrick flags in-memory caching without proper locking mechanisms, identifying the exact line numbers where race conditions can occur.
For rate limiting race conditions, the scanner tests concurrent requests to identify if the limit can be bypassed:
const axios = require('axios');
async function testRateLimitRace(url, ip) {
const promises = [];
for (let i = 0; i < 110; i++) {
promises.push(
axios.get(url, { headers: { 'X-Forwarded-For': ip } })
);
}
const responses = await Promise.allSettled(promises);
const allowed = responses.filter(r => r.status === 'fulfilled').length;
return allowed;
}The scanner runs this test with 110 concurrent requests against endpoints claiming to enforce 100 request limits, measuring how many actually succeed.
middleBrick also detects Express-specific async/await anti-patterns that create race conditions:
app.post('/update-profile', async (req, res) => {
const { userId } = req.session;
const { email } = req.body;
// Vulnerable: No transaction, no locking
const user = await User.findById(userId);
user.email = email;
await user.save(); // Another request could modify this user between find and save
res.json({ success: true });
});The scanner identifies operations where database reads are followed by writes without transactions or optimistic locking, which is the most common race condition pattern in Express applications.
For comprehensive testing, middleBrick's CLI includes a race condition detection mode:
npx middlebrick scan https://yourapi.com --race-detection --concurrent-requests 100
This runs the target API through stress testing specifically designed to expose race conditions in authentication, authorization, and data modification endpoints.
Express-Specific Remediation
Fixing race conditions in Express requires understanding both JavaScript's event loop and database transaction mechanisms. Here are Express-specific remediation patterns:
For the transfer race condition, use database transactions with row-level locking:
const { Sequelize, Op } = require('sequelize');
app.post('/transfer', async (req, res) => {
const { from, to, amount } = req.body;
try {
const transaction = await sequelize.transaction();
// SELECT ... FOR UPDATE locks the rows
const [fromAccount, toAccount] = await Promise.all([
Account.findByPk(from, { lock: Transaction.LOCK.UPDATE, transaction }),
Account.findByPk(to, { lock: Transaction.LOCK.UPDATE, transaction })
]);
if (fromAccount.balance < amount) {
await transaction.rollback();
return res.status(400).json({ error: 'Insufficient funds' });
}
// Update within transaction
fromAccount.balance -= amount;
toAccount.balance += amount;
await Promise.all([
fromAccount.save({ transaction }),
toAccount.save({ transaction })
]);
await transaction.commit();
res.json({ success: true });
} catch (error) {
await transaction.rollback();
res.status(500).json({ error: 'Transfer failed' });
}
});The lock: Transaction.LOCK.UPDATE ensures that concurrent requests wait for the transaction to complete before accessing the same rows.
For in-memory rate limiting, use atomic operations with Redis:
const Redis = require('ioredis');
const redis = new Redis();
app.use(async (req, res, next) => {
const ip = req.ip;
const key = `rate_limit:${ip}`;
// Redis INCR is atomic
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, 60); // 60-second window
}
if (current > 100) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
next();
});Redis operations are atomic and thread-safe, eliminating race conditions that occur with JavaScript object mutations.
For session-based race conditions, use proper session store configuration:
const session = require('express-session');
const RedisStore = require('connect-redis');
app.use(session({
store: new RedisStore({
client: redis,
disableTouch: true
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
rolling: true,
cookie: { secure: true, maxAge: 24 * 60 * 60 * 1000 }
}));Redis-based sessions prevent the race conditions that occur with default MemoryStore when multiple processes handle requests.
For optimistic locking in document databases like MongoDB:
app.put('/update-profile', async (req, res) => {
const { userId } = req.session;
const { email } = req.body;
try {
const user = await User.findById(userId);
// Create update document
const update = { $set: { email } };
// Use findOneAndUpdate with version check
const result = await User.findOneAndUpdate(
{ _id: userId, __v: user.__v }, // Match current version
update,
{ new: true, runValidators: true }
);
if (!result) {
return res.status(409).json({ error: 'Conflict - data modified by another request' });
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Update failed' });
}
});This uses MongoDB's optimistic locking pattern where updates only succeed if the document version matches, preventing lost updates from concurrent modifications.
Frequently Asked Questions
How can I test for race conditions in my Express API without middleBrick?
Create a test script that sends concurrent requests to the same endpoint using Promise.all with 50-100 parallel requests. For financial operations, verify that the final state matches what should happen with serialized execution. For rate limiting, send more requests than the limit allows and check if any bypass the restriction. Use tools like autocannon or k6 to generate sustained concurrent load while monitoring for inconsistent responses or state corruption.
Do race conditions only affect multi-user applications?
No, race conditions can affect single-user applications too. A single user making multiple requests in parallel (like refreshing a page or using multiple browser tabs) can trigger race conditions. Any application that handles concurrent requests from the same user or process is vulnerable. The key factor is whether operations share mutable state, not the number of users involved.