Timing Attack in Feathersjs with Api Keys
Timing Attack in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability
A timing attack in a Feathersjs service that uses API keys can occur when key validation logic does not execute in constant time. Feathersjs is often used with an authentication layer that checks an incoming API key against a database or configuration. If the comparison between the submitted key and the stored key is performed using a standard equality check (e.g., keyStored === keyProvided), the operation may short-circuit: upon the first mismatched character, the check stops and returns false. This causes variable execution time that depends on how many leading characters match. An attacker who can send multiple requests and measure response times can infer whether a prefix of the API key is correct, gradually learning the full key byte by byte without ever triggering an authentication failure from invalid key format.
In Feathersjs, this typically arises in a custom hook or service that manually validates an API key header (e.g., authorization: ApiKey <key>) before allowing access to a service. If the lookup or comparison logic is not hardened, the unauthenticated attack surface includes endpoints that accept API keys, and the timing differences can be measurable across network hops. For example, a hook that does a database find by key and then compares may leak information through response time differences depending on how early the mismatch occurs. An attacker can use these differences to distinguish a valid key prefix from an invalid one, effectively performing a remote timing oracle against the API. This becomes especially relevant when API keys are used for access control without additional protections such as constant-time comparison or rate limiting that obscures timing signals.
The risk is compounded when the API key is accepted in unauthenticated (black-box) scans, as the scanner can send many requests and observe subtle timing deviations. Because Feathersjs services often expose multiple endpoints, an attacker can correlate timing behavior across endpoints that share the same validation logic. While Feathersjs itself does not introduce the vulnerability, insecure implementation of key validation does. Proper mitigation requires ensuring that key checks execute in constant time and that the service does not inadvertently expose timing information through variable response behavior.
Api Keys-Specific Remediation in Feathersjs — concrete code fixes
To remediate timing risks with API keys in Feathersjs, replace standard equality comparisons with a constant-time comparison routine and ensure that key lookup does not leak validity through timing or error messages. Below are concrete, working examples that you can adapt to your service.
1. Constant-time comparison utility
Use a constant-time comparison function that always iterates over the full length of the expected key, preventing early-exit timing leaks. Node.js provides crypto.timingSafeEqual for this purpose when comparing Buffers of equal length.
// utils/constantTimeCompare.js
const crypto = require('node:crypto');
/**
* Constant-time comparison for strings or buffers.
* Returns true if `a` and `b` are equal, false otherwise.
* Always performs work proportional to the length of `a`.
*/
function timingSafeEqual(a, b) {
// Normalize inputs to Buffer
const bufA = Buffer.isBuffer(a) ? a : Buffer.from(a, 'utf8');
const bufB = Buffer.isBuffer(b) ? b : Buffer.from(b, 'utf8');
// Lengths must match to avoid leaking size information
if (bufA.length !== bufB.length) {
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
module.exports = { timingSafeEqual };Example: Hook validating API key using constant-time comparison
In a Feathersjs service, create an authentication hook that retrieves the stored key and compares it using the constant-time utility. Avoid returning early on mismatch and avoid branching on key validity before the comparison.
// hooks/authenticate-api-key.js
const { timingSafeEqual } = require('../utils/constantTimeCompare');
module.exports = function authenticateApiKey(options = {}) {
return async context => {
const { app, params } = context;
const providedKey = context.params.headers && context.params.headers['authorization'];
if (!providedKey || !providedKey.startsWith('ApiKey ')) {
// Return a generic error without indicating whether the key was malformed
throw new Error('Unauthorized');
}
const provided = providedKey.slice('ApiKey '.length);
// Example: fetch stored key from a service (e.g., users/settings)
// Ensure this lookup does not leak timing via absence/presence of record.
// One approach: always use a fixed dummy key when no record exists.
const storedRecord = await app.service('api-keys').find({ query: { $limit: 1 } });
const storedKey = (storedRecord && storedRecord.data && storedRecord.data[0] && storedRecord.data[0].key) || '';
// Use constant-time comparison to avoid timing leaks
const isValid = timingSafeEqual(storedKey, provided);
if (!isValid) {
throw new Error('Unauthorized');
}
// Attach identity or scopes to context for downstream use
context.result = { authenticated: true, scope: 'api-key' };
return context;
};
};
// Attach the hook to a service
const someService = app.service('some-path');
someService.hooks({
before: {
all: [require('./hooks/authenticate-api-key')]
}
});Example: Using environment-stored key with constant-time comparison
If you store a single API key in environment variables (e.g., for simplicity), compare the provided key against the environment key using the same constant-time utility. This avoids timing leaks while keeping deployment straightforward.
// hooks/authenticate-api-key-env.js
const { timingSafeEqual } = require('../utils/constantTimeCompare');
module.exports = function authenticateApiKeyEnv() {
return async context => {
const providedKey = context.params.headers && context.params.headers['authorization'];
if (!providedKey || !providedKey.startsWith('ApiKey ')) {
throw new Error('Unauthorized');
}
const provided = providedKey.slice('ApiKey '.length);
const stored = process.env.API_KEY || '';
if (!timingSafeEqual(stored, provided)) {
throw new Error('Unauthorized');
}
context.result = { authenticated: true };
return context;
};
};
// Usage in service initialization
const app = require('feathers')();
app.configure(require('./hooks/authenticate-api-key-env'));
app.use('/secure', require('./secure-service'));