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());
+    }
 }

Reply via email to