REQUEST SMUGGLING, CRLF SPLITTING, CACHE POISONING

Three attacks that live between your reverse proxy and your app server. The AI generator doesn't think about them because the bug isn't in app code — it's in the disagreement between two HTTP parsers about where one request ends and the next begins.

The scenario referenced below runs on gapbench.vibe-eval.com — a public security benchmark we operate.

Bugs that live in disagreement

The three bugs in this article all share a structure: two pieces of HTTP-handling software disagree about something, and the attacker exploits the disagreement. Request smuggling lives in disagreement about where one request ends. CRLF splitting lives in disagreement about whether a header value can contain newlines. Cache poisoning lives in disagreement about which headers affect the response.

AI-generated app code rarely introduces these directly. What it does is fail to mitigate them at the app layer, and trust the proxy to do the right thing — which the proxy may or may not be configured to do.

Request smuggling

The classic CL.TE attack. The proxy reads Content-Length: 13. The app reads Transfer-Encoding: chunked. The body is crafted such that the proxy thinks one request ends after 13 bytes, and the app thinks the request goes longer. The “extra” bytes that the proxy sees as the next request, the app sees as a continuation of the first.

Smuggled requests bypass the proxy’s auth checks (because they were never seen as separate requests). They can also poison the next user’s connection — a common cloud setup is keep-alive between proxy and app, where the smuggled prefix attaches to the next legitimate user’s request.

The fix is at the proxy and app layer simultaneously:

  • Use a proxy that strips ambiguous headers (modern nginx, modern Cloudflare). Reject any request that has both Content-Length and Transfer-Encoding headers.
  • Use an app server with the latest body-parsing code. Express has had several smuggling-related advisories; keep dependencies current.
  • Disable HTTP/1.1 keep-alive between proxy and app, or at minimum use HTTP/2 between them — HTTP/2 doesn’t have the same parser-disagreement issue.

Live: https://gapbench.vibe-eval.com/site/request-smuggling/.

CRLF response splitting

app.get('/redirect', (req, res) => {
  res.setHeader('Location', req.query.url)
  res.status(302).end()
})

If the user supplies ?url=/dashboard%0d%0aSet-Cookie: session=hijacked, the response now has an injected Set-Cookie header. The browser respects it. The session is now whatever the attacker chose.

Modern frameworks neutralize this for known headers — res.redirect in Express filters CRLF. But:

  • Custom header construction (res.setHeader('Custom-Header', userInput)) doesn’t always.
  • Older frameworks or unusual middleware may pass user input through directly.
  • AI-generated code that builds headers manually, especially in Go or Python frameworks where the contract isn’t as clear, often misses the filter.

Fix: use the framework’s built-in redirect/cookie helpers, not raw setHeader. If you must build headers manually, validate or strip \r and \n from user input.

Live: https://gapbench.vibe-eval.com/site/crlf-response-splitting/.

Cache poisoning

A request hits your CDN. The CDN keys its cache on URL + a small set of headers (Vary). The request includes:

Host: your-site.com
X-Forwarded-Host: attacker.example

Your app reads X-Forwarded-Host to construct internal links (because some framework or middleware does so). The response body contains URLs that point at attacker.example. The CDN caches this response under the URL your-site.com/page because it doesn’t include X-Forwarded-Host in its Vary key. Every subsequent visitor to that URL gets the poisoned response with attacker URLs.

The same shape with other unkeyed headers: X-Original-URL, X-Forwarded-Server, X-Rewrite-URL. Each one a potential vector if the app reads it and the cache ignores it.

Fix:

  • Don’t let your app trust headers the CDN doesn’t key on. If X-Forwarded-Host matters, the CDN’s Vary must include it.
  • Better: don’t trust those headers at all. Pin your app’s idea of the host to a config value.
  • Disable caching for any path that varies based on headers your CDN can’t key. Authenticated paths usually shouldn’t be cached at the CDN at all.

Live: https://gapbench.vibe-eval.com/site/cache-poisoning/.

A specific incident — cache poisoning via X-Forwarded-Host

Anonymized. A SaaS used Cloudflare as the CDN and an Express app behind it. The Express app read X-Forwarded-Host to construct internal links — for emails, for OG tags, for “share this page” buttons. Cloudflare’s default cache key is URL + the headers in Vary. X-Forwarded-Host was not in Vary.

Attacker sent:

GET /article/popular-post HTTP/1.1
Host: legitimate-site.com
X-Forwarded-Host: attacker.example

Express built the rendered HTML with attacker.example in the canonical link, the OG image URL, and the share buttons. Cloudflare cached the response under legitimate-site.com/article/popular-post. Subsequent visitors to that URL got back the poisoned response — content served from the legitimate site, with attacker’s URLs in the OG tags. The attacker’s URL hosted a phishing clone.

Cache TTL was 5 minutes, so the poisoning was self-healing. But during that 5-minute window, a popular post got ~300 visitors all served the poisoned version, all with the attacker’s URLs in social shares. The team noticed because their analytics showed an unusual spike in outbound clicks to a domain they didn’t own.

The cleanup: stop reading X-Forwarded-Host (use Host plus an explicit allowlist), add X-Forwarded-Host to Cloudflare’s Vary header for any path that needed it, audit other unkeyed-header reads. The deeper fix was teaching the team that any header the CDN ignores is effectively user-controlled, and the app shouldn’t trust them.

A taxonomy of unkeyed headers

Beyond X-Forwarded-Host, the headers worth auditing:

  • X-Forwarded-Server — same shape, less common, equally dangerous if read.
  • X-Original-URL, X-Rewrite-URL — used by some frameworks for routing. Attacker-controllable in some configs.
  • X-Forwarded-For — used for client IP. Trusting it for rate-limiting or auth without a trust proxy config gives the attacker control of “their” IP.
  • X-HTTP-Method-Override — used by some frameworks to override the HTTP method via header. Lets attacker turn a GET into a POST/DELETE in some setups.
  • Host header itself — covered in host-header-injection for password-reset poisoning. Same root cause.

Anywhere you read these and the CDN/proxy doesn’t normalize them, you have a potential bug.

Smuggling — the modern variants

CL.TE and TE.CL are the classic. Modern proxies are mostly hardened against both. The variants that still land:

  • HTTP/1.1 vs HTTP/2 downgrade smuggling. Frontend speaks HTTP/2, backend speaks HTTP/1.1, the translation between them is occasionally lossy. James Kettle’s research has documented several variants over the past few years.
  • TE.TE smuggling. Both proxy and app see Transfer-Encoding, but they parse it differently (one treats Transfer-Encoding: chunked, X-Stuff as chunked, the other as unknown).
  • Header-name encoding. Proxy treats Transfer-Encoding as the header but the app treats transfer-encoding (lowercased) only — case sensitivity divergence.
  • Pseudo-header smuggling in HTTP/2. Specific to certain proxy implementations.

The detection landscape evolves with the published research. The mitigation is universal: keep proxies updated, keep app servers updated, prefer HTTP/2 end-to-end if your stack supports it.

Wrong fix vs right fix

// WRONG: trust X-Forwarded-Host blindly
const host = req.headers['x-forwarded-host'] || req.headers.host
const link = `https://${host}/share?id=${id}`
// WRONG: Set CDN to vary on X-Forwarded-Host
// This works but inflates cache key cardinality, bad for cache hit rate
// RIGHT: pin host to a config value
const TRUSTED_HOST = process.env.PUBLIC_HOST  // 'example.com'
const link = `https://${TRUSTED_HOST}/share?id=${id}`
// Doesn't matter what header arrives; the URL is always the trusted value

Cross-stack notes

  • Express: app.set('trust proxy', N) controls whether X-Forwarded-* headers are trusted. Default is false. AI-generated apps sometimes set trust proxy: true blindly, which trusts the headers from any upstream — bad if you’re behind multiple proxies of varying trust levels.
  • Next.js: Reads X-Forwarded-Host in some middleware patterns. Audit specifically.
  • Django: USE_X_FORWARDED_HOST = True is the unsafe pattern unless paired with ALLOWED_HOSTS strictly.
  • Rails: request.host reads X-Forwarded-Host if the proxy is trusted. Same caveat.
  • Go (net/http): No automatic trust; you have to manually parse the header. Less convenient, less risky.

How we detect

Request smuggling: we send carefully malformed CL.TE and TE.CL probes and observe the proxy/app pair’s behavior. The detection is one of the harder ones because false positives are easy and the actual exploit primitives differ across stacks.

CRLF: we send redirect endpoints and other header-emitting routes with CRLF payloads and observe whether the response headers reflect the injection.

Cache poisoning: we send the same URL twice with different unkeyed headers, observe whether the cached response carries content that depends on the header.

All three are runtime detections. Static scanners can’t do any of them — they need to observe the proxy + app pair’s behavior under hostile HTTP.

CWE / OWASP

  • CWE-444 — HTTP Request/Response Smuggling
  • CWE-93 — Improper Neutralization of CRLF Sequences (CRLF Injection)
  • CWE-349 — Acceptance of Extraneous Untrusted Data With Trusted Data
  • OWASP Top 10 — A03:2021 Injection, A05:2021 Security Misconfiguration

Reproduce it yourself

COMMON QUESTIONS

01
What is request smuggling?
When a request hits your reverse proxy and your app server, both have to agree on where one request ends. Two headers tell them: Content-Length and Transfer-Encoding. If the proxy uses one and the app uses the other, an attacker can craft a request where the proxy sees one request and the app sees two. The 'second' request is the smuggled one — it gets processed by the app as if it came from a different user, often inheriting connection state or auth context. CL.TE and TE.CL are the standard names for the two main flavors.
Q&A
02
What is CRLF response splitting?
If your app concatenates user input into a response header — typically a redirect, sometimes a Set-Cookie — and the user input contains carriage-return-line-feed sequences, the user can split the header into multiple headers, or split the response into headers and body. The result: attacker controls additional response headers, can inject Set-Cookie, can inject HTML body content. Modern frameworks usually neutralize this; AI-generated middleware sometimes reintroduces it.
Q&A
03
What is cache poisoning?
If your CDN or reverse proxy caches responses keyed only on the URL — not on every header that affects the response — an attacker can send a request that includes a header the cache ignores but the app uses. The app generates a poisoned response. The cache stores it under the URL key. Every subsequent request for that URL gets the poisoned response. X-Forwarded-Host, Host header rebinds, and similar 'unkeyed' headers are the classic vectors.
Q&A
04
Why do AI generators miss these?
Because the bug isn't in app code. It's in the configuration of the proxy, the version of the proxy, and the way the app's framework handles edge-case headers. The AI generator writes app code; it doesn't write or audit nginx.conf and Express's body-parsing options at the same level. The bugs persist because nobody looking at app code can see them.
Q&A
05
Where can I see this on a real URL?
https://gapbench.vibe-eval.com/site/request-smuggling/, https://gapbench.vibe-eval.com/site/crlf-response-splitting/, https://gapbench.vibe-eval.com/site/cache-poisoning/.
Q&A
06
What CWE does this map to?
CWE-444 (HTTP Request/Response Smuggling), CWE-93 (CRLF Injection), CWE-349 (Acceptance of Extraneous Untrusted Data With Trusted Data) for cache poisoning. OWASP A03:2021 (Injection), A05:2021 (Security Misconfiguration).
Q&A

PROBE YOUR PROXY LAYER

We send the malformed-but-RFC-bendy requests these attacks rely on and observe the proxy's behavior.

RUN THE SCAN