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.
- 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.
- Do not downgrade without rebuilding. When translating to HTTP/1.1, validate every name and value before writing it.
- 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
- OWASP Application Security Verification Standard, requirement V4.2.4 (level 3)
- RFC 9113, HTTP/2, field validity