diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java index 93a1839372..7be0508ce6 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartEventHttpMessageReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -53,6 +55,10 @@ public class PartEventHttpMessageReader extends LoggingCodecSupport implements H private int maxHeadersSize = 10 * 1024; + private int maxParts = -1; + + private long maxPartSize = -1; + private Charset headersCharset = StandardCharsets.UTF_8; @@ -85,6 +91,24 @@ public class PartEventHttpMessageReader extends LoggingCodecSupport implements H this.maxHeadersSize = byteCount; } + /** + * Specify the maximum number of parts allowed in a given multipart request. + *

By default this is set to -1, meaning that there is no maximum. + * @since 6.1 + */ + public void setMaxParts(int maxParts) { + this.maxParts = maxParts; + } + + /** + * Configure the maximum size allowed for any part. + *

By default this is set to -1, meaning that there is no maximum. + * @since 6.1 + */ + public void setMaxPartSize(long maxPartSize) { + this.maxPartSize = maxPartSize; + } + /** * Set the character set used to decode headers. *

Defaults to UTF-8 as per RFC 7578. @@ -125,31 +149,50 @@ public class PartEventHttpMessageReader extends LoggingCodecSupport implements H return Flux.error(new DecodingException("No multipart boundary found in Content-Type: \"" + message.getHeaders().getContentType() + "\"")); } - return MultipartParser.parse(message.getBody(), boundary, this.maxHeadersSize, this.headersCharset) - .windowUntil(t -> t instanceof MultipartParser.HeadersToken, true) - .concatMap(tokens -> tokens.switchOnFirst((signal, flux) -> { - if (signal.hasValue()) { - MultipartParser.HeadersToken headersToken = (MultipartParser.HeadersToken) signal.get(); - Assert.state(headersToken != null, "Signal should be headers token"); + Flux allPartsTokens = MultipartParser.parse(message.getBody(), boundary, + this.maxHeadersSize, this.headersCharset); - HttpHeaders headers = headersToken.headers(); - Flux bodyTokens = - flux.filter(t -> t instanceof MultipartParser.BodyToken) - .cast(MultipartParser.BodyToken.class); - return createEvents(headers, bodyTokens); + AtomicInteger partCount = new AtomicInteger(); + return allPartsTokens + .windowUntil(t -> t instanceof MultipartParser.HeadersToken, true) + .concatMap(partTokens -> { + if (tooManyParts(partCount)) { + return Mono.error(new DecodingException("Too many parts (" + partCount.get() + "/" + + this.maxParts + " allowed)")); } else { - // complete or error signal - return flux.cast(PartEvent.class); + return partTokens.switchOnFirst((signal, flux) -> { + if (signal.hasValue()) { + MultipartParser.HeadersToken headersToken = (MultipartParser.HeadersToken) signal.get(); + Assert.state(headersToken != null, "Signal should be headers token"); + + HttpHeaders headers = headersToken.headers(); + Flux bodyTokens = + flux.filter(t -> t instanceof MultipartParser.BodyToken) + .cast(MultipartParser.BodyToken.class); + return createEvents(headers, bodyTokens); + } + else { + // complete or error signal + return flux.cast(PartEvent.class); + } + }); } - })); + }); }); } + private boolean tooManyParts(AtomicInteger partCount) { + int count = partCount.incrementAndGet(); + return this.maxParts > 0 && count > this.maxParts; + } + + private Publisher createEvents(HttpHeaders headers, Flux bodyTokens) { if (MultipartUtils.isFormField(headers)) { Flux contents = bodyTokens.map(MultipartParser.BodyToken::buffer); - return DataBufferUtils.join(contents, this.maxInMemorySize) + int maxSize = (int) Math.min(this.maxInMemorySize, this.maxPartSize); + return DataBufferUtils.join(contents, maxSize) .map(content -> { String value = content.toString(MultipartUtils.charset(headers)); DataBufferUtils.release(content); @@ -157,18 +200,30 @@ public class PartEventHttpMessageReader extends LoggingCodecSupport implements H }) .switchIfEmpty(Mono.fromCallable(() -> DefaultPartEvents.form(headers))); } - else if (headers.getContentDisposition().getFilename() != null) { - return bodyTokens - .map(body -> DefaultPartEvents.file(headers, body.buffer(), body.isLast())) - .switchIfEmpty(Mono.fromCallable(() -> DefaultPartEvents.file(headers))); - } else { + boolean isFilePart = headers.getContentDisposition().getFilename() != null; + AtomicLong partSize = new AtomicLong(); return bodyTokens - .map(body -> DefaultPartEvents.create(headers, body.buffer(), body.isLast())) - .switchIfEmpty(Mono.fromCallable(() -> DefaultPartEvents.create(headers))); // empty body + .concatMap(body -> { + DataBuffer buffer = body.buffer(); + if (tooLarge(partSize, buffer)) { + DataBufferUtils.release(buffer); + return Mono.error(new DataBufferLimitException("Part exceeded the limit of " + + this.maxPartSize + " bytes")); + } + else { + return isFilePart ? Mono.just(DefaultPartEvents.file(headers, buffer, body.isLast())) + : Mono.just(DefaultPartEvents.create(headers, body.buffer(), body.isLast())); + } + }) + .switchIfEmpty(Mono.fromCallable(() -> + isFilePart ? DefaultPartEvents.file(headers) : DefaultPartEvents.create(headers))); } + } - + private boolean tooLarge(AtomicLong partSize, DataBuffer buffer) { + long size = partSize.addAndGet(buffer.readableByteCount()); + return this.maxPartSize > 0 && size > this.maxPartSize; } } diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java index de6a30e842..853b3b10cb 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.ContentDisposition; @@ -226,6 +227,38 @@ class PartEventHttpMessageReaderTests { .verifyComplete(); } + @Test + void tooManyParts() { + MockServerHttpRequest request = createRequest( + new ClassPathResource("simple.multipart", getClass()), "simple-boundary"); + + PartEventHttpMessageReader reader = new PartEventHttpMessageReader(); + reader.setMaxParts(1); + + Flux result = reader.read(forClass(PartEvent.class), request, emptyMap()); + + StepVerifier.create(result) + .expectError(DecodingException.class) + .verify(); + } + + @Test + void partSizeTooLarge() { + MockServerHttpRequest request = createRequest(new ClassPathResource("safari.multipart", getClass()), + "----WebKitFormBoundaryG8fJ50opQOML0oGD"); + + PartEventHttpMessageReader reader = new PartEventHttpMessageReader(); + reader.setMaxPartSize(60); + + Flux result = reader.read(forClass(PartEvent.class), request, emptyMap()); + + StepVerifier.create(result) + .assertNext(data(headersFormField("text1"), bodyText("a"), true)) + .assertNext(data(headersFormField("text2"), bodyText("b"), true)) + .expectError(DataBufferLimitException.class) + .verify(); + + } @Test public void utf8Headers() {