← 全部新闻
security4 分钟阅读

HTTP cache poisoning: the unkeyed header problem in AI-generated proxies

作者:Seaworthy · 2026年7月7日

Network cables plugged into server rack switch panels in a data center

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.

本文由 AI 生成,总结了公开可用的来源。

来自 Seaworthy 团队

Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。

安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。

$ npx seaworthycode