Brute Force Attack in Express with Hmac Signatures
Brute Force Attack in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A brute force attack against an Express API that uses HMAC signatures attempts many secret-key guesses to forge valid authenticated requests. Even when the endpoint validates signatures, weaknesses in how the signature is applied can make brute force practical. For example, if the server compares signatures with a non-constant-time operation or exposes timing differences via response codes or latency, an attacker can infer partial correctness and iteratively guess the secret.
Consider an Express route that expects an HMAC in a header, where the server recomputes the HMAC over a known payload and path and compares it with the client-supplied value. If the comparison leaks information (e.g., returns 401 for malformed input before checking the HMAC, or returns different error messages), an attacker can mount a brute force or offline dictionary attack. Weak iteration counts, predictable nonces, or reused nonces further weaken the scheme. Attackers may also abuse missing rate limiting to send many requests per second, probing many keys within a short window.
Because HMAC itself is deterministic for a given key and message, offline brute force becomes feasible if the attacker can obtain a valid signature-message pair. Without salting or key stretching, low-entropy secrets are vulnerable to fast guessing. In Express, common misconfigurations that amplify risk include logging signatures in access logs, exposing endpoints without authentication, and failing to bind additional context (such as method and scope) into the signed payload, enabling signature reuse across endpoints.
The LLM/AI Security checks in middleBrick include active prompt injection testing and system prompt leakage detection, which are unrelated to HMAC brute force, but its inventory and unsafe consumption checks can highlight missing rate limiting and unsafe handling of inputs that facilitate brute force. The scanner also cross-references OpenAPI definitions with runtime findings to confirm whether authentication and rate limiting are declared and exercised.
Hmac Signatures-Specific Remediation in Express — concrete code fixes
To mitigate brute force risks, use strong keys, constant-time comparison, key stretching, and binding contextual data into the signed payload. Below are concrete Express examples that address these concerns.
1) Use crypto.timingSafeEqual for signature comparison to prevent timing attacks:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.HMAC_SECRET;
if (!SHARED_SECRET || SHARED_SECRET.length < 32) {
throw new Error('HMAC_SECRET must be a strong, high-entropy string');
}
function verifySignature(payload, receivedHmac) {
const expected = crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
const received = Buffer.from(receivedHmac, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
if (received.length !== expectedBuf.length) return false;
return crypto.timingSafeEqual(received, expectedBuf);
}
app.post('/api/action', (req, res) => {
const { amount, currency } = req.body;
const message = `${amount}:${currency}:${req.headers['x-nonce']}`;
const signature = req.headers['x-hmac'];
if (!signature || !verifySignature(message, signature)) {
return res.status(401).json({ error: 'invalid signature' });
}
// process action
res.json({ ok: true });
});
2) Bind method, path, and a per-request nonce into the signed payload to prevent replay and scope escalation:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.HMAC_SECRET;
function signPayload(payload) {
return crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
}
app.post('/api/transfer', (req, res) => {
const { from, to, amount, nonce, timestamp } = req.body;
const payload = JSON.stringify({ method: req.method, path: req.path, from, to, amount, nonce, timestamp });
const signature = req.headers['x-hmac'];
if (!signature || !verifySignature(payload, signature)) {
return res.status(401).json({ error: 'invalid signature' });
}
// additional business logic
res.json({ ok: true });
});
function verifySignature(data, received) {
const expected = crypto.createHmac('sha256', SHARED_SECRET).update(data).digest('hex');
const receivedBuf = Buffer.from(received, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
if (receivedBuf.length !== expectedBuf.length) return false;
return crypto.timingSafeEqual(receivedBuf, expectedBuf);
}
3) Enforce rate limiting to curb online brute force attempts:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'rate limit exceeded' }
});
app.use('/api/', limiter);
4) Use high-entropy secrets and rotate them periodically; store them in environment variables or a secrets manager, never in source code. Prefer an algorithm like HMAC-SHA256 and avoid custom truncation or homegrown schemes. If you need key stretching, apply a KDF (e.g., HKDF) to derive subkeys per context rather than inventing your own mechanism.