Side Channel Attack in Feathersjs with Jwt Tokens
Side Channel Attack in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A side channel attack in a FeathersJS application that uses JWT tokens exploits indirect information leaks rather than breaking the token algorithm itself. In this stack, timing differences, error messages, or behavioral variations reveal details about authentication or authorization that an attacker can use to infer valid tokens or escalate access.
FeathersJS is a framework that typically sits between an HTTP transport (REST or Socket) and service logic. When JWTs are used, the framework often relies on hooks to authenticate requests. If these hooks are implemented in a way that behaves differently depending on whether a token is valid, expired, malformed, or associated with a missing or disabled user, an attacker can measure response times or observe error distinctions to learn something about the token or the underlying user directory.
For example, consider a FeathersJS hook that first verifies the JWT signature and then performs a database lookup for the associated user. An attacker who can control the request timing may observe that a request with a syntactically valid but unknown token consistently takes longer than a request with an obviously malformed token. The additional time may reflect the database query for user lookup, thereby confirming that the token’s embedded subject corresponds to an existing user record. This is a classic timing side channel applied to JWT validation flows.
Another vector arises from user enumeration via error handling. If the server returns distinct error messages such as "invalid token" versus "user not found" or "token expired" versus "user disabled," an attacker can distinguish between different failure conditions. In FeathersJS, this often happens in authentication hooks or in services that wrap the JWT verification logic. For instance, returning a 401 with a verbose message can disclose whether the token was recognized but invalid or whether the identity linked to the token cannot be located. Such distinctions enable an attacker to iteratively refine guesses about valid tokens or associated user identifiers.
SSRF and network-based side channels can compound the risk. A FeathersJS service that accepts URLs or endpoints from clients and makes outbound HTTP requests based on token claims may inadvertently expose introspection capabilities. If an attacker can supply a target URL that the server attempts to reach, they may infer properties of the server-side token validation or user resolution logic by observing whether certain requests trigger errors or take longer to complete. This is particularly relevant when JWTs include host or scope claims that influence which backend endpoints the server attempts to contact.
Additionally, improper handling of JWT claims such as audience or issuer can lead to authorization side channels. In FeathersJS, a hook that validates these claims might short-circuit early for an invalid audience but proceed with additional logic for a valid audience, creating timing or behavioral differences. An attacker who can craft tokens with varying claims can measure these differences and map out the validation path, potentially learning which claim combinations are accepted or rejected by the service.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on making authentication and token validation behavior consistent and side-channel resistant. In FeathersJS, this means standardizing responses, avoiding conditional branching on sensitive data, and ensuring token verification steps do not leak information through timing or errors.
First, ensure that all JWT verification and user lookup steps take approximately the same amount of time regardless of token validity. This can be achieved by performing a constant-time verification flow, such as always running a dummy lookup or using a delay mechanism in non-production environments to obscure timing differences. Below is an example of a FeathersJS hook that validates JWTs and normalizes execution time by performing a placeholder lookup when the token is invalid:
// src/hooks/authentication.js
const { AuthenticationError } = require('@feathersjs/errors');
const jwt = require('jsonwebtoken');
module.exports = function authentication(options = {}) {
return async context => {
const { accessToken } = context.params.headers || {};
let user = null;
let tokenValid = false;
if (accessToken && typeof accessToken === 'string') {
try {
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET, { algorithms: ['HS256'] });
// Always perform a user lookup to keep timing consistent; in production, index the user table.
user = await context.app.service('users').get(decoded.userId).catch(() => null);
tokenValid = !!user;
} catch (err) {
// Do not reveal the exact failure reason; treat as missing user.
user = await context.app.service('users').get(0).catch(() => null); // dummy lookup
tokenValid = false;
}
} else {
// No token provided; run dummy lookup to obscure timing.
await context.app.service('users').get(0).catch(() => null);
}
if (!tokenValid) {
throw new AuthenticationError('Invalid token');
}
context.params.user = user;
return context;
};
};
Second, standardize error responses to avoid user enumeration. Instead of returning distinct messages for "invalid token," "expired token," or "user not found," use a generic authentication failure message and a uniform HTTP status code. Here is a hook that enforces this policy:
// src/hooks/standardize-auth-errors.js
module.exports = function standardizeAuthErrors(options = {}) {
return context => {
if (context.error && context.error.name === 'AuthenticationError') {
// Log the detailed error internally if needed, but return a generic response.
context.error.message = 'Authentication failed';
context.error.code = 401;
}
return context;
};
};
Third, validate JWT claims in a consistent order and avoid early branching based on optional claims. Always parse and validate all relevant claims before making authorization decisions, and then apply a uniform check. For example:
// src/hooks/validate-jwt-claims.js
module.exports = function validateJwtClaims(options = {}) {
return async context => {
const { accessToken } = context.params.headers || {};
if (!accessToken) {
return context;
}
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET, { algorithms: ['HS256'] });
const now = Math.floor(Date.now() / 1000);
// Validate standard claims uniformly.
if (decoded.iss !== process.env.JWT_ISSUER) {
throw new Error('Invalid token');
}
if (decoded.aud !== process.env.JWT_AUDIENCE) {
throw new Error('Invalid token');
}
if (decoded.nbf && decoded.nbf > now) {
throw new Error('Token not yet valid');
}
if (decoded.exp && decoded.exp < now) {
throw new Error('Token expired');
}
// Attach claims for downstream use without further branching on failure.
context.params.authClaims = decoded;
return context;
};
};
Finally, integrate these hooks into your FeathersJS application and ensure they are applied before service methods that require authentication. Using the FeathersJS hook chain, you can combine authentication and claim validation while preserving consistent behavior:
// src/app.js
const authentication = require('./hooks/authentication');
const standardizeAuthErrors = require('./hooks/standardize-auth-errors');
const validateJwtClaims = require('./hooks/validate-jwt-claims');
app.configure(authentication());
app.configure(validateJwtClaims());
app.configure(standardizeAuthErrors());
// Define services after hooks are configured.
app.use('/messages', require('./services/messages'));