Use After Free in Feathersjs with Jwt Tokens
Use After Free in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Use After Free (UAF) in a FeathersJS application that uses JWT tokens occurs when an application logic error causes a reference to an object or data structure to be used after it has been deallocated or reset. In the context of FeathersJS and JWT authentication, this can manifest when token payloads, decoded user contexts, or cached authorization states are reused or accessed after being invalidated, replaced, or explicitly cleared.
FeathersJS is a framework that often relies on hooks to manage authentication. A common pattern is to verify a JWT token in an authentication hook, decode its payload, and attach the user object to the connection or request context (e.g., context.params.user). If the application subsequently modifies or replaces this user object—such as during a logout or session revocation routine—and a later hook or service method still references the old context, it may read or act on stale data. This becomes a UAF-like condition when the underlying data structure is effectively freed (e.g., removed from internal caches or replaced with a new instance) but a reference is still used to authorize a request.
JWT tokens themselves are typically stateless and self-contained, but FeathersJS applications often introduce stateful elements to handle revocation, such as token denylists or per-user session caches. If a token is revoked and its associated session data is freed from the cache, but a concurrent or subsequent request with the same JWT token is processed and attempts to access that freed session data, the application may exhibit UAF behavior. For example, a hook might check a denylist cache using a user identifier extracted from the JWT payload, but if the cache entry has been removed and the lookup returns an unexpected null or recycled object, the authorization logic might incorrectly grant access or misinterpret permissions.
Real-world attack patterns that exploit such weaknesses include scenarios where an attacker triggers session invalidation (e.g., logout) to free session state, then attempts to reuse an otherwise valid JWT token in a different request path. If the application improperly handles the transition between valid and freed session states, the token may be evaluated against stale or incorrect context, potentially bypassing intended authorization checks. This is particularly relevant when authorization decisions depend on mutable user state rather than purely on the signed claims within the JWT itself.
Consider a FeathersJS hook that attaches user data to the context after verifying a JWT token:
// src/hooks/authentication.js
const { AuthenticationError } = require('@feathersjs/errors');
module.exports = function () {
return async context => {
const { accessToken } = context.headers.authorization?.split(' ') || ['', ''];
if (!accessToken) {
throw new AuthenticationError('Authentication required');
}
try {
const decoded = context.app.get('jwtService').verify(accessToken);
const user = await context.app.service('users').get(decoded.userId);
// Attaching user to context for downstream hooks/services
context.params.user = user;
return context;
} catch (error) {
throw new AuthenticationError('Invalid token');
}
};
};
If the user object retrieved from the service is later replaced or cleared (for instance, due to a cache eviction or session cleanup) while the request continues to use the context.params.user reference, a UAF condition may occur. This risk is heightened if the application logic relies on mutable properties of the user object for authorization checks in subsequent hooks or services.
To mitigate UAF risks in this combination, ensure that authorization decisions are based on immutable data from the JWT payload (such as user ID, roles, or scopes) rather than on mutable runtime state that may be freed or altered. Validate the presence and integrity of any cached session data before use, and avoid retaining references to objects that may be deallocated or reused within the request lifecycle.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on minimizing reliance on mutable runtime state for authorization and ensuring that JWT-based contexts are either immutable or safely re-validated before use. Below are concrete code examples for secure FeathersJS implementations.
1. Use only JWT claims for authorization decisions
Instead of attaching a full user object from the database to the context, extract and use only the necessary claims from the verified JWT payload. This avoids issues where the user object might be freed or altered.
// src/hooks/authentication-safe.js
const { AuthenticationError } = require('@feathersjs/errors');
module.exports = function () {
return async context => {
const authHeader = context.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new AuthenticationError('Authentication required');
}
const token = authHeader.slice(7);
const decoded = context.app.get('jwtService').verify(token);
// Use only immutable claims from the token
if (!decoded.sub || !decoded.scope) {
throw new AuthenticationError('Invalid token claims');
}
// Attach minimal, immutable data to context
context.params.authInfo = {
userId: decoded.sub,
scope: decoded.scope,
roles: decoded.roles || []
};
return context;
};
};
2. Re-validate critical session state before use
If your application requires checking a denylist or session cache, ensure that the cache entry exists and is valid at the moment of use, and avoid holding references beyond the immediate check.
// src/hooks/revocation-check.js
const { ForbiddenError } = require('@feathersjs/errors');
module.exports = function () {
return async context => {
const authHeader = context.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) return context;
const token = authHeader.slice(7);
const decoded = context.app.get('jwtService').verify(token, { ignoreExpiration: false });
// Check denylist with a direct, short-lived lookup
const isRevoked = await context.app.service('token-denylist').find({
query: {
tokenId: decoded.jti,
$limit: 1
}
});
if (isRevoked.data && isRevoked.data.length > 0) {
throw new ForbiddenError('Token has been revoked');
}
// Do not store denylist result for later use; validate per request
return context;
};
};
3. Avoid caching user objects in mutable request context
Ensure that any user or session data attached to context.params is either derived from the JWT or freshly fetched and not stored for reuse across hooks in a way that could lead to use-after-free conditions.
// src/hooks/attach-auth-data.js
module.exports = function (options = {}) {
return async context => {
// Do not overwrite with mutable external data if not necessary
const authHeader = context.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.slice(7);
const decoded = context.app.get('jwtService').verify(token);
// Keep only verified claims
context.params.auth = {
subject: decoded.sub,
tenant: decoded.tenant
};
}
return context;
};
};
4. Configure JWT service with strict validation
Ensure your JWT service configuration enforces strong algorithms and validates standard claims to reduce the impact of token misuse.
// src/app.js
const jwt = require('@feathersjs/authentication-jwt');
app.configure(jwt({
secret: process.env.JWT_SECRET,
algorithms: ['HS256'],
issuer: 'myapp',
audience: 'myapp-users'
}));
By adopting these practices, you reduce the likelihood of Use After Free conditions related to JWT token handling in FeathersJS, ensuring that authorization logic remains robust and based on verifiable, immutable token data.