Cache Poisoning in Feathersjs with Jwt Tokens
Cache Poisoning in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Cache poisoning in a Feathersjs application that uses Jwt Tokens occurs when an attacker causes cached representations of an authenticated response to be stored under a key that includes or is influenced by attacker-controlled data. Because Feathersjs often caches data at the service or transport layer and Jwt Tokens carry identity claims, a cached entry may be reused across users if the cache key does not incorporate the token’s subject or roles. This can lead to one user seeing another user’s data, or an attacker forcing cached malicious representations to be served.
For example, consider a Feathers service that caches user profile responses and derives the cache key only from the request path and query parameters, ignoring that the request includes an Authorization header with a Jwt Token. Two different users with different identities hit the same endpoint /api/profile; if caching is based solely on the path, the second user’s cached response may be served to the first user. An attacker could register or manipulate parameters (e.g., query strings) so that a victim’s token context is reflected in a cache entry that is later reused for a different token context.
Jwt Tokens commonly include claims such as sub, roles, and tenant. If these claims are not part of the cache key, cached data can be mismatched. Additionally, if the service caches representations that embed user-specific fields derived from the token’s payload without validating that the cached payload matches the requesting token’s subject, an attacker may leverage predictable or tampered JWTs (e.g., with altered subject or role claims) to poison the cache for others. This is especially risky when token validation is performed once at the transport layer but authorization checks and cache decisions are not consistently aligned with the token’s claims.
In Feathers, if authentication is handled via hooks that verify Jwt Tokens and attach the decoded payload to params, but caching occurs before or outside the hook’s user-aware logic, the cache may not differentiate between users. For instance, a hook that sets params.account from the token payload must ensure that any caching layer uses values from params.account as part of the cache key. Without this, the combination of Jwt Tokens and caching can unintentionally expose one user’s data to another or enable injection of malicious cached content.
Real-world attack patterns include an authenticated user manipulating a non-user-specific query parameter to cause the service to cache a response with attacker-influenced data, then tricking other users into triggering the same cache entry. OWASP API Top 10 A01:2023 Broken Object Level Authorization and A05:2023 Security Misconfiguration are relevant when cache keys omit identity claims present in Jwt Tokens. PCI-DSS and SOC2 controls also require that authenticated sessions not share cached representations across distinct identities.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring cache keys incorporate token-derived identity claims and that cached responses are never shared across distinct token contexts. In Feathers, this means including values from the authenticated Jwt Token payload—such as sub, roles, and tenant identifiers—in the cache key, and validating that cached data matches the requesting token’s context before use.
Below is a concrete Feathers hook that verifies a Jwt Token and attaches claims to params, followed by a caching strategy that uses those claims in the cache key.
// src/hooks/authentication.js
const { AuthenticationError } = require('@feathersjs/errors');
const jwt = require('jsonwebtoken');
module.exports = function authHook(options = {}) {
return async context => {
const { headers } = context.params;
const token = headers && headers.authorization && headers.authorization.split(' ')[1];
if (!token) {
throw new AuthenticationError('Not authenticated');
}
let payload;
try {
payload = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new AuthenticationError('Invalid token');
}
// Attach claims used for cache keying and authorization
context.params.account = {
subject: payload.sub,
roles: payload.roles || [],
tenant: payload.tenant
};
return context;
};
};
Next, a service hook that incorporates account claims into the cache key. This example assumes a simple in-memory cache layer; adapt the key construction to your caching provider (e.g., Redis) while preserving the same principles.
// src/hooks/caching.js
module.exports = function cachingHook(options = {}) {
const cache = new Map(); // Replace with your cache store
return async context => {
const { path, account } = context.params;
if (!account || !account.subject) {
// Not authenticated via JWT; skip cache key personalization
return context;
}
// Build a cache key that includes identity claims
const cacheKey = [
path,
account.subject,
(account.roles || []).join(','),
account.tenant || 'default'
].join('|');
if (cache.has(cacheKey)) {
context.result = cache.get(cacheKey);
context.cacheHit = true;
return context;
}
// Proceed with the service call; context.result will be set by the service
await context.app.service(path)._create(context);
cache.set(cacheKey, context.result);
return context;
};
};
In a real Feathers app, you would register these hooks appropriately. For example:
// src/app.js
const feathers = require('@feathersjs/feathers');
const authentication = require('./hooks/authentication');
const caching = require('./hooks/caching');
const profileService = require('./services/profile');
const app = feathers();
app.configure(authentication);
app.use('/profile', profileService);
app.service('profile').hooks({
before: {
all: [caching()], // Ensure caching runs after authentication so account is available
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
});
Additional remediation steps include avoiding caching of sensitive or user-specific responses unless the cache key explicitly includes the token’s subject and roles, and ensuring that any cached data does not embed user claims that could be swapped via token manipulation. For multi-tenant setups, include the tenant claim in the cache key to prevent cross-tenant leakage. Periodically review token payloads for unnecessary claims that could inadvertently influence caching behavior.
middleBrick’s scans can help identify missing account context in cache keys and misconfigurations where Jwt Tokens are accepted but not considered in caching logic. Using the CLI (middlebrick scan <url>) or the GitHub Action to add API security checks to your CI/CD pipeline can surface these issues before deployment.