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