Skip to content

Reject control characters at the boundary of HTTP method names (#16723) #16933

Open
chrisvest wants to merge 2 commits into
netty:4.1from
chrisvest:4.1-reject-control-characters
Open

Reject control characters at the boundary of HTTP method names (#16723) #16933
chrisvest wants to merge 2 commits into
netty:4.1from
chrisvest:4.1-reject-control-characters

Conversation

@chrisvest

Copy link
Copy Markdown
Member

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 #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 #15047

(cherry picked from commit 6ad888e)

daguimu and others added 2 commits June 10, 2026 15:03
…#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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants