Agent Wranglers

APS-47Recommended

Keep unkeyed headers from changing cached pages

Let a cached reply depend only on the parts of the request in the cache key. Never build a cached page from a header the cache ignores.

How to do it

Web cache poisoning sends one request with a header the cache ignores but the app reads, such as X-Forwarded-Host or X-Original-URL. The cache then serves that poisoned reply to every visitor. Build links from a configured site address, never from request headers, as make password resets safe also needs. Accept forwarding headers only from your own proxy, and strip them at the edge. When a reply must change with a header, name it in Vary or in the CDN's cache key. Private pages follow keep private pages out of shared caches.

Examples

JavaScript example

const SITE_URL = new URL(process.env.SITE_URL);
// Bad: new URL(`/shop/${slug}`, `https://${req.get('x-forwarded-host')}`)
const canonical = new URL(`/shop/${slug}`, SITE_URL);

Python example

SITE_URL = os.environ["SITE_URL"]
# Bad: f"https://{request.headers['X-Forwarded-Host']}/shop/{slug}"
canonical = urljoin(SITE_URL, f"/shop/{slug}")

How to check

On the test copy, request a cached page with X-Forwarded-Host: evil.example.net and a fresh query string, such as ?cb=PENTEST-31. Then request the same address without the header. Neither reply contains evil.example.net.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/application-security/requests-files-and-caches/web-cache-poisoning">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.