Timing Attack in Express with Hmac Signatures
Timing Attack in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A timing attack in Express when using Hmac signatures arises when signature comparison does not execute in constant time. Express itself does not provide a built-in constant-time compare for Hmac digests, so developers commonly use standard string equality (===) or Buffer.equals to verify a computed Hmac against a client-supplied signature. If the comparison short-circuits on the first mismatching byte, an attacker can send many requests while measuring response times to progressively learn the correct signature byte-by-byte.
The attack flow is measurable and practical because network jitter is typically low in controlled environments or from nearby clients, and Hmac outputs are often represented as hex strings (e.g., 64 characters for SHA-256). By observing whether a request with a slightly altered signature takes marginally longer — indicating a longer matching prefix — an attacker can infer the expected signature. Although the secret key remains unknown, the attacker does not need to recover the key; they only need to forge a valid Hmac for known or guessed parameters (e.g., a tampered resource ID or an escalated privilege claim).
In Express, common scenarios that expose this include:
- Using crypto.createHmac to compute a digest and then comparing it with == or === against user input.
- Comparing Hmac values after parsing query or body parameters without considering timing side channels.
- Failing to enforce a fixed processing path for both valid and invalid signatures, allowing measurable differences in CPU usage or branch prediction behavior.
Real-world analogies exist in API tokens or session identifiers signed with Hmac. For example, an endpoint like /transfer?amount=100&to=attacker&signature=hex where the server recomputes the Hmac over the concatenated parameters using a server-side secret is vulnerable if the comparison leaks timing. Even with TLS, timing differences can be observed because the comparison happens after decryption, inside the Node.js event loop.
To assess whether an endpoint is vulnerable, a scanner can send two requests with nearly identical signatures differing in the last byte and measure response time differences. Repeated measurements can amplify small timing differences into detectable patterns, especially when combined with statistical methods. This is one of the 12 security checks run in parallel by middleBrick during unauthenticated black-box scanning, including checks for Unsafe Consumption and Input Validation that may interact with Hmac handling.
Hmac Signatures-Specific Remediation in Express — concrete code fixes
Remediation centers on using a constant-time comparison function and ensuring that the code path for valid and invalid signatures takes roughly the same amount of time. In Node.js, crypto.timingSafeEqual is the standard approach for comparing Buffers of equal length. If lengths differ, you should return a failure without performing the comparison, but you must still execute a dummy constant-time comparison to avoid leaking length information via timing.
Example of a vulnerable Express route using Hmac:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SECRET = 'super-secret-key';
function computeSignature(params) {
const hmac = crypto.createHmac('sha256', SECRET);
hmac.update(params.resourceId + params.action + params.timestamp);
return hmac.digest('hex');
}
app.post('/action', (req, res) => {
const expected = computeSignature(req.body);
const received = req.body.signature;
if (expected === received) { // vulnerable: early return on mismatch
// proceed
res.send({ ok: true });
} else {
res.status(401).send({ error: 'invalid signature' });
}
});
Fixed version using crypto.timingSafeEqual with consistent control flow:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SECRET = 'super-secret-key';
function computeSignature(params) {
const hmac = crypto.createHmac('sha256', SECRET);
hmac.update(params.resourceId + params.action + params.timestamp);
return hmac.digest('hex');
}
app.post('/action', (req, res) => {
const expectedHex = computeSignature(req.body);
const receivedHex = req.body.signature;
// Convert to Buffers with equal length; if received is malformed length, use dummy buffer
let isValid = false;
if (receivedHex.length === expectedHex.length) {
const expected = Buffer.from(expectedHex, 'hex');
const received = Buffer.from(receivedHex, 'hex');
// Ensure buffers are the same length (should be true due to above check)
if (expected.length === received.length) {
isValid = crypto.timingSafeEqual(expected, received);
}
}
// Always execute a dummy comparison to hide length differences if needed
if (!isValid) {
// Perform a dummy constant-time operation to avoid leaking length via timing
const dummy = crypto.timingSafeEqual(Buffer.alloc(32), Buffer.alloc(32));
// dummy is always true, but the call takes similar time as a real compare
res.status(401).send({ error: 'invalid signature' });
} else {
res.send({ ok: true });
}
});
Additional guidance:
- Ensure Hmac output encoding is consistent (e.g., hex or base64) on both sides to avoid length mismatches that could be probed.
- Validate and normalize input parameters before concatenation to avoid ambiguities that could affect the computed digest.
- If you use middleware to verify signatures, apply the same constant-time logic there to protect all endpoints uniformly.
- Combine with other security practices such as short timestamp windows and replay prevention to reduce the utility of any leaked information even if timing noise is minimal.
For teams using middleBrick, the CLI tool (middlebrick scan <url>) can detect whether responses exhibit timing-sensitive behavior patterns across multiple signature mismatches, while the Web Dashboard and GitHub Action integrations can track this risk category over time and fail builds if insecure comparison patterns are inferred from repeated scans.