Safeguard
Application Security•

ASVS 5.0 V4.2.4: Reject CR and LF in HTTP/2 and 3 Headers

Header names and values in HTTP/2 and HTTP/3 must never contain CR or LF. Reject them on input to block header injection and smuggling after downgrade.

Safeguard Security Team
3 min read

ASVS 5.0 V4.2.4 (level 3) requires that your application accepts HTTP/2 and HTTP/3 requests only when the header fields and values contain no carriage return, line feed or CRLF sequences.

In HTTP/1.1 a CRLF ends a header line. In HTTP/2 headers are binary fields, so a CRLF inside a value is just two bytes and the protocol does not stop it. The danger arrives when something later turns that value back into text.

Why it matters

Suppose an edge speaks HTTP/2 to clients and HTTP/1.1 to the backend. A client sends a header value like a\r\nX-Admin: true. HTTP/2 carries it intact. On downgrade, the proxy writes it into an HTTP/1.1 stream and the backend sees a brand new header, X-Admin: true. That is header injection, and it can be extended to full request smuggling or response splitting. See CWE-113.

Your own code can recreate the problem too, when it copies a request header into a redirect, a log line, or an outbound call.

How to implement it

Layer the defense.

  1. Use servers that reject such fields at the protocol layer. The HTTP/2 spec says a message with them is malformed, and current servers return a stream error.
  2. Do not downgrade without rebuilding. When translating to HTTP/1.1, validate every name and value before writing it.
  3. Validate in your app when you copy header values anywhere.
function safeHeaderValue(v) {
  if (/[\r\n\0]/.test(v)) throw new Error('invalid header value');
  return v;
}

res.setHeader('X-Request-Origin', safeHeaderValue(req.headers['x-origin'] ?? ''));

Node already throws on invalid characters in setHeader, but custom writers, raw sockets and log formatters do not, so keep the check explicit where you build output by hand.

How to test it

Send an HTTP/2 request with a CR or LF inside a header value using a tool that does not sanitize, such as nghttp with a crafted value or a Python h2 script. The correct result is a stream reset or a 400, and nothing malformed should show up in the backend access log.

import h2.connection
# build headers with ('x-test', 'a\r\nX-Injected: 1') and send via a raw socket

Also test your own endpoints that echo or forward headers, and confirm they refuse values with %0d%0a once decoded.

Common mistakes

  • Trusting the HTTP/2 layer and then concatenating headers into text without checks.
  • Validating header names but not values.
  • Decoding percent encoding after validation, which brings a CRLF back.
  • Logging raw header values, which lets an attacker forge log lines.

Quick check

Why can a CRLF in an HTTP/2 header value be dangerous?

HTTP/2 carries it without complaint, but when a proxy downgrades to HTTP/1.1 the CRLF ends a header line and lets the attacker add new headers.

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.