Cache Poisoning in Strapi with Api Keys
Cache Poisoning in Strapi with Api Keys — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker manipulates cached responses so that subsequent users receive malicious or incorrect data. In Strapi, this risk is amplified when API keys are used for authorization but caching mechanisms incorrectly associate a public or shared cache key with user-specific or privileged data. Strapi’s default REST and GraphQL endpoints can expose this issue when responses are cached based solely on the request URL and query parameters while an API key is used for access control.
Consider a Strapi collection type Product with an endpoint /api/products that is protected by an API key header x-api-key. If a caching layer (for example, a CDN or reverse proxy) treats requests with different x-api-key values as the same cache key, it may serve data intended for one client to another. A public API key shared across multiple integrations could cause private product details or pricing to be returned to unintended consumers. Additionally, if Strapi’s locale or pagination parameters are not included in the cache key, an attacker who can control one parameter might poison the cache for other users.
Another scenario involves user-specific data exposed through Strapi’s dynamic zones or component fields. Suppose an endpoint /api/user-profile uses an API key for authorization but the cache key excludes user identifiers. A malicious user could induce the server to cache a response containing sensitive profile data under a public key, enabling other users to retrieve that data via the same cache entry. This intersects with authorization flaws because the cache operates outside Strapi’s policy enforcement, allowing access to data that authenticated and properly authorized requests would otherwise restrict.
The LLM/AI Security checks in middleBrick highlight risks where endpoints used by AI tooling expose sensitive or inconsistent data through caching. For example, if an unauthenticated LLM endpoint in Strapi proxies through caching, outputs could reveal system prompts or training data patterns. middleBrick tests for system prompt leakage and output scanning for PII or API keys, which are particularly relevant when API keys are mishandled in cache logic.
Because Strapi can serve both public and authenticated content from the same routes, careful cache segregation by header, cookie, and user context is essential. Without it, API keys alone do not prevent cache poisoning; they may even create a false sense of security if access control is assumed but caching bypasses it.
Api Keys-Specific Remediation in Strapi — concrete code fixes
To mitigate cache poisoning when using API keys in Strapi, ensure cache keys incorporate all dimensions that affect response uniqueness, including the API key value or a derived tenant identifier, locale, and pagination tokens. Avoid using a shared cache key when responses differ by authorization context.
Example 1: Configuring cache headers in Strapi controller to vary by API key.
// src/api/product/controllers/product.js
'use strict';
module.exports = {
async find(ctx) {
const { 'x-api-key': apiKey } = ctx.request.headers;
if (!apiKey) {
return ctx.unauthorized('API key is required');
}
// Include API key hash in cache key to isolate responses per client
const cacheKey = `products:${require('crypto').createHash('sha256').update(apiKey).digest('hex')}:${ctx.query.locale || 'en'}:${ctx.query._limit || 10}:${ctx.query._start || 0}`;
const cached = await caches.get(cacheKey);
if (cached) {
return cached;
}
const results = await strapi.entityService.findMany('api::product.product', {
filters: ctx.request.query,
});
const response = { data: results };
await caches.set(cacheKey, response, { ttl: 60 });
return response;
},
};
Example 2: Using Strapi’s HTTP cache plugins with Vary headers to prevent cross-client poisoning.
// src/admin/config/middlewares.js
module.exports = [
{
name: 'cache-control',
config: {
policy: 'public',
maxAge: 60,
varyBy: ['x-api-key', 'accept-language', 'authorization'],
},
},
];
Example 3: Securing GraphQL endpoints by including API key context in cache resolution.
// src/api/graphql/resolvers/product.js
const { getCacheKey } = require('../../utils/cache');
module.exports = {
Query: {
products: async (parent, args, context) => {
const { apiKey } = context.state.user; // validated earlier
const cacheKey = getCacheKey('products', { apiKey, locale: args.locale, filters: args.filters });
const cached = await context.cache.get(cacheKey);
if (cached) return cached;
const products = await context.db.query('Product', { where: args.filters });
await context.cache.set(cacheKey, products, { ttl: 30 });
return products;
},
},
};
These examples emphasize that API keys must be part of the cache key when responses are user- or client-specific. Additionally, validate and sanitize all inputs to prevent injection into cache logic, and ensure that cache invalidation accounts for changes in data permissions.