HTTP response splitting is a web application vulnerability that lets an attacker place carriage return and line feed characters into a response header, close the real header block early, and append a second HTTP response of their own design. The browser or the shared cache in front of your site treats that forged message as authentic output from your server. This guide walks through how the split happens at the byte level, which header sinks usually cause it, how to probe an endpoint safely, and why the durable fix belongs in your code. The mechanics are simpler than most engineers expect.

Key Takeaways

  • A CR and LF pair inside a response header value ends the header block, so whatever follows is parsed as a new message.
  • The weakness is catalogued as CWE-113 and maps to the injection category of the OWASP Top 10.
  • Location and Set-Cookie are the most common sinks because they routinely carry values taken from the request.
  • Splitting is only a delivery mechanism. Cache poisoning, cross-site scripting, page hijacking and cross-user defacement are the payoffs.
  • The fix is rejecting CR, LF and NUL at the exact point where the header value is written.
  • Most modern runtimes already reject these bytes, so live cases involve legacy stacks, hand-rolled header writers, or values decoded twice.
  • Edge filtering shortens exposure and buys time, but it never removes the vulnerable line of code.

How a Single CRLF Turns One Response Into Two

An HTTP/1.1 message separates its header block from its body with one blank line, written on the wire as %0d%0a%0d%0a. If an application copies a request value into a header without removing those bytes, the value itself terminates the header block. Everything the attacker appended after it is then read as a complete, independent response.

That is the whole trick. There is no memory corruption and no clever timing. The server writes exactly what it was told to write, and the parser downstream does exactly what the specification says it should do with a blank line. MITRE lists the weakness as CWE-113, improper neutralization of CRLF sequences in HTTP headers, and notes that horizontal tab and space characters can be abused in the same family of tricks.

It helps to separate this from its close relative. Response splitting corrupts what your server sends outward. HTTP request smuggling corrupts how two systems disagree about an inbound request. They share the vocabulary of desynchronised HTTP parsing, but the vulnerable line of code sits in very different places.

Aspect HTTP response splitting Request smuggling CRLF log injection
Injected into An outgoing response header value Framing headers on an inbound request A log line written by the application
Root cause Unvalidated CR or LF in a header the app writes Two parsers disagreeing on message length Unvalidated CR or LF in a logged value
Who is fooled The browser or the shared cache The back end behind the proxy Whoever reads or parses the log
Primary fix Reject CR and LF where the header is set Align parsers and reject ambiguous framing Escape control characters before logging

Knowing the shape of the attack is one thing. Finding the line that allows it is another, and it is rarely where teams look first.

Where Response Splitting Enters Your Code

Response splitting enters wherever request data reaches a response header without validation. That narrows the search dramatically, because most applications only write a handful of dynamic headers.

Redirects lead the list. A login flow that reads a returnUrl parameter and drops it into a Location header is the textbook case, and it is the exact pattern the Ratpack advisory described for versions up to 1.7.4. Cookie writers come next, since locale, tenant and tracking values so often arrive with the request. After those sit custom debugging headers that echo a request identifier, and Content-Disposition filenames built from uploaded file names.

Four cards showing Location, Set-Cookie, custom X- headers and Content-Disposition as common response header injection points

You might be thinking a modern framework already blocks this. Largely true: Java EE containers, ASP.NET and current Node.js releases reject raw CR and LF in header values. The residual risk sits in three places. Legacy runtimes that predate those checks. Custom code that assembles headers as strings. And values decoded twice, where %250d%250a becomes %0d%0a after the first pass and a real CRLF after the second. Treating it as one member of a wider injection attack family, rather than solved history, keeps it on the review checklist.

Grep for where your code writes a header, not where it reads a parameter. One helper that builds redirect targets by string concatenation deserves more attention than a hundred sanitised form fields.

Once a forged response is on the wire, what an attacker does with it depends entirely on who reads it next.

What Attackers Gain From a Forged Second Response

A split response is a delivery mechanism, not the goal. OWASP lists four outcomes that follow from it: cross-user defacement, cache poisoning, cross-site scripting and page hijacking.

 Flow diagram showing an injected second response leading to cache poisoning, cross-site scripting, page hijacking and cross-user defacement

Cache poisoning is the one that scales. If a shared cache stores the forged message, every later visitor whose request maps to the same cache key receives attacker content until that entry is purged. One request becomes a persistent defacement served from infrastructure the victim has every reason to trust. A CDN reverse proxy sitting in front of the origin makes the blast radius wider, not smaller, because it multiplies the audience for a single stored entry.

The cross-site scripting angle deserves its own note. Script delivered through a split response executes on the real origin, so same-origin protections work in the attacker’s favour rather than the defender’s. From there, stealing a session cookie and moving on to session hijacking is a short step. Page hijacking is the quieter variant: on a shared connection, the response meant for a victim is delivered to the attacker instead, headers and all.

Given that range of outcomes, confirming whether a given endpoint is actually vulnerable is worth doing carefully rather than assuming.

How to Test an Endpoint for Response Splitting

Test by sending an encoded CRLF through every parameter that could reach a header, then reading the raw response headers rather than the rendered page. The signal you are looking for is a header you did not expect, or a second status line inside what should be one message.

# Single encoding: closes the header block and starts a forged response

curl -sD - "https://target.example/redirect?next=/home%0d%0a%0d%0aHTTP/1.1%20200%20OK"

# Double encoding: survives one decoding pass in the framework

curl -sD - "https://target.example/redirect?next=/home%250d%250a%250d%250a"

# Bare LF only: some parsers accept it as a line terminator

curl -sD - "https://target.example/redirect?next=/home%0aSet-Cookie:%20probe=1"

Three results are worth separating. A 400 status means the runtime is validating, which is the healthy outcome. A 200 with the CRLF silently stripped is acceptable, though worth confirming as deliberate. A response carrying your injected header is a confirmed finding. Static analysis complements this well, since MITRE rates it as highly effective at connecting request sources to header sinks.

Run these probes against staging, never production. A successful split on a live shared cache is a customer-facing incident, not a test result.

A confirmed finding then raises the question every team asks next: where exactly should the fix go?

How to Prevent HTTP Response Splitting in Application Code

Prevent it by refusing to write CR, LF or NUL into a header value at the moment the header is set. That single rule closes the vulnerability class, and the protocol itself backs it.

“Field values containing CR, LF, or NUL characters are invalid and dangerous.”

In practice that becomes five habits, in rough order of how much risk each removes.

  1. Reject rather than strip. Silently removing a control character hides the bug, which is why the Ratpack maintainers chose a runtime exception. There is no safe way to escape a header terminator.
  2. Use the framework header API instead of building header strings by hand. The API is where the validation lives.
  3. Allowlist redirect targets. Compare the requested destination against a set of known paths rather than echoing whatever arrived.
  4. Decode once, then validate the decoded value. Double decoding is what turns a filtered payload back into a live one.
  5. Keep the runtime patched. Many response splitting fixes shipped in the HTTP layer of the platform rather than in application code.

What most teams miss is the ordering. Validation before the final decode is validation of the wrong string. Check the value in the exact form it will hold when it reaches the wire.

Why an Edge Layer Supports the Fix but Never Replaces It

An edge layer is a useful second line, and it is not the cure. Filtering catches the encodings an attacker tries first and gives detection signal while a code change moves through review, but the vulnerable line is still there when the filter is bypassed.

 Two panels comparing the primary application layer fix for response splitting against the supporting role of edge, proxy and WAF controls

The support is real, though. An advanced web application firewall can block requests carrying encoded CRLF sequences before they touch the origin, and it can normalise or reject malformed upstream responses so a forged message never reaches a cache. Well-tuned WAF rules covering single, double and mixed encodings of the same payload turn a silent vulnerability into an alert you can act on.

Use it as a shock absorber rather than a substitute. Filtering buys hours or days. The header writer decides whether the vulnerability exists at all.

Final Thought on HTTP Response Splitting

HTTP response splitting endures because it exploits correct behaviour rather than broken behaviour. Every parser in the chain is doing precisely what the specification asks. The only defect is a header value that was never checked, and the only reliable place to check it is the code that writes it.

Treat header values with the same seriousness you give database queries and rendered HTML. Audit your redirect and cookie writers, confirm decoding happens once, and verify your runtime rejects control characters rather than quietly removing them. Layer edge filtering on top for detection, and keep correctness where it belongs.

Common Questions About HTTP Response Splitting

Is HTTP response splitting still exploitable on a current framework?

Rarely through the framework itself, since current Java EE containers, ASP.NET and Node.js releases reject CR and LF in header values. It stays exploitable where an application writes headers as raw strings, where an unpatched legacy runtime is still in service, or where a value passes two decoding steps before it is written.

Does HTTP/2 or HTTP/3 remove the risk?

Those versions carry header fields in a binary framing layer rather than as CRLF-delimited text, which removes the classic splitting vector on that hop. The risk returns when a gateway translates traffic back to HTTP/1.1 for the origin, so mixed-protocol paths still need the same validation.

How is CRLF injection different from response splitting?

CRLF injection is the general weakness of unvalidated carriage return and line feed characters reaching any structured output, including log files. Response splitting is the case where that output is an HTTP response header, which is why CWE lists it under the broader CRLF injection entry.