This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 4d62fb72f8 fix: bound multipart buffering in FileSizeFilter to prevent
memory DoS (#6627) (#7061)
4d62fb72f8 is described below
commit 4d62fb72f8db41cb408bf530a60c3a7f586e81fa
Author: wy471x <[email protected]>
AuthorDate: Sun Sep 20 21:09:26 2026 +0800
fix: bound multipart buffering in FileSizeFilter to prevent memory DoS
(#6627) (#7061)
Co-authored-by: aias00 <[email protected]>
---
.../apache/shenyu/web/filter/FileSizeFilter.java | 62 ++++++++++++++++++----
.../shenyu/web/filter/FileSizeFilterTest.java | 35 ++++++++++++
2 files changed, 87 insertions(+), 10 deletions(-)
diff --git
a/shenyu-web/src/main/java/org/apache/shenyu/web/filter/FileSizeFilter.java
b/shenyu-web/src/main/java/org/apache/shenyu/web/filter/FileSizeFilter.java
index 1313a29ba5..03fc99f7e7 100644
--- a/shenyu-web/src/main/java/org/apache/shenyu/web/filter/FileSizeFilter.java
+++ b/shenyu-web/src/main/java/org/apache/shenyu/web/filter/FileSizeFilter.java
@@ -27,6 +27,7 @@ import org.apache.shenyu.plugin.base.utils.ResponseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@@ -57,13 +58,20 @@ public class FileSizeFilter implements WebFilter {
private final int fileMaxSize;
+ /**
+ * The max number of bytes buffered while reading a multipart body, or -1
when the configured
+ * max size is not positive (in that case every multipart request is
rejected without buffering).
+ */
+ private final int maxInMemorySize;
+
private final List<HttpMessageReader<?>> messageReaders;
public FileSizeFilter(final int fileMaxSize) {
+ this.fileMaxSize = fileMaxSize;
+ this.maxInMemorySize = maxInMemorySize(fileMaxSize);
HandlerStrategies handlerStrategies = HandlerStrategies.builder()
- .codecs(configurer ->
configurer.defaultCodecs().maxInMemorySize(-1)).build();
+ .codecs(configurer ->
configurer.defaultCodecs().maxInMemorySize(this.maxInMemorySize)).build();
this.messageReaders = handlerStrategies.messageReaders();
- this.fileMaxSize = fileMaxSize;
}
@Override
@@ -71,17 +79,19 @@ public class FileSizeFilter implements WebFilter {
public Mono<Void> filter(@NonNull final ServerWebExchange exchange,
@NonNull final WebFilterChain chain) {
MediaType mediaType =
exchange.getRequest().getHeaders().getContentType();
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType)) {
+ // a non-positive max size rejects every multipart request, so its
body is not buffered at all
+ if (fileMaxSize <= 0) {
+ return payloadTooLarge(exchange, "The configured max size is "
+ fileMaxSize + "M");
+ }
ServerRequest serverRequest = ServerRequest.create(exchange,
messageReaders);
return serverRequest.bodyToMono(DataBuffer.class)
.flatMap(dataBuffer -> {
- if (dataBuffer.capacity() > Constants.BYTES_PER_MB *
fileMaxSize) {
- ServerHttpResponse response =
exchange.getResponse();
- response.setStatusCode(HttpStatus.BAD_REQUEST);
- Object error = ShenyuResultWrap.error(exchange,
ShenyuResultEnum.PAYLOAD_TOO_LARGE);
- LOG.info("The file size exceeds the limit. The
actual size is {}M , response:{}",
- dataBuffer.capacity() /
Constants.BYTES_PER_MB, error);
- return WebFluxResultUtils.result(exchange, error);
+ if (dataBuffer.capacity() > maxInMemorySize) {
+ final int actualSize = dataBuffer.capacity();
+ DataBufferUtils.release(dataBuffer);
+ return payloadTooLarge(exchange,
+ "The actual size is " + actualSize /
Constants.BYTES_PER_MB + "M");
}
BodyInserter<Mono<DataBuffer>,
ReactiveHttpOutputMessage> bodyInsert = BodyInserters
.fromPublisher(Mono.just(dataBuffer),
DataBuffer.class);
@@ -97,12 +107,44 @@ public class FileSizeFilter implements WebFilter {
}))
.doFinally(signalType ->
DataBufferUtils.release(dataBuffer))
.onErrorResume(throwable ->
ResponseUtils.release(outputMessage, throwable));
- });
+ })
+ .onErrorResume(DataBufferLimitException.class,
+ e -> payloadTooLarge(exchange, "The max size is "
+ fileMaxSize + "M"));
}
return chain.filter(exchange);
}
+ /**
+ * Reject the request as payload too large.
+ *
+ * @param exchange the exchange
+ * @param sizeDetail the size detail used for logging
+ * @return the result
+ */
+ private Mono<Void> payloadTooLarge(final ServerWebExchange exchange, final
String sizeDetail) {
+ ServerHttpResponse response = exchange.getResponse();
+ response.setStatusCode(HttpStatus.BAD_REQUEST);
+ Object error = ShenyuResultWrap.error(exchange,
ShenyuResultEnum.PAYLOAD_TOO_LARGE);
+ LOG.info("The file size exceeds the limit. {} , response:{}",
sizeDetail, error);
+ return WebFluxResultUtils.result(exchange, error);
+ }
+
+ /**
+ * The max number of bytes to buffer while reading a multipart body.
+ * A bounded value makes the codec fail as soon as the body exceeds the
limit,
+ * instead of buffering an unbounded body in memory.
+ *
+ * @param fileMaxSize the file max size in MB
+ * @return the max number of bytes, or -1 for a non-positive file max size
+ */
+ private static int maxInMemorySize(final int fileMaxSize) {
+ if (fileMaxSize <= 0) {
+ return -1;
+ }
+ return (int) Math.min((long) fileMaxSize * Constants.BYTES_PER_MB,
Integer.MAX_VALUE);
+ }
+
private ServerHttpRequestDecorator decorate(final ServerWebExchange
exchange,
final CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
diff --git
a/shenyu-web/src/test/java/org/apache/shenyu/web/filter/FileSizeFilterTest.java
b/shenyu-web/src/test/java/org/apache/shenyu/web/filter/FileSizeFilterTest.java
index 70a6b045f8..19de8aa860 100644
---
a/shenyu-web/src/test/java/org/apache/shenyu/web/filter/FileSizeFilterTest.java
+++
b/shenyu-web/src/test/java/org/apache/shenyu/web/filter/FileSizeFilterTest.java
@@ -24,6 +24,9 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
@@ -31,6 +34,7 @@ import
org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -39,6 +43,8 @@ import java.lang.reflect.Method;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
@@ -117,4 +123,33 @@ public final class FileSizeFilterTest {
ServerHttpRequestDecorator decorator = (ServerHttpRequestDecorator)
declaredMethod.invoke(fileSizeFilterError, webExchangeTextPlain,
cachedBodyOutputMessage);
Assertions.assertEquals(decorator.getBody(),
cachedBodyOutputMessage.getBody());
}
+
+ @Test
+ public void testFilterRejectsOversizedBodyWhileBuffering() {
+ final int fileMaxSize = 1;
+ final int bufferSize = 512 * 1024;
+ final int bufferCount = 4;
+ final int[] consumedBuffers = {0};
+ final Flux<DataBuffer> body = Flux.range(0, bufferCount)
+ .map(index -> (DataBuffer) new
DefaultDataBufferFactory().allocateBuffer(bufferSize).write(new
byte[bufferSize]))
+ .doOnNext(dataBuffer -> consumedBuffers[0]++);
+ ServerWebExchange webExchange =
MockServerWebExchange.from(MockServerHttpRequest
+ .post("http://localhost:8080")
+ .contentType(MediaType.MULTIPART_FORM_DATA)
+ .contentLength((long) bufferSize * bufferCount)
+ .body(body));
+
+ WebFilterChain webFilterChain = mock(WebFilterChain.class);
+ when(webFilterChain.filter(any())).thenReturn(Mono.empty());
+
+ FileSizeFilter fileSizeFilter = new FileSizeFilter(fileMaxSize);
+ Mono<Void> voidMono = fileSizeFilter.filter(webExchange,
webFilterChain);
+ StepVerifier.create(voidMono).expectSubscription().verifyComplete();
+
+ Assertions.assertEquals(HttpStatus.BAD_REQUEST,
webExchange.getResponse().getStatusCode());
+ // the body must not be buffered in full: the codec aborts as soon as
the limit is exceeded
+ Assertions.assertTrue(consumedBuffers[0] < bufferCount,
+ "expected the oversized body to be rejected before being
buffered in full, consumed buffers: " + consumedBuffers[0]);
+ verify(webFilterChain, never()).filter(any());
+ }
}