From e749179e325b769e1b1b4f6a7e1b943f71397fef Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 21 Aug 2026 17:53:20 +1000 Subject: [PATCH] Fixes for MultiPartCompliance configuration Signed-off-by: Lachlan Roberts --- .../org/eclipse/jetty/http/MultiPart.java | 12 +++- .../jetty/http/MultiPartCompliance.java | 2 +- .../eclipse/jetty/http/MultiPartFormData.java | 7 +-- .../jetty/http/MultiPartFormDataTest.java | 60 +++++++++++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java index a2c5f2df7d4c..0297f172367e 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java @@ -1104,8 +1104,8 @@ public static class Parser private final Utf8StringBuilder text = new Utf8StringBuilder(); private final String boundary; private final SearchPattern boundaryFinder; - private final MultiPartCompliance compliance; private final Listener listener; + private MultiPartCompliance compliance; private int partHeadersLength; private int partHeadersMaxLength = -1; private State state; @@ -1160,6 +1160,16 @@ public int getPartHeadersMaxLength() return partHeadersMaxLength; } + public MultiPartCompliance getMultiPartCompliance() + { + return compliance; + } + + public void setMultiPartCompliance(MultiPartCompliance multiPartCompliance) + { + compliance = Objects.requireNonNull(multiPartCompliance); + } + /** * @param partHeadersMaxLength the max length of a {@link Part} headers, in bytes, or -1 for unlimited length */ diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartCompliance.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartCompliance.java index b96747c73bb2..06941e5030a7 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartCompliance.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartCompliance.java @@ -84,7 +84,7 @@ public String getDescription() public static final MultiPartCompliance LEGACY = new MultiPartCompliance( "LEGACY", EnumSet.complementOf(EnumSet.of(Violation.BASE64_TRANSFER_ENCODING))); - private static final List KNOWN_MODES = Arrays.asList(RFC7578, LEGACY); + private static final List KNOWN_MODES = Arrays.asList(RFC7578, RFC7578_STRICT, LEGACY); private static final AtomicInteger __custom = new AtomicInteger(); public static MultiPartCompliance valueOf(String name) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java index 2517d92aaf59..477aa07444ce 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormData.java @@ -391,7 +391,6 @@ public static class Parser private final PartsListener listener = new PartsListener(); private final MultiPart.Parser parser; private ByteBufferPool.Sized bufferPool; - private MultiPartCompliance compliance; private ComplianceViolation.Listener complianceListener; private boolean useFilesForPartsWithoutFileName = true; private Path filesDirectory; @@ -411,9 +410,8 @@ public Parser(String boundary) @Deprecated public Parser(String boundary, MultiPartCompliance multiPartCompliance, ComplianceViolation.Listener complianceViolationListener) { - compliance = Objects.requireNonNull(multiPartCompliance); complianceListener = Objects.requireNonNull(complianceViolationListener); - parser = new MultiPart.Parser(Objects.requireNonNull(boundary), compliance, listener); + parser = new MultiPart.Parser(Objects.requireNonNull(boundary), Objects.requireNonNull(multiPartCompliance), listener); } /** @@ -693,7 +691,7 @@ public void configure(MultiPartConfig config) useFilesForPartsWithoutFileName = config.isUseFilesForPartsWithoutFileName(); filesDirectory = config.getLocation(); complianceListener = config.getViolationListener(); - compliance = config.getMultiPartCompliance(); + parser.setMultiPartCompliance(config.getMultiPartCompliance()); bufferPool = config.getBufferPool(); } @@ -899,6 +897,7 @@ public void onFailure(Throwable failure) @Override public void onViolation(MultiPartCompliance.Violation violation) { + MultiPartCompliance compliance = parser.getMultiPartCompliance(); boolean allowed = compliance.allows(violation); ComplianceViolation.Event event = new ComplianceViolation.Event(compliance, violation, "multipart spec violation", allowed); ComplianceUtils.notify(complianceListener, event); diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java index 01aedd360d2d..db62335b7ea6 100644 --- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java +++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormDataTest.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -32,14 +33,18 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.io.content.AsyncContent; +import org.eclipse.jetty.io.content.ByteBufferContentSource; import org.eclipse.jetty.io.content.InputStreamContentSource; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.US_ASCII; @@ -48,6 +53,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsStringIgnoringCase; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -429,6 +435,60 @@ public void testLFOnlyEOLStrict() assertThat(ee.getCause().getMessage(), containsString("invalid LF-only EOL")); } + @ParameterizedTest + @ValueSource(strings = {"RFC7578", "RFC7578_STRICT"}) + public void testComplianceModeConfiguration(String spec) throws Exception + { + MultiPartCompliance compliance = MultiPartCompliance.from(spec); + + String boundary = "boundary"; + String str = """ + --$B + Content-Disposition: form-data; name="greeting" + Content-Type: text/plain; charset=US-ASCII + + Hello World + --$B-- + """.replace("$B", boundary); + + CaptureMultiPartViolations listener = new CaptureMultiPartViolations(); + MultiPartConfig config = new MultiPartConfig.Builder() + .location(_tmpDir) + .maxMemoryPartSize(-1) + .complianceMode(compliance) + .violationListener(listener) + .build(); + + String contentType = "multipart/form-data; boundary=\"" + boundary + "\""; + Attributes attributes = new Attributes.Mapped(); + Content.Source source = new ByteBufferContentSource(BufferUtil.toBuffer(str, UTF_8)); + if (compliance.equals(MultiPartCompliance.RFC7578)) + { + // RFC7578 tolerates LF line endings, parsing succeeds and the violation is reported. + try (MultiPartFormData.Parts parts = MultiPartFormData.getParts(source, attributes, contentType, config)) + { + assertThat(parts.size(), is(1)); + MultiPart.Part greeting = parts.getFirst("greeting"); + assertThat(greeting, notNullValue()); + assertThat(Content.Source.asString(greeting.getContentSource()), is("Hello World")); + } + assertThat(listener.getEvents().stream().map(ComplianceViolation.Event::violation).collect(Collectors.toList()), + hasItem(MultiPartCompliance.Violation.LF_LINE_TERMINATION)); + } + else if (compliance.equals(MultiPartCompliance.RFC7578_STRICT)) + { + // RFC7578_STRICT rejects LF line endings, the parsing must fail. + CompletionException ce = assertThrows(CompletionException.class, + () -> MultiPartFormData.getParts(source, attributes, contentType, config)); + assertThat(ce.getCause(), instanceOf(HttpException.class)); + assertThat(ce.getCause().getMessage(), containsString("invalid LF-only EOL")); + } + else + { + throw new IllegalStateException("Unexpected compliance mode: " + compliance); + } + } + /** * Test of parsing where there is whitespace before the boundary. *