Cache Poisoning in Feathersjs with Dynamodb
Cache Poisoning in Feathersjs with Dynamodb — how this specific combination creates or exposes the vulnerability
Cache poisoning in a Feathersjs service backed by DynamoDB occurs when an attacker causes cached responses to store attacker-controlled data or to be shared across users or contexts where they should not be. Because Feathersjs can expose multiple REST and WebSocket endpoints that map to the same DynamoDB table or index, cache keys that include only user-supplied input or incomplete context can lead to one user seeing another user’s data or metadata.
DynamoDB does not provide server-side cache isolation by default; any caching layer introduced in Feathersjs (for example an in-memory LRU cache, a reverse proxy cache, or an application-level map) must use cache keys that incorporate tenant, user, and authorization context. If the key omits the authenticated subject or includes only non-authoritative parameters, an authenticated request for /users?role=admin might be cached and later served to a different user who does not have admin privileges, effectively leaking data without an access-control enforcement gap.
Feathersjs hooks can transform requests and responses before they reach DynamoDB. A hook that normalizes query parameters for caching but fails to bind the result to the requester’s identity enables horizontal privilege escalation via cache poisoning. For example, a cached response that includes sensitive PII or internal status fields can be returned to unrelated clients if the cache key does not incorporate user ID or tenant ID. Attackers may also exploit inconsistent TTLs or cache invalidation logic to keep poisoned entries alive across deployments.
SSRF and input validation issues can compound cache poisoning in this stack. If an attacker can inject a hostname or path into a query parameter that is used to build a cache key, they may force the service to cache responses from internal endpoints and later replay them to other users. DynamoDB attribute values that flow into cache keys without strict allowlists or escaping may reflect attacker-controlled content, leading to key collision or injection of malicious objects into cached structures.
Because middleBrick tests unauthenticated and authenticated attack surfaces in parallel, it can surface cache poisoning risks by comparing responses across users and detecting data leakage across authorization boundaries. The LLM/AI Security checks further probe whether cached error messages or data can be coaxed into revealing system prompts or PII, which would indicate a more severe exposure when combined with weak cache isolation.
Dynamodb-Specific Remediation in Feathersjs — concrete code fixes
Remediation centers on ensuring cache keys incorporate authorization context and that responses are not cached across users. In Feathersjs, this is achieved through careful hook design and explicit cache-control headers, while DynamoDB usage remains limited to authorized, parameterized queries.
1. Include user and tenant context in cache keys
When implementing application-level caching around DynamoDB calls, embed the user ID and any tenant or scope identifier into the cache key. Avoid using only query parameters or path segments.
// Example: cache key builder in a Feathers before hook
function cacheKeyFromContext(context) {
const { user, params } = context;
if (!user || !user._id) {
throw new Error('Cannot build cache key without authenticated user');
}
// Include tenant when applicable
const tenantId = params.headers['x-tenant-id'] || 'default';
return `feathers:dynamodb:${tenantId}:users:${user._id}:${JSON.stringify(params.query)}`;
}
2. Parameterized DynamoDB queries with strict attribute projection
Always use parameterized commands and project only required attributes to reduce the impact of accidental data exposure via caching. Do not rely on client-supplied field lists.
// Example: safe DynamoDB get in a Feathers service
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
async function getUserById(userId) {
const params = {
TableName: process.env.DYNAMODB_USERS,
Key: {
userId: userId
},
// Explicitly project safe, public attributes only
ProjectionExpression: 'userId, username, email, avatarUrl, createdAt'
};
const { Item } = await dynamodb.get(params).promise();
if (!Item) {
const error = new Error('Not found');
error.code = 404;
throw error;
}
return Item;
}
3. Hook-level cache control and Vary headers
Ensure HTTP caches and any intermediate caches differentiate responses by user and scope. Set Vary and Cache-Control appropriately within Feathers hooks to prevent shared caching of user-specific responses.
// Feathers hook to set safe caching headers
function setCacheHeaders(context) {
const { user, result } = context;
if (user && result && result.data) {
// Example for an HTTP response cache; adjust per transport
context.result.headers = {
...context.result.headers,
'Cache-Control': `private, max-age=300, s-maxage=0`, // private to the user
'Vary': 'Authorization, X-Tenant-ID'
};
}
return context;
}
4. Avoid caching sensitive or mutable fields from DynamoDB
Do not cache items that contain secrets, tokens, or fields that change frequently. If you must cache, implement versioned keys or short TTLs and explicitly invalidate on write operations.
// Invalidate or refresh cache on user update
app.service('users').hooks({
after: {
async update(id, data, params) {
const key = cacheKeyFromContext({
user: params.user,
params: { headers: params.headers, query: { userId: id } }
});
// Assuming a cache.delete(key) operation is available
if (typeof cache !== 'undefined' && cache.del) {
await cache.del(key);
}
return data;
}
}
});
5. Validate and sanitize inputs that influence caching
Use allowlists for any query parameters that affect cache key construction or DynamoDB queries. Reject or normalize unexpected parameters to reduce key collision and injection risk.
// Basic validation example in a before hook
function validateQueryForCache(context) {
const allowed = new Set(['sort', 'limit', 'role', 'fields']);
const query = context.params.query || {};
for (const key of Object.keys(query)) {
if (!allowed.has(key)) {
throw new Error(`Query parameter not allowed: ${key}`);
}
}
return context;
}