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 d409a0429a fix(admin): limit swagger import response body size (#6411)
d409a0429a is described below

commit d409a0429a9657b55474dd91014af7e989d04858
Author: SouthwestAsiaFloat <[email protected]>
AuthorDate: Sun Jul 5 22:51:13 2026 +0800

    fix(admin): limit swagger import response body size (#6411)
    
    * fix(admin): limit swagger import response body size
    
    * fix:change to ShenyuAdminResult
    
    ---------
    
    Co-authored-by: 西南亚瓢 <[email protected]>
    Co-authored-by: aias00 <[email protected]>
---
 .../admin/controller/SwaggerImportController.java  |   9 +
 .../service/impl/SwaggerImportServiceImpl.java     |  65 ++++++-
 shenyu-admin/src/main/resources/application.yml    |   2 +
 .../controller/SwaggerImportControllerTest.java    |  89 +++++++++
 .../service/impl/SwaggerImportServiceImplTest.java | 210 +++++++++++++++++++++
 5 files changed, 373 insertions(+), 2 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/SwaggerImportController.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/SwaggerImportController.java
index 96548a0b15..d6dd5ee41f 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/SwaggerImportController.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/SwaggerImportController.java
@@ -23,6 +23,7 @@ import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
 import org.apache.shenyu.admin.service.SwaggerImportService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -60,6 +61,10 @@ public class SwaggerImportController {
             String result = swaggerImportService.importSwagger(request);
             return ShenyuAdminResult.success(result);
 
+        } catch (IllegalArgumentException e) {
+            // Oversized Swagger bodies and other invalid import inputs are 
client request errors.
+            LOG.error("Failed to import swagger document", e);
+            return ShenyuAdminResult.error(HttpStatus.BAD_REQUEST.value(), 
"Import failed: " + e.getMessage());
         } catch (Exception e) {
             LOG.error("Failed to import swagger document", e);
 
@@ -79,6 +84,10 @@ public class SwaggerImportController {
         try {
             String result = swaggerImportService.importMcpConfig(request);
             return ShenyuAdminResult.success(result);
+        } catch (IllegalArgumentException e) {
+            // Oversized Swagger bodies and other invalid import inputs are 
client request errors.
+            LOG.error("Failed to import mcp server config", e);
+            return ShenyuAdminResult.error(HttpStatus.BAD_REQUEST.value(), 
"Import failed: " + e.getMessage());
         } catch (Exception e) {
             LOG.error("Failed to import mcp server config", e);
             return ShenyuAdminResult.error("Import failed" + e.getMessage());
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 d4ff363225..95b79582e3 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,6 +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;
@@ -42,12 +43,17 @@ import 
org.apache.shenyu.register.common.dto.McpToolsRegisterDTO;
 import org.apache.shenyu.register.common.dto.MetaDataRegisterDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+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;
@@ -65,10 +71,18 @@ 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;
+
     @Resource
     private ShenyuClientRegisterMcpServiceImpl shenyuClientRegisterMcpService;
 
@@ -102,6 +116,10 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
             
             return "Import successful, supports Swagger 2.0 and OpenAPI 3.0 
formats";
             
+        } catch (IllegalArgumentException e) {
+            // Keep bad user input unwrapped so the controller can return HTTP 
400.
+            LOG.error("Failed to import swagger document: {}", 
request.getProjectName(), e);
+            throw e;
         } catch (Exception e) {
             LOG.error("Failed to import swagger document: {}", 
request.getProjectName(), e);
             throw new RuntimeException("Import failed: " + e.getMessage(), e);
@@ -124,6 +142,10 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
             });
 
             return "Import mcp server config successful, supports Swagger 2.0 
and OpenAPI 3.0 formats";
+        } catch (IllegalArgumentException e) {
+            // Keep bad user input unwrapped so the controller can return HTTP 
400.
+            LOG.error("Failed to import mcp config: {}", 
request.getProjectName(), e);
+            throw e;
         } catch (IOException e) {
             LOG.error("Failed to import mcp config: {}", 
request.getProjectName(), e);
             throw new RuntimeException("Import mcp server config failed: " + 
e.getMessage(), e);
@@ -252,9 +274,48 @@ public class SwaggerImportServiceImpl implements 
SwaggerImportService {
             if (response.code() != 200) {
                 throw new RuntimeException("Failed to get Swagger document, 
HTTP status code: " + response.code());
             }
-            
-            return response.body().string();
+
+            return 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) {
diff --git a/shenyu-admin/src/main/resources/application.yml 
b/shenyu-admin/src/main/resources/application.yml
index 6d2c3af3bf..0e986c0c0a 100755
--- a/shenyu-admin/src/main/resources/application.yml
+++ b/shenyu-admin/src/main/resources/application.yml
@@ -193,6 +193,8 @@ shenyu:
       - /v3/api-docs/**
       - /csrf
       - /alert/report
+  swagger:
+    max-body-size: 10485760
   dashboard:
     core:
       onlySuperAdminPermission:
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/SwaggerImportControllerTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/SwaggerImportControllerTest.java
new file mode 100644
index 0000000000..5ac14b613d
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/SwaggerImportControllerTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.controller;
+
+import org.apache.shenyu.admin.model.dto.SwaggerImportRequest;
+import org.apache.shenyu.admin.service.SwaggerImportService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.hamcrest.core.Is.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Test for {@link SwaggerImportController}.
+ */
+@ExtendWith(MockitoExtension.class)
+public class SwaggerImportControllerTest {
+
+    private static final String IMPORT_REQUEST = "{"
+            + "\"swaggerUrl\":\"https://8.8.8.8/swagger.json\",";
+            + "\"projectName\":\"test\""
+            + "}";
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private SwaggerImportController swaggerImportController;
+
+    @Mock
+    private SwaggerImportService swaggerImportService;
+
+    @BeforeEach
+    public void setUp() {
+        mockMvc = 
MockMvcBuilders.standaloneSetup(swaggerImportController).build();
+    }
+
+    @Test
+    public void importSwaggerShouldReturnBadRequestCodeWhenBodyExceedsLimit() 
throws Exception {
+        
when(swaggerImportService.importSwagger(any(SwaggerImportRequest.class)))
+                .thenThrow(new IllegalArgumentException(
+                        "Swagger document response body exceeds maximum size 
of 10 bytes"));
+
+        mockMvc.perform(MockMvcRequestBuilders.post("/swagger/import")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(IMPORT_REQUEST))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", 
is(HttpStatus.BAD_REQUEST.value())));
+    }
+
+    @Test
+    public void 
importMcpConfigShouldReturnBadRequestCodeWhenBodyExceedsLimit() throws 
Exception {
+        
when(swaggerImportService.importMcpConfig(any(SwaggerImportRequest.class)))
+                .thenThrow(new IllegalArgumentException(
+                        "Swagger document response body exceeds maximum size 
of 10 bytes"));
+
+        mockMvc.perform(MockMvcRequestBuilders.post("/swagger/import/mcp")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(IMPORT_REQUEST))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code", 
is(HttpStatus.BAD_REQUEST.value())));
+    }
+}
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
new file mode 100644
index 0000000000..6c02347175
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service.impl;
+
+import okhttp3.MediaType;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okio.Buffer;
+import okio.BufferedSource;
+import org.apache.shenyu.admin.model.bean.DocInfo;
+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.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test for {@link SwaggerImportServiceImpl}.
+ */
+public class SwaggerImportServiceImplTest {
+
+    private static final String SWAGGER_URL = "https://8.8.8.8/swagger.json";;
+
+    private static final String SWAGGER_JSON = 
"{\"swagger\":\"2.0\",\"info\":{\"title\":\"test\",\"version\":\"1.0\"},\"paths\":{}}";
+
+    private static final MediaType JSON_UTF_8 = 
MediaType.parse("application/json; charset=utf-8");
+
+    private RecordingDocManager docManager;
+
+    private StubHttpUtils httpUtils;
+
+    @BeforeEach
+    public void setUp() {
+        docManager = new RecordingDocManager();
+        httpUtils = new StubHttpUtils();
+    }
+
+    @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)));
+
+        String result = service.importSwagger(request());
+
+        assertEquals("Import successful, supports Swagger 2.0 and OpenAPI 3.0 
formats", result);
+        assertEquals(SWAGGER_JSON, docManager.getDocJson());
+    }
+
+    @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)));
+
+        assertThrows(IllegalArgumentException.class, () -> 
service.importSwagger(request()));
+    }
+
+    @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);
+    }
+
+    private SwaggerImportRequest request() {
+        SwaggerImportRequest request = new SwaggerImportRequest();
+        request.setSwaggerUrl(SWAGGER_URL);
+        request.setProjectName("test");
+        return request;
+    }
+
+    private static Response response(final ResponseBody body) {
+        return new Response.Builder()
+                .request(new Request.Builder().url(SWAGGER_URL).build())
+                .protocol(Protocol.HTTP_1_1)
+                .code(200)
+                .message("OK")
+                .body(body)
+                .build();
+    }
+
+    private static ResponseBody responseBody(final String body, final long 
contentLength, final MediaType mediaType) {
+        return responseBody(body, contentLength, mediaType, 
StandardCharsets.UTF_8);
+    }
+
+    private static ResponseBody responseBody(final String body, final long 
contentLength, final MediaType mediaType,
+                                             final Charset charset) {
+        return responseBody(body.getBytes(charset), contentLength, mediaType);
+    }
+
+    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);
+            }
+        };
+    }
+
+    private static final class RecordingDocManager implements DocManager {
+
+        private String docJson;
+
+        @Override
+        public void addDocInfo(final UpstreamInstance instance, final String 
docJson, final String oldMd5,
+                               final Consumer<DocInfo> callback) {
+            this.docJson = docJson;
+        }
+
+        @Override
+        public Collection<DocInfo> listAll() {
+            return Collections.emptyList();
+        }
+
+        private String getDocJson() {
+            return docJson;
+        }
+    }
+
+    private static final class StubHttpUtils extends HttpUtils {
+
+        private Response response;
+
+        private void setResponse(final Response response) {
+            this.response = response;
+        }
+
+        @Override
+        public Response requestForResponse(final String url, final Map<String, 
?> form,
+                                           final Map<String, String> header, 
final HTTPMethod method,
+                                           final boolean followRedirects) {
+            return response;
+        }
+    }
+}

Reply via email to