HTTP cache poisoning: the unkeyed header problem in AI-generated proxies
Par Seaworthy · 7 juillet 2026
Photo by Taylor Vick on Unsplash
What "unkeyed" actually means
A cache key is the set of request attributes a cache uses to decide whether a stored response can be served. Typically: method, host, path, maybe query string. If a request header is not part of the cache key, the cache ignores its value entirely. You get the same stored response regardless of what you send in that header. That's an unkeyed header.
X-Forwarded-Host, X-Original-URL, and X-Rewrite-URL are almost never included in a cache key. They're proxy housekeeping headers, not content differentiators. But if the application reads one of these headers and uses the value in the response, you have a problem: the cache will serve that poisoned response to every subsequent visitor.
How AI-generated proxies get this wrong
Ask an LLM to write an Nginx reverse proxy config or an Express middleware that "passes through the original host", and you'll often get something like this:
server {
listen 80;
location / {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $http_host;
}
}
Then on the backend, a well-meaning Express app reads that header to build absolute URLs for redirects:
app.use((req, res, next) => {
req.baseUrl = `https://${req.headers['x-forwarded-host'] || req.headers.host}`;
next();
});
app.get('/login', (req, res) => {
res.redirect(301, `${req.baseUrl}/dashboard`);
});
The redirect uses whatever X-Forwarded-Host says. If there's a caching layer in front (Nginx proxy_cache, Varnish, Cloudflare, a CDN), and that layer doesn't vary on X-Forwarded-Host, the first response gets stored. Any attacker can do this:
curl -H "X-Forwarded-Host: attacker.com" https://example.com/login
The server responds with Location: https://attacker.com/dashboard. If that 301 gets cached, the next thousand users who visit /login get redirected to attacker.com. The attacker sets up a credential-harvesting page there and starts collecting passwords from legitimate traffic.
Why AI tools produce this
The model has seen countless tutorials showing proxy_set_header X-Forwarded-Host $http_host. Passing through the original host is a reasonable pattern in many contexts. The model doesn't know your deployment has a caching layer in front. It doesn't know the backend reflects that header into cacheable responses. It writes code that looks correct in isolation and is subtly wrong in context.
This isn't a failure of the AI. It's a failure of the security reasoning that should happen after the code is generated.
Web cache deception, the related variant
Cache poisoning and web cache deception are often confused. Poisoning is about storing a bad response so other users receive it. Deception is about tricking the cache into storing a private response (your profile page, your account settings) under a URL that other users can request. Different mechanism, same root cause: a mismatch between what the cache considers a valid storage key and what the application actually serves.
If you see an AI-generated proxy that strips file extensions from requests before forwarding them, you may have a cache deception setup. /api/user/profile.css gets forwarded as /api/user/profile, the cache stores it because .css looks static, and the next visitor gets your session data.
The fix
Strip unkeyed headers at the edge before they reach the app. The simplest Nginx fix: don't forward X-Forwarded-Host at all, or explicitly clear it:
proxy_set_header X-Forwarded-Host "";
If the backend doesn't need it, remove it. If it does need it, make sure the backend never uses it in responses that flow through the cache.
The other option is to vary the cache on headers the response depends on. If you genuinely need the host in the response, tell the cache:
Vary: X-Forwarded-Host
This forces the cache to store separate entries per header value. Not all caches handle Vary correctly on custom headers, and cache efficiency drops significantly. Stripping the header is usually the cleaner choice.
At the application level, never trust X-Forwarded-Host unconditionally. Validate it against an allowlist of known hosts before using it in a response:
const ALLOWED_HOSTS = new Set(['example.com', 'www.example.com']);
app.use((req, res, next) => {
const forwarded = req.headers['x-forwarded-host'];
const host = (forwarded && ALLOWED_HOSTS.has(forwarded))
? forwarded
: req.headers.host;
req.baseUrl = `https://${host}`;
next();
});
The check you probably skipped
Fixing this after the fact is tedious. You need to audit every proxy config, every middleware that touches Host or X-Forwarded-Host, and every caching layer in the stack. Then check whether those responses include cache-control headers that would have prevented storage in the first place.
The pattern is findable statically. It's a specific combination of proxy header forwarding and response generation that uses those headers without validation. Seaworthy flags this pattern as part of its configuration checks.
Cet article a été généré par IA et résume des sources publiquement disponibles.
Seaworthy analyse votre dépôt pour détecter les problèmes abordés dans des articles comme celui-ci.
Failles de sécurité, secrets exposés et mauvaises configurations détectés avant le déploiement. Gratuit, aucun compte requis.