Skip to content

Commit 7bc0fda

Browse files
yawkatclaude
andauthored
Ignore Content-Type parameters for part metadata without quirks (#52)
The vintage multipart decoder stored every Content-Type parameter in currentFieldAttributes under its own name. A parameter named name, filename or content-length therefore replaced the value from Content-Disposition or Content-Length, changing the field name or upload filename, turning a plain field into a FileUpload, or changing the part length. Legacy Netty behaves the same way, so this is kept when LEGACY_HEADER_SPLITTING is enabled. Without the quirk, only the type itself and the charset parameter of Content-Type are used; other parameters and stray tokens after the type are ignored. Fixes #48 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3af7a0b commit 7bc0fda

3 files changed

Lines changed: 101 additions & 1 deletion

File tree

‎multipart-core/src/main/java/io/netty/contrib/multipart/DecoderQuirk.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ public enum DecoderQuirk {
4141
* The modern parser uses structured parsing for {@code Content-Type} parameters, but the legacy decoder exposes
4242
* raw tokens. Enabling this quirk switches to the legacy split and fills the {@code quirkHeader} array so the
4343
* vintage wrapper can continue consuming headers exactly like before.
44+
* <p>
45+
* With this quirk, the vintage multipart decoder also stores {@code Content-Type} parameters such as {@code name},
46+
* {@code filename} and {@code content-length} as part metadata, overriding the values from
47+
* {@code Content-Disposition} and {@code Content-Length}, matching legacy Netty.
4448
*/
4549
LEGACY_HEADER_SPLITTING,
4650

‎multipart-vintage/src/main/java/io/netty/contrib/multipart/vintage/HttpPostMultipartRequestDecoder.java‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,10 @@ private boolean handleHeader(String[] contents) {
652652
throw new ErrorDataDecoderException("Mixed Multipart found in a previous Mixed Multipart");
653653
}
654654
} else {
655+
// Legacy decoders stored every Content-Type parameter under its own name, so parameters such as
656+
// name, filename or content-length would overwrite the part metadata. Without the quirk, only the
657+
// type itself and the charset parameter are used.
658+
boolean legacyParameters = decoder.hasQuirk(DecoderQuirk.LEGACY_HEADER_SPLITTING);
655659
for (int i = 1; i < contents.length; i++) {
656660
final String charsetHeader = HttpHeaderValues.CHARSET.toString();
657661
if (contents[i].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
@@ -666,7 +670,10 @@ private boolean handleHeader(String[] contents) {
666670
throw new ErrorDataDecoderException(e);
667671
}
668672
putCurrentFieldAttribute(HttpHeaderValues.CHARSET, attribute);
669-
} else if (contents[i].contains("=")) {
673+
} else if (!legacyParameters && i > 1) {
674+
// Other parameters (and stray tokens after the type) do not affect the part.
675+
continue;
676+
} else if (legacyParameters && contents[i].contains("=")) {
670677
String name = StringUtil.substringBefore(contents[i], '=');
671678
String values = StringUtil.substringAfter(contents[i], '=');
672679
Attribute attribute;

‎multipart-vintage/src/test/java/io/netty/contrib/multipart/vintage/MultipartQuirksVintageTest.java‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,22 @@
2323
import io.netty.handler.codec.http.HttpHeaderValues;
2424
import io.netty.handler.codec.http.HttpMethod;
2525
import io.netty.handler.codec.http.HttpVersion;
26+
import io.netty.handler.codec.http.multipart.Attribute;
2627
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
28+
import io.netty.handler.codec.http.multipart.FileUpload;
2729
import io.netty.handler.codec.http.multipart.HttpDataFactory;
30+
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
2831
import org.junit.jupiter.api.AfterEach;
2932
import org.junit.jupiter.api.BeforeEach;
3033
import org.junit.jupiter.api.Test;
3134
import org.junit.jupiter.params.ParameterizedClass;
3235
import org.junit.jupiter.params.provider.ValueSource;
3336

37+
import java.io.IOException;
3438
import java.nio.charset.StandardCharsets;
3539

3640
import static org.junit.jupiter.api.Assertions.assertEquals;
41+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
3742

3843
@ParameterizedClass
3944
@ValueSource(booleans = {false, true})
@@ -78,4 +83,88 @@ void useFieldCharsetForDelimiterSearch() {
7883
decoder.destroy();
7984
}
8085
}
86+
87+
private HttpPostRequestDecoder legacyHeaderSplittingDecoder(String body) {
88+
HttpPostRequestDecoder.Builder builder = HttpPostRequestDecoder.builder().dataFactory(dataFactory);
89+
if (quirk) {
90+
builder.enableQuirks(DecoderQuirk.LEGACY_HEADER_SPLITTING);
91+
}
92+
return builder.build(fullRequest("a", body));
93+
}
94+
95+
@Test
96+
void contentTypeNameParameterDoesNotOverrideFieldName() {
97+
HttpPostRequestDecoder decoder = legacyHeaderSplittingDecoder("--a\r\n" +
98+
"Content-Disposition: form-data; name=\"file\"; filename=\"real.pdf\"\r\n" +
99+
"Content-Type: application/pdf; name=\"x.pdf\"\r\n" +
100+
"\r\n" +
101+
"v\r\n" +
102+
"--a--\r\n");
103+
try {
104+
FileUpload upload = assertInstanceOf(FileUpload.class, decoder.getBodyHttpDatas().get(0));
105+
assertEquals(quirk ? "x.pdf" : "file", upload.getName());
106+
assertEquals("real.pdf", upload.getFilename());
107+
assertEquals("application/pdf", upload.getContentType());
108+
} finally {
109+
decoder.destroy();
110+
}
111+
}
112+
113+
@Test
114+
void contentTypeFilenameParameterDoesNotOverrideFilename() {
115+
HttpPostRequestDecoder decoder = legacyHeaderSplittingDecoder("--a\r\n" +
116+
"Content-Disposition: form-data; name=\"file\"; filename=\"real.pdf\"\r\n" +
117+
"Content-Type: application/pdf; filename=\"x.pdf\"\r\n" +
118+
"\r\n" +
119+
"v\r\n" +
120+
"--a--\r\n");
121+
try {
122+
FileUpload upload = assertInstanceOf(FileUpload.class, decoder.getBodyHttpDatas().get(0));
123+
assertEquals("file", upload.getName());
124+
assertEquals(quirk ? "x.pdf" : "real.pdf", upload.getFilename());
125+
} finally {
126+
decoder.destroy();
127+
}
128+
}
129+
130+
@Test
131+
void contentTypeFilenameParameterDoesNotTurnFieldIntoFileUpload() throws IOException {
132+
HttpPostRequestDecoder decoder = legacyHeaderSplittingDecoder("--a\r\n" +
133+
"Content-Disposition: form-data; name=\"field\"\r\n" +
134+
"Content-Type: text/plain; filename=\"x.pdf\"; charset=UTF-8\r\n" +
135+
"\r\n" +
136+
"v\r\n" +
137+
"--a--\r\n");
138+
try {
139+
InterfaceHttpData data = decoder.getBodyHttpDatas().get(0);
140+
assertEquals("field", data.getName());
141+
if (quirk) {
142+
assertEquals("x.pdf", assertInstanceOf(FileUpload.class, data).getFilename());
143+
} else {
144+
Attribute attribute = assertInstanceOf(Attribute.class, data);
145+
assertEquals("v", attribute.getValue());
146+
assertEquals(StandardCharsets.UTF_8, attribute.getCharset());
147+
}
148+
} finally {
149+
decoder.destroy();
150+
}
151+
}
152+
153+
@Test
154+
void contentTypeContentLengthParameterDoesNotOverrideLength() throws IOException {
155+
HttpPostRequestDecoder decoder = legacyHeaderSplittingDecoder("--a\r\n" +
156+
"Content-Disposition: form-data; name=\"field\"\r\n" +
157+
"Content-Type: text/plain; content-length=100\r\n" +
158+
"\r\n" +
159+
"value\r\n" +
160+
"--a--\r\n");
161+
try {
162+
Attribute attribute = assertInstanceOf(Attribute.class, decoder.getBodyHttpDatas().get(0));
163+
assertEquals("field", attribute.getName());
164+
assertEquals("value", attribute.getValue());
165+
assertEquals(quirk ? 100 : 0, attribute.definedLength());
166+
} finally {
167+
decoder.destroy();
168+
}
169+
}
81170
}

0 commit comments

Comments
 (0)