Side Channel Attack in Feathersjs with Basic Auth
Side Channel Attack in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
A side channel attack in a Feathersjs application that uses HTTP Basic Auth occurs when an attacker infers sensitive information from indirect signals rather than by breaking the authentication algorithm itself. Timing differences in how Feathersjs handles requests for valid versus invalid usernames or passwords can be observable over the network. If the server performs credential verification by first locating a user record and then comparing the provided password hash, the comparison may short-circuit or take longer for a valid user, creating a measurable timing deviation.
Consider a Feathersjs service where authentication is implemented in a hook. A naive implementation might retrieve a user by username and then compare the hash of the submitted password with the stored hash. If the hash comparison is not constant-time, an attacker who can measure response times with high precision may distinguish between a user that exists and one that does not, or between a correct and incorrect password for an existing user. This becomes a side channel because the protocol (Basic Auth) does not directly leak the password, but the observable behavior of the server does.
Basic Auth transmits credentials in an encoded (not encrypted) header on each request. While the encoding is not encryption, the repeated transmission of the same credentials in each request can expose patterns. If the server logs or rate-limits authentication attempts per user, an attacker may correlate request frequency with timing or error responses. In a Feathersjs application, if different authentication failure paths produce distinct status codes or response sizes (e.g., 401 vs 403, or different JSON error shapes), an attacker can use these differences to infer validity of credentials without needing to crack the hash.
Additionally, network-level observations such as packet timing and TLS handshake behavior can complement application-level side channels. An attacker on a shared network or positioned to observe latency can combine timing measurements with knowledge of the Feathersjs service’s deployment characteristics to refine an attack. Because Basic Auth lacks built-in protections like rate limiting at the protocol layer, the burden falls on the application to ensure that authentication paths do not leak information through timing, error messages, or other observable behaviors.
To illustrate, a vulnerable Feathersjs hook might look like this, where timing differences can arise due to conditional logic based on user existence:
const { AuthenticationError } = require('@feathersjs/errors');
async function authHook(context) {
const { authorization } = context.headers;
if (!authorization || !authorization.startsWith('Basic ')) {
throw new AuthenticationError('Unauthorized');
}
const base64 = authorization.split(' ')[1];
const decoded = Buffer.from(base64, 'base64').toString('utf-8');
const [username, password] = decoded.split(':');
if (!username || !password) {
throw new AuthenticationError('Invalid credentials');
}
// Simulated user lookup; timing depends on DB response
const user = await context.app.service('users').Model.findOne({ where: { username } });
if (!user) {
// Trigger a generic hash compare with a dummy to reduce timing differences
await hashCompareDummy(password);
throw new AuthenticationError('Invalid credentials');
}
const isValid = await hashCompare(password, user.passwordHash);
if (!isValid) {
throw new AuthenticationError('Invalid credentials');
}
context.result = { userId: user.id };
return context;
}
async function hashCompareDummy(password) {
// Dummy work to reduce timing gaps (not a complete mitigation)
const dummyHash = '$2b$10$dummyplaceholderhashforconstanttime';
return require('bcrypt').compare(password, dummyHash);
}
In this example, the presence or absence of a user record and the branching logic introduce timing variability. Even with a dummy hash comparison, subtle differences in database query latency and conditional branches can be measurable. An attacker who sends many requests with crafted usernames can statistically infer whether a username exists, and subsequently mount an online password guessing attack.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring that all authentication paths execute in constant time and avoid leaking information through status codes, response sizes, or timing. In Feathersjs, this means standardizing error responses, using constant-time comparison functions, and ensuring that user lookup and verification steps do not branch on sensitive data.
First, always return the same HTTP status code for authentication failures (e.g., 401) and avoid exposing whether a username exists. Ensure response bodies are uniform in size and structure to prevent size-based side channels. Second, use a constant-time string comparison for password hashes. Many language runtimes provide constant-time comparison utilities; in Node.js, the crypto.timingSafeEqual function can be used with Buffers, or a library like bcrypt with proper configuration can handle secure comparison.
Here is a hardened Feathersjs hook example that mitigates timing and information-leak side channels:
const { AuthenticationError } = require('@feathersjs/errors');
const crypto = require('crypto');
const bcrypt = require('bcrypt');
async function secureAuthHook(context) {
const { authorization } = context.headers;
if (!authorization || !authorization.startsWith('Basic ')) {
throw new AuthenticationError('Unauthorized');
}
const base64 = authorization.split(' ')[1];
let decoded;
try {
decoded = Buffer.from(base64, 'base64').toString('utf-8');
} catch (err) {
throw new AuthenticationError('Invalid credentials');
}
const [username, password] = decoded.split(':');
if (!username || !password) {
throw new AuthenticationError('Invalid credentials');
}
// Always fetch a user record; if missing, use a dummy hash to keep timing consistent
const user = await context.app.service('users').Model.findOne({ where: { username } });
const dummyHash = await bcrypt.hash('dummy_password_salt_rounds_10', 10);
const targetHash = user ? user.passwordHash : dummyHash;
// Use bcrypt.compare which is designed to be constant-time
const isValid = await bcrypt.compare(password, targetHash);
if (!isValid) {
throw new AuthenticationError('Invalid credentials');
}
// Only after successful verification, attach user to context
if (user) {
context.result = { userId: user.id };
} else {
// Still return success to avoid signaling user existence; but safe to reject
throw new AuthenticationError('Invalid credentials');
}
return context;
}
Key practices derived from this remediation:
- Perform user lookup for every request to avoid branching on existence; use a dummy hash when the user is not found to keep execution time similar.
- Use a password hashing function that provides constant-time comparison internally (e.g., bcrypt) rather than manually comparing hashes with standard equality operators.
- Standardize error responses to have the same structure and size; avoid detailed error messages that can aid an attacker.
- Ensure TLS is enforced to protect credentials in transit, as Basic Auth transmits credentials on every request.
These changes reduce the feasibility of timing-based and other side channel attacks against Basic Auth in Feathersjs services.