Skip to content

Pin multipart Content-Type / Content-Transfer-Encoding case folding t…#16784

Merged
normanmaurer merged 1 commit into
4.1from
loc41
May 11, 2026
Merged

Pin multipart Content-Type / Content-Transfer-Encoding case folding t…#16784
normanmaurer merged 1 commit into
4.1from
loc41

Conversation

@normanmaurer

Copy link
Copy Markdown
Member

…o Locale.US (#16768)

Two spots in the HTTP multipart codec normalize a header value with String.toLowerCase() without an explicit Locale, then compare the result to lowercase ASCII constants. The JVM default Locale governs that call, and on a Turkish (tr_TR) JVM the ASCII 'I' lowercases to 'ı' (U+0131). A perfectly valid uppercase form silently misses the comparison.

Concretely on a Turkish-locale JVM:

  • HttpPostMultipartRequestDecoder decoding a part with Content-Transfer-Encoding: BINARY (uppercase, RFC 2045 §6.1 says mechanism tokens are case-insensitive) lowercases the value to "bınary", fails the equality check against the binary / 7bit / 8bit constants, and throws ErrorDataDecoderException: TransferEncoding Unknown: bınary.
  • HttpPostRequestEncoder.finalizeRequest() skipping a pre-existing Content-Type: MULTIPART/form-data; boundary=... header (mixed case is allowed by the Content-Type ABNF, RFC 7231 §3.1.1.1) lowercases the value to "multıpart/...", misses the
    startsWith("multipart/form-data") check, and the original mixed-case header survives alongside the freshly-built multipart Content-Type the encoder is about to add. The outgoing request ends up with two Content-Type headers.

The values being compared against are lowercase ASCII tokens and have no business consulting the JVM locale.

Pin both String.toLowerCase() calls to Locale.US:

codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.javagetFileUpload(String delimiter) lowercases the Content-Transfer-Encoding value before matching it against BIT7 / BIT8 / BINARY mechanism tokens.

codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.javafinalizeRequest() lowercases each existing Content-Type before checking if it is the multipart or application/x-www-form-urlencoded form (which the encoder is responsible for setting fresh).

Change Point Test
Decoder Content-Transfer-Encoding lowercase under Turkish HttpPostMultiPartRequestDecoderTest.testUppercaseBinaryTransferEncodingUnderTurkishLocale — installs the Turkish locale (restored in finally), feeds a multipart body with Content-Transfer-Encoding: BINARY, and asserts the file part decodes successfully. Without the fix this throws TransferEncoding Unknown: bınary.
Encoder Content-Type lowercase under Turkish
HttpPostRequestEncoderTest.testFinalizeRemovesPreexistingMultipartContentTypeUnderTurkishLocale — pre-sets Content-Type: MULTIPART/form-data; boundary=preexisting, runs the encoder under the Turkish locale, and asserts only one Content-Type header survives. Without the fix the request ends up with two Content-Type headers.

Both tests fail deterministically on the unfixed code (one as an assertion failure, the other as a thrown ErrorDataDecoderException) and pass after the Locale.US change.

Verification:

mvn -pl codec-http -am test \
  -Dtest='HttpPostRequestEncoderTest#testFinalizeRemovesPreexistingMultipartContentTypeUnderTurkishLocale,HttpPostMultiPartRequestDecoderTest#testUppercaseBinaryTransferEncodingUnderTurkishLocale' \
  -Dsurefire.failIfNoSpecifiedTests=false
  • Behavior on en/CJK/most locales: unchanged (the previous default-locale lowercase already produced ASCII).
  • Behavior on Turkish (and other locales with non-ASCII case mappings): a multipart upload with uppercase Content-Transfer-Encoding: BINARY now decodes instead of throwing; an outgoing multipart request with a pre-existing mixed-case Content-Type: MULTIPART/... no longer leaks a duplicate Content-Type header.
  • API: no signature change. All callers continue through the existing public API.
  • Risk: bounded — Locale.US is the standard Netty pattern for protocol-string normalization (see e.g.
    WebSocketClientHandshaker.java:761).

Sibling fix to #16765, which pinned the same locale gotcha in HttpVersion, RtspMethods, and RtspVersions.

…o Locale.US (#16768)

Two spots in the HTTP multipart codec normalize a header value with
`String.toLowerCase()` without an explicit Locale, then compare the
result to lowercase ASCII constants. The JVM default Locale governs that
call, and on a Turkish (`tr_TR`) JVM the ASCII `'I'` lowercases to `'ı'`
(U+0131). A perfectly valid uppercase form silently misses the
comparison.

Concretely on a Turkish-locale JVM:

- `HttpPostMultipartRequestDecoder` decoding a part with
`Content-Transfer-Encoding: BINARY` (uppercase, RFC 2045 §6.1 says
mechanism tokens are case-insensitive) lowercases the value to
`"bınary"`, fails the equality check against the `binary` / `7bit` /
`8bit` constants, and throws `ErrorDataDecoderException:
TransferEncoding Unknown: bınary`.
- `HttpPostRequestEncoder.finalizeRequest()` skipping a pre-existing
`Content-Type: MULTIPART/form-data; boundary=...` header (mixed case is
allowed by the Content-Type ABNF, RFC 7231 §3.1.1.1) lowercases the
value to `"multıpart/..."`, misses the
`startsWith("multipart/form-data")` check, and the original mixed-case
header survives alongside the freshly-built multipart Content-Type the
encoder is about to add. The outgoing request ends up with **two**
`Content-Type` headers.

The values being compared against are lowercase ASCII tokens and have no
business consulting the JVM locale.

Pin both `String.toLowerCase()` calls to `Locale.US`:

-
`codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java`
— `getFileUpload(String delimiter)` lowercases the
`Content-Transfer-Encoding` value before matching it against `BIT7` /
`BIT8` / `BINARY` mechanism tokens.
-
`codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java`
— `finalizeRequest()` lowercases each existing `Content-Type` before
checking if it is the multipart or `application/x-www-form-urlencoded`
form (which the encoder is responsible for setting fresh).

| Change Point | Test |
|--------------|------|
| Decoder Content-Transfer-Encoding lowercase under Turkish |
`HttpPostMultiPartRequestDecoderTest.testUppercaseBinaryTransferEncodingUnderTurkishLocale`
— installs the Turkish locale (restored in `finally`), feeds a multipart
body with `Content-Transfer-Encoding: BINARY`, and asserts the file part
decodes successfully. Without the fix this throws `TransferEncoding
Unknown: bınary`. |
| Encoder Content-Type lowercase under Turkish |
`HttpPostRequestEncoderTest.testFinalizeRemovesPreexistingMultipartContentTypeUnderTurkishLocale`
— pre-sets `Content-Type: MULTIPART/form-data; boundary=preexisting`,
runs the encoder under the Turkish locale, and asserts only one
Content-Type header survives. Without the fix the request ends up with
two Content-Type headers. |

Both tests fail deterministically on the unfixed code (one as an
assertion failure, the other as a thrown `ErrorDataDecoderException`)
and pass after the `Locale.US` change.

Verification:

```
mvn -pl codec-http -am test \
  -Dtest='HttpPostRequestEncoderTest#testFinalizeRemovesPreexistingMultipartContentTypeUnderTurkishLocale,HttpPostMultiPartRequestDecoderTest#testUppercaseBinaryTransferEncodingUnderTurkishLocale' \
  -Dsurefire.failIfNoSpecifiedTests=false
```

- Behavior on en/CJK/most locales: unchanged (the previous
default-locale lowercase already produced ASCII).
- Behavior on Turkish (and other locales with non-ASCII case mappings):
a multipart upload with uppercase `Content-Transfer-Encoding: BINARY`
now decodes instead of throwing; an outgoing multipart request with a
pre-existing mixed-case `Content-Type: MULTIPART/...` no longer leaks a
duplicate Content-Type header.
- API: no signature change. All callers continue through the existing
public API.
- Risk: bounded — `Locale.US` is the standard Netty pattern for
protocol-string normalization (see e.g.
`WebSocketClientHandshaker.java:761`).

Sibling fix to #16765, which pinned the same locale gotcha in
`HttpVersion`, `RtspMethods`, and `RtspVersions`.

---------

Co-authored-by: Norman Maurer <norman_maurer@apple.com>
@normanmaurer normanmaurer added this to the 4.1.134.Final milestone May 10, 2026
@normanmaurer normanmaurer merged commit c4232c2 into 4.1 May 11, 2026
33 of 34 checks passed
@normanmaurer normanmaurer deleted the loc41 branch May 11, 2026 06:14
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