Timing Attack in Express with Cockroachdb
Timing Attack in Express with Cockroachdb — how this specific combination creates or exposes the vulnerability
A timing attack in an Express service that uses CockroachDB can occur when response times leak information about authentication or data lookup logic. For example, an endpoint that compares a user-supplied token or password with a value stored in CockroachDB may take variable time depending on how early the comparison fails. If the implementation performs a character-by-character comparison in JavaScript instead of a constant-time comparison, an attacker can measure round-trip times to infer how many characters match, gradually reconstructing the expected value.
With CockroachDB, the timing characteristics can be influenced by how queries are structured and how results are processed. An endpoint that performs a conditional lookup like SELECT * FROM users WHERE email = $1 and then conditionally verifies a password in Node.js can introduce a measurable difference between a query that returns a row and one that returns none. Even though CockroachDB may return results quickly, the server-side processing in Express may branch differently depending on presence versus absence of a row, causing observable timing variation.
Consider an authentication route that retrieves a user by email and then compares a candidate password using a non-constant-time check:
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const { rows } = await client.query('SELECT id, password_hash FROM users WHERE email = $1', [email]);
if (rows.length === 0) {
// Simulated delay to obscure timing, still not constant-time
await sleep(50);
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = rows[0];
// Non-constant-time comparison: early exit on mismatch
if (!slowEquals(user.password_hash, password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
res.json({ ok: true });
});
An attacker can send many login requests with slightly altered emails or passwords and observe response times. Shorter times may indicate a missing row; longer times may indicate a partial hash match before the comparison fails. Even if the database query itself is constant-time, the surrounding logic in Express can reintroduce variability. This is especially relevant when using CockroachDB in a distributed setup where network latency is consistent enough that server-side processing differences become more noticeable.
Another scenario involves rate-limiting or enumeration checks where timing differences reveal whether an account exists. For instance, delaying a response only when an account is found can be detectable. Because CockroachDB is optimized for consistent performance, the variability introduced by application-side logic stands out more, making timing attacks feasible against an Express + Cockroachdb stack.
Cockroachdb-Specific Remediation in Express — concrete code fixes
To mitigate timing attacks, ensure all operations that depend on secret values run in constant time and avoid branching on sensitive data. Use constant-time comparison functions and design queries so that execution paths and response durations do not reveal information about the presence or correctness of secrets.
Use a constant-time comparison utility for passwords and tokens. In Node.js, crypto.timingSafeEqual is appropriate for Buffers of equal length. For Express routes, always return a generic error and ensure both branches take similar time:
const crypto = require('crypto');
function timingSafeCompare(a, b) {
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
Refactor the login route to always perform a lookup and a constant-time comparison, regardless of whether the email exists. Use a dummy hash to preserve consistent timing when the row is absent:
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const { rows } = await client.query('SELECT id, password_hash FROM users WHERE email = $1', [email]);
const dummyHash = '$2a$10$dummyvalueforsecureconstanttimecomparisondummyvalue'; // fixed-length placeholder
const storedHash = rows.length > 0 ? rows[0].password_hash : dummyHash;
// Ensure password is converted to a fixed-length Buffer (e.g., hashed input)
const candidateBuf = Buffer.from(password); // In practice, use the hashed candidate matching storedHash encoding
const storedBuf = Buffer.from(storedHash);
let equal = false;
if (candidateBuf.length === storedBuf.length) {
equal = crypto.timingSafeEqual(candidateBuf, storedBuf);
}
// Always perform a small, fixed-duration operation to obscure timing further if needed
await new Promise(setImmediate);
if (!equal) {
return res.status(401).json({ error: 'Invalid credentials' });
}
res.json({ ok: true });
});
When using parameterized queries with CockroachDB, keep the query shape consistent. Avoid conditional logic that changes the number of rows returned based on sensitive data. Instead, fetch a single row with a known email and compare in constant time. If you need to avoid leaking existence via timing, consider always running the lookup and then deciding after the constant-time comparison.
For broader protection, pair these code changes with operational practices such as ensuring CockroachDB client configurations use consistent timeouts and that network latency variability is minimized. Regularly test endpoints with controlled timing measurements to confirm that response durations do not correlate with secret-dependent branching.