Reject control characters at the boundary of HTTP method names (#16723) #16933
Open
chrisvest wants to merge 2 commits into
Open
Reject control characters at the boundary of HTTP method names (#16723) #16933chrisvest wants to merge 2 commits into
chrisvest wants to merge 2 commits into
Conversation
…#16723) ## Problem `HttpMethod`'s constructor accepts a wire-level method name such as `\x00GET\x00` and silently treats it as `GET`. Because the method name is later compared against expected values (`HttpMethod.GET`, etc.), this masks the difference between a clean `GET` and a control-byte-padded one — a known HTTP request-smuggling vector when Netty sits behind a proxy or in front of a backend that interprets the bytes differently. A reproducer is in netty#15047: ``` printf '\x00GET\x00 / HTTP/1.1\r\n\r\n' | nc localhost 80 # → request is decoded as method=GET, isSuccess=true ``` ## Root Cause `HttpMethod(String)` runs `checkNonEmptyAfterTrim(name, …)` before validating the name as an HTTP token. `String.trim()` strips every character with code point ≤ 0x20, which includes `NUL`, `CR`, `LF`, `VT`, `FF`, and the rest of the C0 range. After the trim the surviving string is a clean `"GET"`, which passes `HttpHeaderValidationUtil.validateToken` even though the wire bytes contained a non-token character at the boundary. ## Fix In `codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java`: - Replace the `String.trim()`-based pre-pass with an explicit loop that only skips the single space (`0x20`) and horizontal tab (`0x09`) characters at the start and end. - Throw `IllegalArgumentException("name cannot be empty")` if the result is empty. - Run the existing `HttpHeaderValidationUtil.validateToken` facade against the resulting substring, so any non-token character — including a `NUL` left at the boundary — is reported via `"Illegal character in HTTP Method: 0x…"`. The HTTP request decoder already wraps `createMessage` exceptions into a decoder failure on the resulting `HttpRequest`, so the upstream effect is that `\x00GET\x00 …` produces an `HttpRequest` with `decoderResult().isSuccess() == false` instead of a phantom `GET`. ## Tests Added New `codec-http/src/test/java/io/netty/handler/codec/http/HttpMethodTest.java` (17 tests). NUL bytes are constructed via `String.valueOf((char) 0x00)` so the source file stays text-only. | Change point | Test | |--------------|------| | Cached lookup of standard methods unchanged | `valueOfReturnsCachedInstanceForKnownMethods` | | Custom method names still accepted | `constructorAcceptsCustomMethodName` | | `SP` trim still works (regression) | `constructorTrimsLeadingAndTrailingSpaces` | | `HT` trim still works (regression) | `constructorTrimsLeadingAndTrailingTabs` | | Reject NUL at start/end/both/embedded | `constructorRejectsLeadingNul`, `constructorRejectsTrailingNul`, `constructorRejectsLeadingAndTrailingNul`, `constructorRejectsEmbeddedNul` | | Reject other C0 control chars previously stripped by `trim()` | `constructorRejectsCarriageReturn`, `constructorRejectsLineFeed`, `constructorRejectsVerticalTab`, `constructorRejectsFormFeed` | | Reject embedded space (still a non-token char per RFC 7230) | `constructorRejectsEmbeddedSpace` | | Reject empty / blank-only names | `constructorRejectsEmptyString`, `constructorRejectsBlankString` | | End-to-end: decoder fails on NUL-padded method | `requestDecoderRejectsNulPaddedMethod` | | End-to-end regression: clean `GET` still parses | `requestDecoderAcceptsCleanMethod` | `mvn -pl codec-http test` runs 7834 tests with 0 failures locally. ## Impact - API: `HttpMethod`'s public constructor becomes stricter — inputs containing characters that were previously silently stripped (anything below `0x20` other than `SP` and `HT`) now throw `IllegalArgumentException`. Method names that already conform to RFC 7230's `token` rule are unaffected, and lenient `SP`/`HT` padding is still tolerated for backward compatibility. - Wire effect: A request whose method on the wire contains `NUL`, `CR`, `LF`, `VT`, `FF`, or other control bytes now produces a failed `HttpRequest` (`decoderResult().isSuccess() == false`) instead of being silently normalised — the existing `HttpRequestDecoder` failure plumbing handles the rest. - No changes outside `HttpMethod` and the new test class. Fixes netty#15047 (cherry picked from commit 6ad888e)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
HttpMethod's constructor accepts a wire-level method name such as\x00GET\x00and silently treats it asGET. Because the method nameis later compared against expected values (
HttpMethod.GET, etc.), thismasks the difference between a clean
GETand a control-byte-padded one— a known HTTP request-smuggling vector when Netty sits behind a proxy
or in front of a backend that interprets the bytes differently.
A reproducer is in #15047:
Root Cause
HttpMethod(String)runscheckNonEmptyAfterTrim(name, …)beforevalidating the name as an HTTP token.
String.trim()strips everycharacter with code point ≤ 0x20, which includes
NUL,CR,LF,VT,FF, and the rest of the C0 range. After the trim the survivingstring is a clean
"GET", which passesHttpHeaderValidationUtil.validateTokeneven though the wire bytescontained a non-token character at the boundary.
Fix
In
codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java:String.trim()-based pre-pass with an explicit loop thatonly skips the single space (
0x20) and horizontal tab (0x09)characters at the start and end.
IllegalArgumentException("name cannot be empty")if the resultis empty.
HttpHeaderValidationUtil.validateTokenfacadeagainst the resulting substring, so any non-token character — including
a
NULleft at the boundary — is reported via"Illegal character in HTTP Method: 0x…".The HTTP request decoder already wraps
createMessageexceptions into adecoder failure on the resulting
HttpRequest, so the upstream effectis that
\x00GET\x00 …produces anHttpRequestwithdecoderResult().isSuccess() == falseinstead of a phantomGET.Tests Added
New
codec-http/src/test/java/io/netty/handler/codec/http/HttpMethodTest.java(17 tests). NUL bytes are constructed via
String.valueOf((char) 0x00)so the source file stays text-only.
valueOfReturnsCachedInstanceForKnownMethodsconstructorAcceptsCustomMethodNameSPtrim still works (regression)constructorTrimsLeadingAndTrailingSpacesHTtrim still works (regression)constructorTrimsLeadingAndTrailingTabsconstructorRejectsLeadingNul,constructorRejectsTrailingNul,constructorRejectsLeadingAndTrailingNul,constructorRejectsEmbeddedNultrim()constructorRejectsCarriageReturn,constructorRejectsLineFeed,constructorRejectsVerticalTab,constructorRejectsFormFeedconstructorRejectsEmbeddedSpaceconstructorRejectsEmptyString,constructorRejectsBlankStringrequestDecoderRejectsNulPaddedMethodGETstill parsesrequestDecoderAcceptsCleanMethodmvn -pl codec-http testruns 7834 tests with 0 failures locally.Impact
HttpMethod's public constructor becomes stricter — inputscontaining characters that were previously silently stripped (anything
below
0x20other thanSPandHT) now throwIllegalArgumentException. Method names that already conform to RFC7230's
tokenrule are unaffected, and lenientSP/HTpadding isstill tolerated for backward compatibility.
NUL,CR,LF,VT,FF, or other control bytes now produces a failedHttpRequest(decoderResult().isSuccess() == false) instead of beingsilently normalised — the existing
HttpRequestDecoderfailure plumbinghandles the rest.
HttpMethodand the new test class.Fixes #15047
(cherry picked from commit 6ad888e)