Skip to content

Reject multipart parts missing required Content-Disposition 'name'#29

Open
daguimu wants to merge 2 commits into
netty-contrib:1.0from
daguimu:fix/multipart-missing-name-attribute
Open

Reject multipart parts missing required Content-Disposition 'name'#29
daguimu wants to merge 2 commits into
netty-contrib:1.0from
daguimu:fix/multipart-missing-name-attribute

Conversation

@daguimu

@daguimu daguimu commented Apr 23, 2026

Copy link
Copy Markdown

Problem

When a client sends a multipart/form-data request whose Content-Disposition header is missing the required name= parameter (e.g., the attack-style payloads referenced in netty/netty#16176), HttpPostMultipartRequestDecoder in the vintage module surfaces an opaque NPE:

java.lang.NullPointerException: Cannot invoke
  "io.netty.handler.codec.http.multipart.Attribute.getValue()"
  because "nameAttribute" is null
    at HttpPostMultipartRequestDecoder.decodeMultipart(...)

Operators get no hint about which part of the request was malformed, and the decoder relies on catch (NullPointerException) as its validation mechanism.

Root Cause

currentFieldAttributes.get(HttpHeaderValues.NAME) returns null when the inbound Content-Disposition has no name= parameter. Both getAttribute (FIELD branch) and getFileUpload dereference the resulting Attribute via nameAttribute.getValue() without first checking for null. The surrounding catch (NullPointerException | IllegalArgumentException | IOException) around factory.createAttribute(...) produces the confusing message above.

Fix

Add an explicit nameAttribute == null check in both getAttribute() and getFileUpload() before the cleanString(nameAttribute.getValue()) dereference, throwing ErrorDataDecoderException("Content-Disposition is missing required 'name' parameter").

Because the multipart-vintage module promises 100% behavioral compatibility with the legacy Netty 4 decoder when all quirks are enabled (verified by MultipartComparisonTest), the new check is gated behind a new DecoderQuirk:

  • NPE_ON_MISSING_CONTENT_DISPOSITION_NAME — when enabled, preserves the legacy NPE-wrapped-in-ErrorDataDecoderException. When disabled (the new default for builder users who do not call .enableAllQuirks()), the decoder throws the descriptive exception.

Users of the deprecated constructors and anyone calling .enableAllQuirks() (including the comparison fuzz suite) see unchanged behavior. New users who construct via HttpPostRequestDecoder.builder() get the improved error.

Tests Added

Change point Test
Null check in getAttribute FIELD branch testFieldWithoutNameAttributeThrowsErrorDataDecoderException — sends a multipart field with Content-Disposition: form-data (no name=) and asserts ErrorDataDecoderException whose message mentions the name parameter. Runs under both quirk=false (clean message) and quirk=true (NPE-wrapped message).
Null check in getFileUpload testFileUploadWithoutNameAttributeThrowsErrorDataDecoderException — sends a file upload with Content-Disposition: form-data; filename="upload.txt" (no name=) and asserts the same. Runs under both quirk modes.

Both tests pass in both parameterized runs. Full suite: mvn test → multipart-core 35/35, multipart-vintage 233/233 (2 pre-existing skips unrelated to this change), including the 58 fuzz-corpus cases in MultipartComparisonTest which assert 100% parity with the legacy decoder — the quirk gate keeps those green.

Impact

  • Default behavior (no quirks): malformed multipart requests lacking name= are rejected with a clear ErrorDataDecoderException whose message identifies the missing parameter. The exception type is unchanged, so existing catch blocks behave the same.
  • Legacy/compat behavior (.enableAllQuirks() or deprecated constructors): unchanged — the NPE-wrapped-in-ErrorDataDecoderException is still produced, preserving the drop-in-replacement promise.
  • No public API surface is removed; one enum value added to DecoderQuirk following the existing per-bug pattern (e.g., CONSERVATIVE_LF_BACKTRACK, DISABLE_EARLY_MIXED_END).
  • Scope is limited to HttpPostMultipartRequestDecoder in the vintage module plus the one DecoderQuirk enum addition; HttpPostStandardRequestDecoder, UrlEncodedDecoder, and other paths are untouched.

Background

Migrated from netty/netty#16688 per @yawkat's recommendation in that thread. Fixes netty/netty#16176.

When a client sends a multipart/form-data request whose
Content-Disposition header has no 'name' parameter, the vintage
HttpPostMultipartRequestDecoder dereferences the missing name
attribute and surfaces an opaque NPE wrapped in
ErrorDataDecoderException: 'Cannot invoke Attribute.getValue()
because nameAttribute is null'. Operators have no hint about which
part was malformed.

Add an explicit null check before cleanString(nameAttribute.getValue())
in both the FIELD branch (getAttribute) and the file-upload branch
(getFileUpload), throwing a descriptive ErrorDataDecoderException
that names the missing parameter.

Because the vintage module promises 100% behavioral compatibility
with the legacy Netty 4 decoder when all quirks are enabled, the new
check is gated behind a new DecoderQuirk,
NPE_ON_MISSING_CONTENT_DISPOSITION_NAME. Enabling the quirk restores
the legacy NPE-wrapped-in-ErrorDataDecoderException. Migrated via
.enableAllQuirks(), deprecated constructors, or the comparison fuzz
suite, the quirk is on and behavior is unchanged.

Migrated from netty/netty#16688.
Fixes netty/netty#16176.
Align the DecoderQuirk javadoc with sibling quirks ('replicates a
bug' wording used by CONSERVATIVE_LF_BACKTRACK, FORWARD_CHUNK_CR).

Split the missing-name test assertions across quirk modes: when
quirks are on, verify the wrapped cause is a NullPointerException
(confirming legacy behavior is preserved); when quirks are off,
assert the exact new message and absence of a cause. The previous
contains('name') check passed in both modes because the legacy NPE
message happens to contain 'nameAttribute', which would not catch
a regression that flipped the gate.
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.

We're getting a NullPointerException from Netty when handling multipart HTTP 1.1 request using Vert.x

1 participant