Timing Attack in Feathersjs with Basic Auth
Timing Attack in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
A timing attack in a Feathersjs service that uses HTTP Basic Auth can occur because password verification is often implemented with a naive string comparison. When comparing the expected password hash or digest with the value derived from the request's Authorization header, an early exit on the first mismatching character can introduce measurable response time differences. An attacker can measure these differences to incrementally learn information about the stored credential. Feathersjs commonly relies on hooks for authentication; if the hook performs the comparison directly in JavaScript without constant-time logic, the variability in processing time becomes observable to a remote network attacker.
In the Basic Auth flow, the client sends an Authorization header formatted as Basic base64(username:password). Feathersjs receives this in a hook, decodes the payload, and must validate the password. If the validation code uses standard equality (e.g., == or ===) on byte sequences or strings, the runtime may stop checking as soon as a mismatch is found. Although Feathersjs itself does not prescribe a particular comparison method, implementations that do not explicitly enforce constant-time comparison expose a measurable difference that can be exploited via statistical analysis across many requests.
The attack surface is further shaped by the unauthenticated scan nature of middleBrick. middleBrick runs 12 security checks in parallel, including Authentication and Input Validation, and it can detect whether timing-related behaviors are present without credentials. By submitting controlled requests and measuring response times, a scanner can infer whether the service responds faster for incorrect usernames versus incorrect passwords, or vice versa. Although middleBrick reports findings and remediation guidance rather than fixing the issue, such observations highlight the importance of implementing constant-time comparison in authentication hooks to mitigate timing-based information leakage.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
To mitigate timing attacks in Feathersjs when using Basic Auth, ensure that password verification is performed with a constant-time comparison function. Instead of using native JavaScript equality, use a library that provides constant-time byte comparison, such as crypto.timingSafeEqual available in Node.js. This prevents early exit behavior and ensures that the validation path takes the same amount of time regardless of how much of the input matches.
Below is an example of a Feathersjs authentication hook that decodes the Basic Auth header and performs constant-time verification against a stored hash. The implementation assumes passwords are stored as hashes and uses crypto.timingSafeEqual to compare the derived digest with the stored digest after decoding both to buffers of fixed length.
const crypto = require('crypto');
function timingSafeBufferCompare(a, b) {
// Ensure both buffers are the same length; if not, compare against a dummy buffer
const maxLength = Math.max(a.length, b.length);
const bufA = Buffer.concat([a, Buffer.alloc(maxLength - a.length)]);
const bufB = Buffer.concat([b, Buffer.alloc(maxLength - b.length)]);
return crypto.timingSafeEqual(bufA, bufB);
}
function hashPassword(password, salt) {
// Use a strong, salted derivation; adjust digest and iterations for your security profile
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
}
module.exports = function() {
const app = this;
app.hooks.before.push({
name: 'basic-auth-timing-safe',
async before(hook) {
if (hook.method !== 'find' && hook.method !== 'get') {
return;
}
const authHeader = hook.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
// Let other hooks handle missing auth; do not reveal timing differences here
return;
}
const base64 = authHeader.slice('Basic '.length);
let decoded;
try {
decoded = Buffer.from(base64, 'base64');
} catch (error) {
// Avoid branching on decode errors; return a generic timing-safe failure
hook.result = { authenticated: false };
return;
}
const separatorIndex = decoded.indexOf(58); // 58 is ':'
if (separatorIndex <= 0) {
hook.result = { authenticated: false };
return;
}
const username = decoded.slice(0, separatorIndex).toString('utf8');
const passwordCandidate = decoded.slice(separatorIndex + 1).toString('utf8');
// Fetch user record; ensure the query path does not leak timing differences via existence checks
const user = await app.service('users').get(username).catch(() => null);
if (!user || !user.passwordHash || !user.salt) {
// Compute a dummy hash to keep timing consistent
const dummyHash = hashPassword('dummy', Buffer.alloc(16));
timingSafeBufferCompare(dummyHash, Buffer.from(user.passwordHash || dummyHash));
hook.result = { authenticated: false };
return;
}
const candidateHash = hashPassword(passwordCandidate, user.salt);
const authenticated = timingSafeBufferCompare(candidateHash, user.passwordHash);
hook.result = { authenticated };
if (!authenticated) {
// Return a generic response; avoid signaling which part failed
hook.result = { authenticated: false };
}
}
});
};
Additional considerations include ensuring that the user lookup path does not introduce timing variance based on whether the username exists. Where possible, use a fixed-duration dummy computation when the user is not found to keep the overall execution time consistent. middleBrick can validate that your service exhibits uniform response characteristics by running its Authentication and Input Validation checks during scans; developers should review the findings and remediation guidance to confirm that constant-time practices are applied consistently across the authentication path.