This is an automated email from the ASF dual-hosted git repository.

dengliming 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 f9a67c9516 fix(admin): enforce response body size limit on automatic 
swagger doc pull (#6458)
f9a67c9516 is described below

commit f9a67c95164da6796ba8f32a084d03252922400b
Author: wy471x <[email protected]>
AuthorDate: Thu Jul 30 22:59:57 2026 +0800

    fix(admin): enforce response body size limit on automatic swagger doc pull 
(#6458)
    
    Co-authored-by: aias00 <[email protected]>
    Co-authored-by: xiaoyu <[email protected]>
---
 .../service/impl/SwaggerImportServiceImpl.java     |  54 +----------
 .../manager/impl/PullSwaggerDocServiceImpl.java    |   6 +-
 .../org/apache/shenyu/admin/utils/HttpUtils.java   |  49 ++++++++++
 .../service/impl/SwaggerImportServiceImplTest.java |  91 ++----------------
 .../apache/shenyu/admin/utils/HttpUtilsTest.java   | 106 +++++++++++++++++++++
 5 files changed, 170 insertions(+), 136 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
index e2965b2c9c..62846158ed 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java
@@ -26,7 +26,7 @@ import io.swagger.v3.oas.models.Paths;
 import io.swagger.v3.oas.models.parameters.Parameter;
 import io.swagger.v3.parser.OpenAPIV3Parser;
 import okhttp3.Response;
-import okhttp3.ResponseBody;
+
 import org.apache.shenyu.admin.model.bean.UpstreamInstance;
 import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
 import org.apache.shenyu.admin.service.SwaggerImportService;
@@ -47,13 +47,9 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -71,17 +67,12 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
     
     private static final Logger LOG = 
LoggerFactory.getLogger(SwaggerImportServiceImpl.class);
 
-    private static final int READ_BUFFER_SIZE = 8192;
-
-    private static final long DEFAULT_MAX_SWAGGER_BODY_SIZE = 10L * 1024 * 
1024;
-
     private final DocManager docManager;
-    
+
     private final HttpUtils httpUtils;
 
-    // Default is 10 MB and can be overridden by shenyu.swagger.max-body-size.
     @Value("${shenyu.swagger.max-body-size:10485760}")
-    private long maxSwaggerBodySize = DEFAULT_MAX_SWAGGER_BODY_SIZE;
+    private long maxSwaggerBodySize;
 
     @Resource
     private ShenyuClientRegisterMcpServiceImpl shenyuClientRegisterMcpService;
@@ -281,49 +272,12 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
                 throw new RuntimeException("Failed to get Swagger document, 
HTTP status code: " + response.code());
             }
 
-            return readLimitedResponseBody(response.body(), 
maxSwaggerBodySize);
+            return HttpUtils.readLimitedResponseBody(response.body(), 
maxSwaggerBodySize);
         }
     }
 
 
-    private String readLimitedResponseBody(final ResponseBody responseBody, 
final long maxBodySize) throws IOException {
-        if (Objects.isNull(responseBody)) {
-            throw new IllegalArgumentException("Swagger document response body 
is empty");
-        }
-        if (maxBodySize < 0) {
-            throw new IllegalArgumentException("Max Swagger response body size 
must not be negative");
-        }
-
-        long contentLength = responseBody.contentLength();
-        // Reject early when the server declares a body larger than the 
configured limit.
-        if (contentLength > maxBodySize) {
-            throw new IllegalArgumentException(String.format(
-                    "Swagger document response body exceeds maximum size of %d 
bytes", maxBodySize));
-        }
-
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        byte[] buffer = new byte[READ_BUFFER_SIZE];
-        long totalBytes = 0;
-        try (InputStream inputStream = responseBody.byteStream()) {
-            int bytesRead;
-            while ((bytesRead = inputStream.read(buffer)) != -1) {
-                totalBytes += bytesRead;
-                // Content-Length can be missing or wrong, so enforce the 
limit while streaming.
-                if (totalBytes > maxBodySize) {
-                    throw new IllegalArgumentException(String.format(
-                            "Swagger document response body exceeds maximum 
size of %d bytes", maxBodySize));
-                }
-                outputStream.write(buffer, 0, bytesRead);
-            }
-        }
 
-        // Preserve the server-declared charset; default to UTF-8 when it is 
absent.
-        Charset charset = Objects.isNull(responseBody.contentType())
-                ? StandardCharsets.UTF_8
-                : responseBody.contentType().charset(StandardCharsets.UTF_8);
-        return outputStream.toString(charset.name());
-    }
-    
     private void validateSwaggerContent(final String swaggerJson) {
         try {
             JsonObject docRoot = GsonUtils.getInstance().fromJson(swaggerJson, 
JsonObject.class);
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
index f71db31bc3..bbdb392fa5 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/PullSwaggerDocServiceImpl.java
@@ -36,6 +36,7 @@ import org.apache.shenyu.common.constant.AdminConstants;
 import org.apache.shenyu.common.utils.GsonUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.web.util.UriComponentsBuilder;
 
@@ -62,6 +63,9 @@ public class PullSwaggerDocServiceImpl implements 
PullSwaggerDocService {
 
     private static final long DOC_LOCK_EXPIRED_TIME = 60 * 1000;
 
+    @Value("${shenyu.swagger.max-body-size:10485760}")
+    private long maxSwaggerBodySize;
+
     private final Interner<Object> interner = Interners.newWeakInterner();
 
     @Resource
@@ -102,7 +106,7 @@ public class PullSwaggerDocServiceImpl implements 
PullSwaggerDocService {
             if (response.code() != HttpStatus.SC_OK) {
                 throw new IOException(response.toString());
             }
-            final String body = response.body().string();
+            final String body = 
HttpUtils.readLimitedResponseBody(response.body(), maxSwaggerBodySize);
             docManager.addDocInfo(
                 instance,
                 body,
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java
index c8bbf0b637..977b7ec9a7 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java
@@ -43,6 +43,7 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Serializable;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.ArrayList;
@@ -63,6 +64,8 @@ public class HttpUtils {
 
     private static final MediaType MEDIA_TYPE_JSON = 
MediaType.parse("application/json; charset=utf-8");
 
+    private static final int READ_BUFFER_SIZE = 8192;
+
     private Map<String, List<Cookie>> cookieStore = new HashMap<>();
 
     private OkHttpClient httpClient;
@@ -460,6 +463,52 @@ public class HttpUtils {
         }
     }
 
+    /**
+     * Read response body with a size limit to prevent excessive memory usage.
+     *
+     * @param responseBody the response body to read
+     * @param maxBodySize  maximum allowed body size in bytes
+     * @return the response body as a string
+     * @throws IOException              if an I/O error occurs
+     * @throws IllegalArgumentException if the body exceeds maxBodySize
+     */
+    public static String readLimitedResponseBody(final ResponseBody 
responseBody, final long maxBodySize) throws IOException {
+        if (Objects.isNull(responseBody)) {
+            throw new IllegalArgumentException("Response body is empty");
+        }
+        if (maxBodySize < 0) {
+            throw new IllegalArgumentException("Max response body size must 
not be negative");
+        }
+
+        long contentLength = responseBody.contentLength();
+        if (contentLength > maxBodySize) {
+            throw new IllegalArgumentException(String.format(
+                    "Response body exceeds maximum size of %d bytes", 
maxBodySize));
+        }
+
+        ByteArrayOutputStream outputStream = contentLength > 0
+                ? new ByteArrayOutputStream((int) Math.min(contentLength, 
Integer.MAX_VALUE))
+                : new ByteArrayOutputStream();
+        byte[] buffer = new byte[READ_BUFFER_SIZE];
+        long totalBytes = 0;
+        try (InputStream inputStream = responseBody.byteStream()) {
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                totalBytes += bytesRead;
+                if (totalBytes > maxBodySize) {
+                    throw new IllegalArgumentException(String.format(
+                            "Response body exceeds maximum size of %d bytes", 
maxBodySize));
+                }
+                outputStream.write(buffer, 0, bytesRead);
+            }
+        }
+
+        Charset charset = Objects.isNull(responseBody.contentType())
+                ? StandardCharsets.UTF_8
+                : responseBody.contentType().charset(StandardCharsets.UTF_8);
+        return outputStream.toString(charset.name());
+    }
+
     public static class HttpToolConfig {
 
         private int connectTimeoutSeconds = 10;
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
index 86a43ac42e..231c01aa8a 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
@@ -17,10 +17,6 @@
 
 package org.apache.shenyu.admin.service.impl;
 
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.Operation;
-import io.swagger.v3.oas.models.info.Info;
-import io.swagger.v3.oas.models.servers.Server;
 import okhttp3.MediaType;
 import okhttp3.Protocol;
 import okhttp3.Request;
@@ -33,8 +29,6 @@ import org.apache.shenyu.admin.model.bean.UpstreamInstance;
 import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
 import org.apache.shenyu.admin.service.manager.DocManager;
 import org.apache.shenyu.admin.utils.HttpUtils;
-import org.apache.shenyu.client.mcp.common.dto.ShenyuMcpTool;
-import org.apache.shenyu.register.common.dto.MetaDataRegisterDTO;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.test.util.ReflectionTestUtils;
@@ -44,12 +38,10 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
@@ -63,19 +55,24 @@ public class SwaggerImportServiceImplTest {
 
     private static final MediaType JSON_UTF_8 = 
MediaType.parse("application/json; charset=utf-8");
 
+    private static final long DEFAULT_MAX_SWAGGER_BODY_SIZE = 10L * 1024 * 
1024;
+
     private RecordingDocManager docManager;
 
     private StubHttpUtils httpUtils;
 
+    private SwaggerImportServiceImpl service;
+
     @BeforeEach
     public void setUp() {
         docManager = new RecordingDocManager();
         httpUtils = new StubHttpUtils();
+        service = new SwaggerImportServiceImpl(docManager, httpUtils);
+        ReflectionTestUtils.setField(service, "maxSwaggerBodySize", 
DEFAULT_MAX_SWAGGER_BODY_SIZE);
     }
 
     @Test
     public void importSwaggerShouldReadSmallSwaggerBody() throws IOException {
-        SwaggerImportServiceImpl service = new 
SwaggerImportServiceImpl(docManager, httpUtils);
         httpUtils.setResponse(response(responseBody(
                 SWAGGER_JSON, 
SWAGGER_JSON.getBytes(StandardCharsets.UTF_8).length, JSON_UTF_8)));
 
@@ -87,7 +84,6 @@ public class SwaggerImportServiceImplTest {
 
     @Test
     public void importSwaggerShouldRejectKnownContentLengthGreaterThanLimit() 
throws IOException {
-        SwaggerImportServiceImpl service = new 
SwaggerImportServiceImpl(docManager, httpUtils);
         ReflectionTestUtils.setField(service, "maxSwaggerBodySize", 10L);
         httpUtils.setResponse(response(responseBody("small", 11L, 
JSON_UTF_8)));
 
@@ -96,87 +92,12 @@ public class SwaggerImportServiceImplTest {
 
     @Test
     public void 
importSwaggerShouldRejectUnknownContentLengthWhenActualBodyExceedsLimit() 
throws IOException {
-        SwaggerImportServiceImpl service = new 
SwaggerImportServiceImpl(docManager, httpUtils);
         ReflectionTestUtils.setField(service, "maxSwaggerBodySize", 10L);
         httpUtils.setResponse(response(responseBody("01234567890", -1L, 
JSON_UTF_8)));
 
         assertThrows(IllegalArgumentException.class, () -> 
service.importSwagger(request()));
     }
 
-    @Test
-    public void readLimitedResponseBodyShouldAllowBodyExactlyEqualToLimit() 
throws IOException {
-        String body = "0123456789";
-
-        String result = ReflectionTestUtils.invokeMethod(new 
SwaggerImportServiceImpl(docManager, httpUtils),
-                "readLimitedResponseBody", responseBody(body, -1L, 
JSON_UTF_8), 10L);
-
-        assertEquals(body, result);
-    }
-
-    @Test
-    public void readLimitedResponseBodyShouldHandleNullBodySafely() {
-        SwaggerImportServiceImpl service = new 
SwaggerImportServiceImpl(docManager, httpUtils);
-
-        assertThrows(IllegalArgumentException.class, () ->
-                ReflectionTestUtils.invokeMethod(service, 
"readLimitedResponseBody", null, 10L));
-    }
-
-    @Test
-    public void readLimitedResponseBodyShouldUseResponseCharset() throws 
IOException {
-        Charset charset = StandardCharsets.ISO_8859_1;
-        byte[] bytes = new byte[] {'c', 'a', 'f', (byte) 0xE9};
-        String body = new String(bytes, charset);
-        ResponseBody responseBody = responseBody(bytes, -1L, 
MediaType.parse("text/plain; charset=iso-8859-1"));
-
-        String result = ReflectionTestUtils.invokeMethod(new 
SwaggerImportServiceImpl(docManager, httpUtils),
-                "readLimitedResponseBody", responseBody, 10L);
-
-        assertEquals(body, result);
-    }
-
-    @Test
-    public void buildMetaDataRegisterDTOShouldThrowWhenServersIsNull() {
-        SwaggerImportServiceImpl service = new 
SwaggerImportServiceImpl(docManager, httpUtils);
-        OpenAPI openapi = new OpenAPI()
-                .info(new Info().title("test"));
-
-        assertThrows(IllegalArgumentException.class, () ->
-                ReflectionTestUtils.invokeMethod(service, 
"buildMetaDataRegisterDTO",
-                        openapi, "test", new ShenyuMcpTool(), "/ping", "ns1"));
-    }
-
-    @Test
-    public void buildMetaDataRegisterDTOShouldThrowWhenServersIsEmpty() {
-        SwaggerImportServiceImpl service = new 
SwaggerImportServiceImpl(docManager, httpUtils);
-        OpenAPI openapi = new OpenAPI()
-                .info(new Info().title("test"))
-                .servers(Collections.emptyList());
-
-        assertThrows(IllegalArgumentException.class, () ->
-                ReflectionTestUtils.invokeMethod(service, 
"buildMetaDataRegisterDTO",
-                        openapi, "test", new ShenyuMcpTool(), "/ping", "ns1"));
-    }
-
-    @Test
-    public void buildMetaDataRegisterDTOShouldHandleNullParameters() {
-        SwaggerImportServiceImpl service = new 
SwaggerImportServiceImpl(docManager, httpUtils);
-        OpenAPI openapi = new OpenAPI()
-                .info(new Info().title("test"))
-                .servers(List.of(new Server().url("http://localhost:8080";)));
-        Operation operation = new Operation()
-                .operationId("ping")
-                .parameters(null);
-        ShenyuMcpTool tool = new ShenyuMcpTool();
-        tool.setOperation(operation);
-        tool.setToolName("ping");
-
-        MetaDataRegisterDTO result = ReflectionTestUtils.invokeMethod(service, 
"buildMetaDataRegisterDTO",
-                openapi, "test", tool, "/ping", "ns1");
-
-        assertNotNull(result);
-        assertEquals("", result.getParameterTypes());
-    }
-
     private SwaggerImportRequest request() {
         SwaggerImportRequest request = new SwaggerImportRequest();
         request.setSwaggerUrl(SWAGGER_URL);
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java
index a70870ae33..07f5634dde 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java
@@ -23,6 +23,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.InetSocketAddress;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -30,8 +31,12 @@ import java.util.List;
 import java.util.Map;
 import okhttp3.FormBody;
 import okhttp3.HttpUrl;
+import okhttp3.MediaType;
 import okhttp3.Request;
 import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okio.Buffer;
+import okio.BufferedSource;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.jupiter.api.Assertions;
@@ -420,6 +425,107 @@ public class HttpUtilsTest {
         }
     }
 
+    @Test
+    public void readLimitedResponseBodyShouldReadWithinLimit() throws 
IOException {
+        String body = "hello";
+        ResponseBody responseBody = 
responseBody(body.getBytes(StandardCharsets.UTF_8),
+                body.getBytes(StandardCharsets.UTF_8).length,
+                MediaType.parse("application/json; charset=utf-8"));
+
+        String result = HttpUtils.readLimitedResponseBody(responseBody, 1024);
+
+        Assert.assertEquals(body, result);
+    }
+
+    @Test
+    public void readLimitedResponseBodyShouldAllowBodyExactlyEqualToLimit() 
throws IOException {
+        String body = "0123456789";
+
+        String result = HttpUtils.readLimitedResponseBody(
+                responseBody(body.getBytes(StandardCharsets.UTF_8), -1L,
+                        MediaType.parse("text/plain")), 10L);
+
+        Assert.assertEquals(body, result);
+    }
+
+    @Test
+    public void 
readLimitedResponseBodyShouldRejectContentLengthGreaterThanLimit() {
+        ResponseBody responseBody = 
responseBody("small".getBytes(StandardCharsets.UTF_8), 11L,
+                MediaType.parse("application/json"));
+
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> HttpUtils.readLimitedResponseBody(responseBody, 10L));
+    }
+
+    @Test
+    public void readLimitedResponseBodyShouldRejectActualBodyExceedingLimit() {
+        ResponseBody responseBody = 
responseBody("01234567890".getBytes(StandardCharsets.UTF_8), -1L,
+                MediaType.parse("application/json"));
+
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> HttpUtils.readLimitedResponseBody(responseBody, 10L));
+    }
+
+    @Test
+    public void readLimitedResponseBodyShouldThrowOnNullBody() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> HttpUtils.readLimitedResponseBody(null, 1024));
+    }
+
+    @Test
+    public void readLimitedResponseBodyShouldThrowOnNegativeMaxSize() {
+        ResponseBody responseBody = 
responseBody("test".getBytes(StandardCharsets.UTF_8), 4L,
+                MediaType.parse("text/plain"));
+
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> HttpUtils.readLimitedResponseBody(responseBody, -1));
+    }
+
+    @Test
+    public void readLimitedResponseBodyShouldUseResponseCharset() throws 
IOException {
+        Charset charset = StandardCharsets.ISO_8859_1;
+        byte[] bytes = new byte[] {'c', 'a', 'f', (byte) 0xE9};
+        String expected = new String(bytes, charset);
+        ResponseBody responseBody = responseBody(bytes, -1L,
+                MediaType.parse("text/plain; charset=iso-8859-1"));
+
+        String result = HttpUtils.readLimitedResponseBody(responseBody, 100);
+
+        Assert.assertEquals(expected, result);
+    }
+
+    @Test
+    public void readLimitedResponseBodyShouldDefaultToUtf8WhenCharsetMissing() 
throws IOException {
+        String body = "hello";
+        ResponseBody responseBody = 
responseBody(body.getBytes(StandardCharsets.UTF_8),
+                body.getBytes(StandardCharsets.UTF_8).length,
+                MediaType.parse("application/json"));
+
+        String result = HttpUtils.readLimitedResponseBody(responseBody, 1024);
+
+        Assert.assertEquals(body, result);
+    }
+
+    private static ResponseBody responseBody(final byte[] bytes, final long 
contentLength,
+                                             final MediaType mediaType) {
+        return new ResponseBody() {
+            @Override
+            public MediaType contentType() {
+                return mediaType;
+            }
+
+            @Override
+            public long contentLength() {
+                return contentLength;
+            }
+
+            @Override
+            public BufferedSource source() {
+                return new Buffer().write(bytes);
+            }
+        };
+    }
+
     @Test
     public void downloadFileMethodTest() throws IOException {
         HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);

Reply via email to