Skip to content

Commit 0416168

Browse files
committed
Fix bug in max header calculation in DefaultPartHttpMessageReader
This commit fixes a bug in the DefaultPartHttpMessageReader, in the check for exceeding the maximum header size. Before this commit, the entire buffer size was considered, thus triggering an exception even though the max header limit was not exceeded. After this commit, we only consider the size up until the end-of-header mark (CRLFCRLF). Furthermore, this commit increases the default maximum header size to 10k, the same default as Commons File upload. Closes gh-27612
1 parent c4c3d59 commit 0416168

File tree

4 files changed

+67
-33
lines changed

4 files changed

+67
-33
lines changed

spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
6363

6464
private int maxInMemorySize = 256 * 1024;
6565

66-
private int maxHeadersSize = 8 * 1024;
66+
private int maxHeadersSize = 10 * 1024;
6767

6868
private long maxDiskUsagePerPart = -1;
6969

spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -342,33 +342,23 @@ private final class HeadersState implements State {
342342

343343
/**
344344
* First checks whether the multipart boundary leading to this state
345-
* was the final boundary, or whether {@link #maxHeadersSize} is
346-
* exceeded. Then looks for the header-body boundary
347-
* ({@code CR LF CR LF}) in the given buffer. If found, convert
348-
* all buffers collected so far into a {@link HttpHeaders} object
345+
* was the final boundary. Then looks for the header-body boundary
346+
* ({@code CR LF CR LF}) in the given buffer. If found, checks whether
347+
* the size of all header buffers does not exceed {@link #maxHeadersSize},
348+
* converts all buffers collected so far into a {@link HttpHeaders} object
349349
* and changes to {@link BodyState}, passing the remainder of the
350-
* buffer. If the boundary is not found, the buffer is collected.
350+
* buffer. If the boundary is not found, the buffer is collected if
351+
* its size does not exceed {@link #maxHeadersSize}.
351352
*/
352353
@Override
353354
public void onNext(DataBuffer buf) {
354-
long prevCount = this.byteCount.get();
355-
long count = this.byteCount.addAndGet(buf.readableByteCount());
356-
if (prevCount < 2 && count >= 2) {
357-
if (isLastBoundary(buf)) {
358-
if (logger.isTraceEnabled()) {
359-
logger.trace("Last boundary found in " + buf);
360-
}
361-
362-
if (changeState(this, DisposedState.INSTANCE, buf)) {
363-
emitComplete();
364-
}
365-
return;
355+
if (isLastBoundary(buf)) {
356+
if (logger.isTraceEnabled()) {
357+
logger.trace("Last boundary found in " + buf);
366358
}
367-
}
368-
else if (count > MultipartParser.this.maxHeadersSize) {
359+
369360
if (changeState(this, DisposedState.INSTANCE, buf)) {
370-
emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " +
371-
MultipartParser.this.maxHeadersSize + " bytes"));
361+
emitComplete();
372362
}
373363
return;
374364
}
@@ -377,17 +367,23 @@ else if (count > MultipartParser.this.maxHeadersSize) {
377367
if (logger.isTraceEnabled()) {
378368
logger.trace("End of headers found @" + endIdx + " in " + buf);
379369
}
380-
DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx);
381-
this.buffers.add(headerBuf);
382-
DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx);
383-
DataBufferUtils.release(buf);
384-
385-
emitHeaders(parseHeaders());
386-
changeState(this, new BodyState(), bodyBuf);
370+
long count = this.byteCount.addAndGet(endIdx);
371+
if (belowMaxHeaderSize(count)) {
372+
DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx);
373+
this.buffers.add(headerBuf);
374+
DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx);
375+
DataBufferUtils.release(buf);
376+
377+
emitHeaders(parseHeaders());
378+
changeState(this, new BodyState(), bodyBuf);
379+
}
387380
}
388381
else {
389-
this.buffers.add(buf);
390-
requestBuffer();
382+
long count = this.byteCount.addAndGet(buf.readableByteCount());
383+
if (belowMaxHeaderSize(count)) {
384+
this.buffers.add(buf);
385+
requestBuffer();
386+
}
391387
}
392388
}
393389

@@ -407,6 +403,21 @@ private boolean isLastBoundary(DataBuffer buf) {
407403
buf.getByte(0) == HYPHEN);
408404
}
409405

406+
/**
407+
* Checks whether the given {@code count} is below or equal to {@link #maxHeadersSize}
408+
* and emits a {@link DataBufferLimitException} if not.
409+
*/
410+
private boolean belowMaxHeaderSize(long count) {
411+
if (count <= MultipartParser.this.maxHeadersSize) {
412+
return true;
413+
}
414+
else {
415+
emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " +
416+
MultipartParser.this.maxHeadersSize + " bytes"));
417+
return false;
418+
}
419+
}
420+
410421
/**
411422
* Parses the list of buffers into a {@link HttpHeaders} instance.
412423
* Converts the joined buffers into a string using ISO=8859-1, and parses

spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,31 @@ public void utf8Headers(String displayName, DefaultPartHttpMessageReader reader)
270270
latch.await();
271271
}
272272

273+
// gh-27612
274+
@Test
275+
public void exceedHeaderLimit() throws InterruptedException {
276+
Flux<DataBuffer> body = DataBufferUtils
277+
.readByteChannel((new ClassPathResource("files.multipart", getClass()))::readableChannel, bufferFactory, 282);
278+
279+
MediaType contentType = new MediaType("multipart", "form-data", singletonMap("boundary", "----WebKitFormBoundaryG8fJ50opQOML0oGD"));
280+
MockServerHttpRequest request = MockServerHttpRequest.post("/")
281+
.contentType(contentType)
282+
.body(body);
283+
284+
DefaultPartHttpMessageReader reader = new DefaultPartHttpMessageReader();
285+
286+
reader.setMaxHeadersSize(230);
287+
288+
Flux<Part> result = reader.read(forClass(Part.class), request, emptyMap());
289+
290+
CountDownLatch latch = new CountDownLatch(2);
291+
StepVerifier.create(result)
292+
.consumeNextWith(part -> testPart(part, null, LOREM_IPSUM, latch))
293+
.consumeNextWith(part -> testPart(part, null, MUSPI_MEROL, latch))
294+
.verifyComplete();
295+
296+
latch.await();
297+
}
273298

274299
private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary)
275300
throws InterruptedException {

spring-web/src/test/resources/org/springframework/http/codec/multipart/files.multipart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ Content-Disposition: form-data; name="file2"; filename="a.txt"
33
Content-Type: text/plain
44

55
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer iaculis metus id vestibulum nullam.
6-
76
------WebKitFormBoundaryG8fJ50opQOML0oGD
87
Content-Disposition: form-data; name="file2"; filename="b.txt"
98
Content-Type: text/plain
109

1110
.mallun mulubitsev di sutem silucai regetnI .tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL
12-
1311
------WebKitFormBoundaryG8fJ50opQOML0oGD--

0 commit comments

Comments
 (0)