Cache Poisoning in Feathersjs with Basic Auth
Cache Poisoning in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker causes a shared cache to store malicious responses that are then served to other users. In Feathersjs applications that use Basic Auth over unencrypted channels or leak credentials via query strings, this risk is amplified because authentication data can become part of the cache key or be reflected in cached error responses.
Feathersjs services often rely on REST transports and may expose endpoints that accept user-controlled query parameters. When Basic Auth credentials are passed via the Authorization header, some proxy or CDN caches can incorrectly normalize requests by including headers in cache keys or by mishandling Authorization, leading to one user’s authenticated response being cached and served to another. Additionally, if Feathersjs services return detailed errors when authentication credentials are invalid, those error pages may be cached and returned to subsequent users, potentially exposing stack traces or internal paths.
Consider a Feathersjs service with an endpoint /api/profile that uses Basic Auth. If a caching layer uses the full request URL—including query parameters—and the client sends credentials via the header but the server also reflects the username in the response body, an attacker could craft URLs with different usernames in query strings to probe whether responses are being cached. In misconfigured setups, an attacker might also manipulate Authorization header values to trigger 401 responses that get cached, resulting in a scenario where legitimate users receive cached 401 pages instead of valid content.
The combination of Basic Auth and caching is particularly sensitive because the Authorization header should prevent caching per standard HTTP semantics; however, intermediaries that do not strip or properly handle Authorization can break this expectation. For example, a shared cache that does not differentiate by headers may store a response from one authenticated user and serve it to another, effectively leaking one user’s data to another. This violates the principle that authenticated responses must not be shared across users.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
To mitigate cache poisoning when using Basic Auth in Feathersjs, ensure that authenticated responses are never cached by intermediaries and that request normalization excludes sensitive headers. The following practices and code examples help secure Feathersjs services.
1. Configure Feathersjs service to set no-cache headers for authenticated responses
Use a hook to add HTTP headers that prevent caching when authentication is present. This ensures caches do not store responses tied to a specific credential.
// src/hooks/no-cache-auth.js
module.exports = function noCacheAuthHook(options = {}) {
return async context => {
if (context.params && context.params.headers && context.params.headers.authorization) {
// Prevent caching of authenticated responses
context.result = context.result || {};
// If using a REST transport, set headers via context
if (context.app && typeof context.app.set === 'function') {
// For Feathers REST, headers are typically set in the transport layer.
// This example assumes you have access to the response object.
// A more reliable approach is to set headers in the after hook.
}
}
return context;
};
};
// In your service file
const noCacheAuth = require('./hooks/no-cache-auth');
app.use('/api/profile', service());
app.service('api/profile').hooks({
after: [noCacheAuth()]
});
2. Use a dedicated REST hook to set Cache-Control and Vary headers
Feathersjs allows you to set response headers in an after hook. This example sets Cache-Control: no-store when Basic Auth is used, preventing storage of sensitive responses.
// src/hooks/cache-control-auth.js
module.exports = function cacheControlAuthHook(options = {}) {
return async context => {
const { headers } = context.params || {};
if (headers && headers.authorization && /^Basic\s+/i.test(headers.authorization)) {
// Ensure response headers prevent caching
context.result = context.result || {};
// Set headers appropriately for your transport; this example targets REST
if (context.app && context.app.set) {
// Typically, you set headers via the transport. For REST:
const res = context.app.get('server')._events; // Not recommended; see note below
}
}
return context;
};
};
// Better: Use the REST transport's after handler directly if available
// Many deployments use an after hook that has access to the HTTP response object.
// Example for feathers-rest:
const feathers = require('@feathersjs/feathers');
const rest = require('@feathersjs/rest');
const app = feathers().configure(rest());
app.use('/secure', service());
app.service('secure').hooks({
after: [context => {
if (context.params && context.params.headers && /Basic\s+\S+/.test(context.params.headers.authorization || '')) {
// Set no-store header
context.params.res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private');
context.params.res.setHeader('Vary', 'Authorization');
}
return context;
}]
});
3. Avoid exposing usernames in URLs or error messages
Ensure that error responses do not include authentication details. Standardize error messages to avoid leaking whether a username exists. Also, avoid having the username appear in URLs query strings when using Basic Auth.
// src/hooks/sanitize-errors.js
module.exports = function sanitizeErrorsHook(options = {}) {
return async context => {
if (context.error) {
// Replace detailed auth errors with generic messages
if (context.error.message && /authorization|auth|basic/i.test(context.error.message)) {
context.error.message = 'Unauthorized';
context.error.statusCode = 401;
}
}
return context;
};
};
app.service('api/profile').hooks({
error: [sanitizeErrorsHook()]
});
4. Enforce HTTPS and remove credentials from URLs
Always use HTTPS to prevent credentials from being intercepted. Configure your server to redirect HTTP to HTTPS and ensure that Basic Auth credentials are only sent over encrypted connections. Do not include credentials in URLs (e.g., https://user:[email protected]/api), as they may be logged in server logs and cache histories.
// Example server redirect (conceptual, depends on your server setup)
// In production, use your web server (e.g., Nginx, Caddy) to enforce HTTPS.
// MiddleBrick scans can verify that your API enforces HTTPS and does not accept credentials via query strings.