Cache poisoning happens when malicious headers trick your proxy. Learn how to secure your apps against X-Forwarded-Host header manipulation and proxy risks.
During a recent audit of a high-traffic microservices cluster, I noticed our CDN was occasionally serving up broken assets to users. After about two days of digging through Nginx access logs and tracing request flows, we realized the application was trusting the X-Forwarded-Host header to generate absolute URLs, which an attacker was manipulating to poison our shared cache.
At its core, cache poisoning occurs when an intermediary—like a CDN or a reverse proxy—caches a response that contains malicious or attacker-controlled content. When your application logic relies on headers like X-Forwarded-Host or X-Forwarded-Proto to construct canonical links, scripts, or redirects, you’re essentially handing the keys to the kingdom to anyone who can inject their own domain into that header.
If your backend generates a response based on an untrusted header, that response gets cached globally. Suddenly, every legitimate user hitting that edge node receives a page pointing to an attacker-controlled domain. If you haven't yet, you should review Preventing Host Header Injection in Node.js and PHP Apps to see how this pattern manifests in different runtimes.
We first tried solving this by simply stripping the header at the load balancer level. It seemed like a solid fix until we realized our internal billing microservice actually required the X-Forwarded-Host header to correctly route callback URLs for payment providers.
We had created a classic trade-off: block the header and break the business logic, or allow it and risk cache poisoning. We eventually moved to a strict allow-list approach, which is the only reliable way to handle header manipulation when your architecture depends on proxy-injected metadata.
The goal is to ensure your application never blindly trusts these headers. Instead of using the header value directly, validate it against a known, hardcoded list of authorized hostnames.
Don't rely on the application code to sanitize these values. Instead, enforce policy at the edge.
Flow diagram: Client Request → Edge Proxy; B -- Valid Host → Backend App; B -- Invalid Host → 403 Forbidden; Backend App → Cache Server; Cache Server → Client Response
In your Nginx configuration, you should explicitly define valid hosts. If a request comes in with a forged X-Forwarded-Host, drop it before it ever reaches your application logic.
NGINX# Example Nginx snippet to sanitize headers map $http_x_forwarded_host $valid_host { default 0; "myapp.com" 1; "api.myapp.com" 1; } server { if ($valid_host = 0) { return 403; } }
Even with edge-level validation, your application security needs to be resilient. Never trust the Host header or any X-Forwarded-* headers when generating URLs for emails, password resets, or canonical tags.
Always prefer relative URLs where possible. If you absolutely must use absolute URLs, pull the domain from a secure, environment-defined variable rather than the incoming request headers. This is a recurring theme in Business logic security: Preventing state manipulation in workflows, where trusting client-side input leads to state corruption.
Host header in the cache key. If it doesn't, a single poisoned response could impact users globally.Vary header: If you must serve content based on request headers, ensure your response includes Vary: X-Forwarded-Host. This forces the cache to store separate versions of the resource for each unique header value, limiting the blast radius of a poisoning attempt.X-Forwarded-Host values to verify that your staging environment returns 403s or ignores the header entirely.Q: Can I just ignore the X-Forwarded-Host header entirely? A: In an ideal world, yes. However, many legacy systems or complex proxy chains (like those used in Kubernetes ingress controllers) rely on these headers to maintain state. If you can't remove them, enforce strict validation.
Q: Does my CDN automatically handle this? A: Not necessarily. While most modern CDNs have features to prevent cache poisoning, they can't know which headers your application considers "sensitive." You must configure your CDN to ignore or sanitize these headers based on your specific requirements.
Q: What if I have multiple domains? A: Use an allow-list stored in a configuration file or a database. Never allow the header value to be reflected in your HTML response without validation against this list.
We’re still debating whether to migrate fully to a protocol that doesn't rely on these headers at all, but for now, the allow-list approach has stopped the bleeding. Cache poisoning is a persistent threat that evolves as our proxy layers get more complex. My advice? Treat every header coming from a proxy as "tainted" until proven otherwise. It’s better to spend an extra hour on configuration than to spend a week responding to an incident where your users are being redirected to a phishing site.
Master business logic security to prevent state manipulation in multi-step workflows. Learn how to enforce server-side validation and protect critical paths.
Read moreNode.js security relies on robust asynchronous error handling. Learn to prevent unhandled promise rejections and state corruption in your backend services.
Read moreMaster secure multi-part form data handling. Learn how to prevent file upload and injection vulnerabilities in Node.js and PHP using robust input validation.
Read more