Cache Poisoning in Feathersjs with Hmac Signatures
Cache Poisoning in Feathersjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker causes a cache to store malicious or incorrect responses that are then served to other users. In FeathersJS, when Hmac Signatures are used to authenticate requests but caching is applied without considering how the signature scope interacts with cached keys, the vulnerability surface expands.
FeathersJS typically uses hooks to add an Hmac signature to requests, often by hashing selected parts of the request such as the URL path, selected headers, and a timestamp. If the cache key is derived only from the request URL and does not incorporate the canonical Hmac string or the signature parameters, two different signed requests for the same URL can map to the same cache entry. This means a cached response signed with one set of parameters may be reused for another request where the signature is valid but the context differs.
Consider an endpoint that returns user-specific data but is cached based solely on the path. An authenticated user A requests /api/profile with an Hmac signature that includes a timestamp and a user scope. The response is cached. Later, user B requests the same path with a different Hmac signature that includes a different timestamp or scope. If the cache key does not include the signature or the canonical request components that differentiate users, user B might receive user A’s cached data, which may contain sensitive information or incorrect permissions.
Another scenario involves query parameters that affect response content but are omitted from the cache key. An attacker could craft a request with benign query values that produce a cacheable response. If the Hmac signature canonicalization excludes those query parameters, the cached response may be reused for a malicious query that only differs in ways not covered by the signature scope, leading to unintended data exposure or behavior manipulation.
Real-world attack patterns mirror issues seen in CVE-classified cache injection scenarios where trust boundaries between authenticated contexts are not enforced at the cache layer. In FeathersJS, this typically manifests as missing canonicalization of Hmac inputs into the cache key, inadequate validation of cached responses against the original signed request context, and insufficient separation of cache namespaces per user or scope. Because FeathersJS does not enforce a built-in cache, developers must ensure that any caching strategy explicitly includes Hmac-derived identifiers in the cache key and validates that cached responses match the original signed context.
Hmac Signatures-Specific Remediation in Feathersjs — concrete code fixes
To mitigate cache poisoning when using Hmac Signatures in FeathersJS, include the canonical Hmac representation in the cache key and scope cache entries to the authenticated context. Below are concrete code examples that demonstrate how to implement this correctly.
First, ensure your Hmac signature generation produces a canonical string that includes all request dimensions that affect the response, such as method, path, selected headers, and query parameters used in authorization decisions.
// utils/hmac.js
const crypto = require('node:crypto');
function buildCanonicalString(method, url, headers, selectedHeaders, timestamp) {
const sortedHeaders = selectedHeaders.map(h => `${h.toLowerCase()}:${headers[h.toLowerCase()]}`).join('\n');
return [method.toUpperCase(), url, timestamp, sortedHeaders].join('\n');
}
function generateHmacSignature(secret, method, url, headers, selectedHeaders, timestamp) {
const canonical = buildCanonicalString(method, url, headers, selectedHeaders, timestamp);
return crypto.createHmac('sha256', secret).update(canonical).digest('hex');
}
module.exports = { generateHmacSignature, buildCanonicalString };
In your FeathersJS hook, compute the signature and attach it to the request context. Then use it as part of the cache key when integrating with a caching layer.
// hooks/hmac-sign.js
const { generateHmacSignature, buildCanonicalString } = require('../utils/hmac');
module.exports = function hmacSignHook(options = {}) {
return async context => {
const { method, url, headers } = context;
const timestamp = Date.now().toString();
const selectedHeaders = ['x-api-key', 'content-type'];
const secret = process.env.HMAC_SECRET;
if (!secret) {
throw new Error('HMAC_SECRET must be set');
}
const signature = generateHmacSignature(secret, method, url, headers, selectedHeaders, timestamp);
const canonical = buildCanonicalString(method, url, headers, selectedHeaders, timestamp);
// Attach to context for later use in cache key or validation
context.meta = context.meta || {};
context.meta.hmac = { signature, canonical, timestamp };
return context;
};
};
When caching responses, incorporate the canonical string or its hash into the cache key to ensure uniqueness per signed request scope.
// hooks/cache-with-hmac.js
const crypto = require('node:crypto');
module.exports = function cacheWithHmacHook(options = {}) {
const cache = new Map(); // replace with your cache provider in production
return async context => {
const { path } = context;
const hmacInfo = context.meta?.hmac;
if (!hmacInfo) {
throw new Error('Hmac signature must be computed before cache hook');
}
// Use a cache key that includes the canonical Hmac scope
const cacheKey = crypto.createHash('sha256').update(`${path}|${hmacInfo.canonical}`).digest('hex');
if (cache.has(cacheKey)) {
context.result = cache.get(cacheKey);
return context;
}
// Proceed with the service call; after response, store in cache
const result = await context.app.service(path).find(context.params);
cache.set(cacheKey, result);
context.result = result;
return context;
};
};
For user-specific data, scope the cache key to the user identity present in the canonical string or in a user identifier derived from validated claims. Avoid including sensitive fields in cache keys, but ensure that any variation that affects authorization or data content changes the key.
// hooks/user-scoped-cache.js
module.exports = function userScopedCacheHook(options = {}) {
const cache = new Map();
return async context => {
const user = context.params?.user; // assume user object set by auth hook
if (!user || !user.id) {
throw new Error('User identity required for scoped caching');
}
const hmacInfo = context.meta?.hmac;
const cacheKey = `${user.id}:${hmacInfo ? crypto.createHash('sha256').update(hmacInfo.canonical).digest('hex') : 'nocache'}`;
if (cache.has(cacheKey)) {
context.result = cache.get(cacheKey);
return context;
}
const result = await context.app.service(context.path).find(context.params);
cache.set(cacheKey, result);
context.result = result;
return context;
};
};
These hooks ensure that responses are cached per canonical request context, preventing cross-user cache poisoning and ensuring that Hmac-protected endpoints remain consistent with their authorization model. Combine these practices with input validation and strict header selection to reduce the risk of cache-based information leakage.