Safeguard
Application Security•

CWE-113 HTTP Response Splitting: How It Happens and Fixes

CR and LF in an HTTP header let attackers write their own headers and bodies. CWE-113 explained, with MITRE's CVE examples and a fix.

Safeguard Security Team
5 min read

CWE-113, Improper Neutralization of CRLF Sequences in HTTP Headers (HTTP Request/Response Splitting), applies when a product receives data from an HTTP agent or component and does not neutralize carriage return (CR) and line feed (LF) characters before including that data in outgoing HTTP headers.

The "agent" can be a web server, load balancer, reverse proxy, caching proxy, application firewall or browser. If your application copies a value into a header, such as a redirect target, a cookie value or a custom response header, you are in scope. It is the HTTP specific sibling of CRLF injection, CWE-93.

How it happens

HTTP separates headers with CR LF, and ends the header block with an empty line. MITRE explains the consequence: including unexpected data in a header lets an attacker specify the entirety of the HTTP message that the client agent or back-end agent will read, whether it is a request or a response. When unexpected CR and LF characters arrive, the output stream may be interpreted as split into two messages.

# Vulnerable: a redirect target taken straight from a parameter
from flask import Flask, request, make_response
app = Flask(__name__)

@app.route("/go")
def go():
    resp = make_response("", 302)
    resp.headers["Location"] = request.args["next"]   # raw value into a header
    return resp

# next = "/home\r\nSet-Cookie: session=attacker-value"

A modern framework may refuse a header value that contains a newline, and that is a good reason to keep frameworks current. But hand written servers, proxies and older libraries have not always done so, and MITRE's observed examples show it:

  • CVE-2020-15811: a proxy used a substring search instead of parsing the Transfer-Encoding header, allowing request splitting and cache poisoning.
  • CVE-2021-41084: a Scala based HTTP interface allowed request and response splitting through header names, header values, status reasons and URIs.
  • CVE-2018-12116: a JavaScript based framework allowed request splitting through the path option of an HTTP request.
  • CVE-2004-2146: an application accepted CRLF in an object ID, allowing HTTP response splitting.

Two of these are in components, not application code. Header splitting is as likely to live in the library or proxy you depend on as in your handlers.

What an attacker gains

MITRE lists integrity and access control impact: modifying application data and gaining privileges or assuming identity. CR and LF in a header can give attackers control of the remaining headers and the body of the message the application intends to send or receive, and can let them create additional messages entirely. With a shared cache in the path, a forged response can be stored and served to other users.

How to find it

MITRE notes that static analysis can find some instances. Search for places where request data flows into setHeader, addHeader, redirect helpers and cookie setters. Test by sending parameters that contain encoded CR LF, as %0d%0a, and look at the raw response for an extra header or a second response.

A dynamic scan finds this from the outside, and shows what a real attacker sees.

Click through it at your own pace, or open it full screen.

For the library side, the free in-browser dependency scan at gold.safeguard.sh/scan checks up to 100 dependencies with no login, and your file stays on your machine.

How to fix it

MITRE's mitigations:

  1. Construct HTTP headers very carefully, avoiding the use of non-validated input data.
  2. Validate input with an accept known good strategy. Assume all input is malicious, accept only what strictly conforms to your specification, and reject or transform the rest. Do not rely only on denylists.
  3. Specify an output encoding that the downstream component can handle, such as UTF-8. If the encoding is unspecified, a downstream component may guess a different one, and the mismatch can turn bytes that were harmless into special characters.
  4. Decode and canonicalize before you validate, and make sure nothing is decoded twice, so dangerous input cannot be introduced after the check.

For a redirect, the strongest design is not to pass a URL at all.

ALLOWED = {"home": "/home", "account": "/account"}

@app.route("/go")
def go():
    target = ALLOWED.get(request.args.get("next"))
    if target is None:
        return "bad request", 400
    resp = make_response("", 302)
    resp.headers["Location"] = target
    return resp

Where a value really must come from the user, reject any string containing CR or LF before it goes into a header, and keep your web framework and proxies up to date.

Quick check

Why can response splitting poison a cache?

The attacker causes a second, forged response to appear on the connection. A shared cache can store it and serve it to other users, as in the cache poisoning described in CVE-2020-15811.

Is it enough to fix my own code?

Not always. MITRE's examples include a proxy, a Scala HTTP interface and a JavaScript framework. Library and proxy versions matter too.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.