Timing Attack in Adonisjs with Api Keys
Timing Attack in Adonisjs with Api Keys — how this variable-time comparison creates or exposes the vulnerability
A timing attack in AdonisJS when protecting routes with API keys occurs because many implementations compare the client-supplied key with the stored key using a variable-time string comparison. In JavaScript/Node, key1 === key2 is not constant-time at the cryptographic level; an attacker can measure response times to learn how many leading characters match. Because API key validation often happens before authorization checks, an attacker can send many requests with keys that gradually converge toward the correct key and infer the correct key byte-by-byte from timing differences.
In AdonisJS, API key handling is commonly implemented in middleware. Consider a naive middleware that retrieves the expected key from the database and compares it directly:
// routes/middleware/keyAuth.js
const ApiKey = use('App/Models/ApiKey');
async function keyAuth ({ request, response, next }, scope) {
const supplied = request.header('X-API-Key');
if (!supplied) {
return response.status(401).json({ error: 'missing_key' });
}
// WARNING: Database lookup per request; comparison may be variable-time
const keyRecord = await ApiKey.query().where('key', supplied).first();
if (!keyRecord) {
// Attacker can observe slightly different timing when a record exists vs not
return response.status(401).json({ error: 'invalid_key' });
}
// If scope check also uses variable-time logic, further leakage occurs
if (scope && !keyRecord.scopes.includes(scope)) {
return response.status(403).json({ error: 'insufficient_scope' });
}
await next();
}
module.exports = keyAuth;
The where('key', supplied).first() pushes the comparison into the database layer; depending on the driver and query plan, subtle timing differences can still leak information about prefix matches. Moreover, if the comparison of scopes or other metadata uses array methods like .includes, the runtime may short-circuit on the first matching element, again creating observable timing differences. An attacker can send crafted keys and measure round-trip times to gradually deduce the correct API key, bypassing intended secrecy even when the key is long and random.
An additional risk arises when API keys are used to retrieve secrets (e.g., database credentials or JWT signing keys) and the branching logic depends on key validity. The path taken — whether to load secrets or return an error — may itself be observable via timing or error messages, compounding the exposure. This is especially relevant when the same endpoint serves multiple clients with different keys; an attacker can learn which keys are valid and which are not through timing, even without knowing the exact key values initially.
To map findings to standards, this pattern aligns with OWASP API Top 10 2023: Broken Object Level Authorization (BOLA) and Security Misconfiguration, and can affect compliance frameworks such as SOC2 and PCI-DSS when secrets are derivable from API key handling. middleBrick’s 12 security checks run in parallel and include Authentication and BOLA/IDOR tests that can surface timing-related anomalies by correlating runtime behavior with spec-defined authentication schemes. If you use the CLI, you can run middlebrick scan <url> to detect such issues; the GitHub Action can fail builds if risk scores drop below your chosen threshold; and the MCP Server lets you scan APIs directly from your AI coding assistant.
Api Keys-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on ensuring constant-time comparison and avoiding branching that depends on secret key material. The primary fix is to replace direct string equality with a constant-time comparison function and to ensure that the existence check does not leak information via timing or error messages.
Use a constant-time comparison utility. AdonisJS projects often rely on Node’s built-in mechanisms; you can implement a safe compare using crypto.timingSafeEqual. Ensure both buffers are the same length by hashing the supplied key or using a fixed-length representation before comparison:
// app/Helpers/constantCompare.js
const crypto = require('crypto');
function safeCompare(a, b) {
// Normalize to buffers of equal length (e.g., SHA256 fixed length)
const bufA = crypto.createHash('sha256').update(a, 'utf8').digest();
const bufB = crypto.createHash('sha256').update(b, 'utf8').digest();
if (bufA.length !== bufB.length) {
// For fixed-length hashes this is constant, but keep explicit for safety
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
module.exports = { safeCompare };
Refactor the middleware to perform a constant-time check and avoid early branching on key validity. Fetch the record by a stable lookup (e.g., by hashed key or by an indexed non-sensitive identifier) and then compare in constant time:
// routes/middleware/keyAuth.js
const ApiKey = use('App/Models/ApiKey');
const { safeCompare } = use('App/Helpers/constantCompare');
async function keyAuth ({ request, response, next }) {
const supplied = request.header('X-API-Key');
if (!supplied) {
return response.status(401).json({ error: 'missing_key' });
}
// Fetch by a stable, non-sensitive lookup (e.g., keyId or hash)
// Here we assume you store a keyHash in the DB and compute suppliedHash
const suppliedHash = crypto.createHash('sha256').update(supplied, 'utf8').digest('hex');
const keyRecord = await ApiKey.query().where('key_hash', suppliedHash).first();
// Always perform a constant-time compare to avoid leaking existence via timing
const isValid = keyRecord ? safeCompare(keyRecord.key_hash, suppliedHash) : false;
if (!isValid) {
// Return a generic error with consistent timing; avoid differentiating reasons
return response.status(401).json({ error: 'invalid_key' });
}
if (keyRecord.scopes && !keyRecord.scopes.includes('required_scope')) {
return response.status(403).json({ error: 'insufficient_scope' });
}
await next();
}
Store and compare key hashes rather than raw keys to avoid storing or comparing secrets directly. If you must compare raw keys (not recommended), ensure you use crypto.timingSafeEqual with fixed-length buffers derived from the keys. Avoid branching on the key’s validity — e.g., do not include key-dependent logic paths that an attacker can probe via timing. middleBrick’s continuous monitoring (Pro plan) can help detect residual timing anomalies across scans, and its CI/CD integration can fail builds if risk thresholds are violated.
Additional hardening steps include rate limiting at the edge, using constant-time algorithms for any cryptographic operations, and ensuring that error messages and status codes do not reveal whether a key existed. These practices align with OWASP API Top 10 and support compliance mappings for SOC2 and PCI-DSS by reducing the attack surface around key validation.