---
title: "Keep unkeyed headers from changing cached · Agent Wranglers"
url: https://agentwranglers.se/playbook/adversarial-testing/requests-and-files/web-cache-poisoning
updated: 2026-10-21
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Keep unkeyed headers from changing cached pages

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Requests and files](https://agentwranglers.se/playbook/adversarial-testing/requests-and-files.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers). Updated 2026-10-21.

- ID: SEC-55
- Level: Recommended
- Chapter: [Requests and files](https://agentwranglers.se/playbook/adversarial-testing/requests-and-files)
- Rule: 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: 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](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/password-reset) 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](https://agentwranglers.se/playbook/adversarial-testing/requests-and-files/private-responses-uncached).
- 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`.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
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

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

## Sources

- [RFC 9111 section 4.1 Calculating Cache Keys with Vary](https://www.rfc-editor.org/rfc/rfc9111#section-4.1)

## Related rules

- [Make password resets safe](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/password-reset) in Security testing
- [Keep private pages out of shared caches](https://agentwranglers.se/playbook/adversarial-testing/requests-and-files/private-responses-uncached) in Security testing

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/adversarial-testing/requests-and-files/web-cache-poisoning">Agent Wranglers security testing playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Make every server agree where a request ends](https://agentwranglers.se/playbook/adversarial-testing/requests-and-files/request-smuggling)
- Next: [Check the sender of every window message](https://agentwranglers.se/playbook/adversarial-testing/requests-and-files/postmessage-origin)
- [Requests and files](https://agentwranglers.se/playbook/adversarial-testing/requests-and-files)
