Skip to content

Commit 0e1cc3c

Browse files
Aias00moremind
andauthored
Block redirect-based SSRF bypass in Swagger imports (#6320)
* goalx: snapshot before shenyu-analysis * Block redirect-based SSRF bypass in Swagger imports Swagger import requests were validating only the initial URL while the shared OkHttp client silently followed redirects. Disabling redirect following at the HTTP utility layer prevents callers from reaching unvalidated internal targets through 3xx responses. Constraint: Keep the fix in the shared HTTP client so all callers inherit the safer default Rejected: Add Swagger-specific redirect validation only | leaves other HttpUtils callers exposed to the same redirect class Confidence: high Scope-risk: narrow Reversibility: clean Directive: Do not re-enable redirect following without validating every redirect target before the request is sent Tested: mvn -pl shenyu-admin -Dtest=HttpUtilsTest,SwaggerImportServiceTest test Not-tested: Full shenyu-admin test suite and end-to-end Swagger import against live remote servers * Scope redirect blocking to Swagger import requests This follow-up preserves the default redirect-following behavior of the shared HttpUtils client and moves no-redirect handling onto the Swagger import path only. The regression tests now cover both the explicit no-redirect mode and the original default behavior. Constraint: Avoid changing shared HTTP client semantics for unrelated admin features Confidence: high Scope-risk: narrow Reversibility: clean Directive: SSRF hardening for one feature should not silently redefine shared client behavior without an explicit compatibility review Tested: mvn -pl shenyu-admin -Dtest=HttpUtilsTest,SwaggerImportServiceTest test Not-tested: Full shenyu-admin test suite * Replace redirect test literals with named constants This follow-up removes repeated path, status, and body literals from the HttpUtils redirect tests so the cases read in terms of the HTTP behavior being verified instead of raw values. Constraint: Keep the cleanup test-only and local to the redirect SSRF fix Confidence: high Scope-risk: narrow Reversibility: clean Directive: Name repeated HTTP semantics in tests when they describe protocol behavior rather than incidental values Tested: mvn -pl shenyu-admin -Dtest=HttpUtilsTest,SwaggerImportServiceTest test Not-tested: Full shenyu-admin test suite * Fix redirect test URL construction The no-redirect regression test was composing redirect URLs from a base string that already ended with a slash, which produced invalid host/port combinations in CI. This follow-up switches the test to a dedicated host prefix constant so the dynamically allocated port is embedded correctly. Constraint: Keep the fix limited to the test harness; production redirect handling is unchanged Confidence: high Scope-risk: narrow Reversibility: clean Directive: Avoid composing dynamic host:port URLs from request path fixtures that carry trailing slash semantics Tested: mvn -pl shenyu-admin -Dtest=HttpUtilsTest,SwaggerImportServiceTest test Not-tested: Full shenyu-admin test suite --------- Co-authored-by: moremind <hefengen@apache.org>
1 parent aceaf5b commit 0e1cc3c

3 files changed

Lines changed: 101 additions & 3 deletions

File tree

shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ public boolean testConnection(final String swaggerUrl) {
231231
try {
232232
validateSwaggerUrl(swaggerUrl);
233233
try (Response response = httpUtils.requestForResponse(swaggerUrl,
234-
Collections.emptyMap(), Collections.emptyMap(), HttpUtils.HTTPMethod.GET)) {
234+
Collections.emptyMap(), Collections.emptyMap(), HttpUtils.HTTPMethod.GET, false)) {
235235
return response.code() == 200;
236236
}
237237
} catch (Exception e) {
@@ -247,7 +247,7 @@ private void validateSwaggerUrl(final String swaggerUrl) {
247247

248248
private String fetchSwaggerDoc(final String swaggerUrl) throws IOException {
249249
try (Response response = httpUtils.requestForResponse(swaggerUrl,
250-
Collections.emptyMap(), Collections.emptyMap(), HttpUtils.HTTPMethod.GET)) {
250+
Collections.emptyMap(), Collections.emptyMap(), HttpUtils.HTTPMethod.GET, false)) {
251251

252252
if (response.code() != 200) {
253253
throw new RuntimeException("Failed to get Swagger document, HTTP status code: " + response.code());

shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/HttpUtils.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,30 @@ public Response requestCall(final String url, final Map<String, ?> form, final M
325325
*/
326326
public Response requestForResponse(final String url, final Map<String, ?> form, final Map<String, String> header,
327327
final HTTPMethod method) throws IOException {
328+
return requestForResponse(url, form, header, method, true);
329+
}
330+
331+
/**
332+
* request data with configurable redirect handling.
333+
*
334+
* @param url request url
335+
* @param form request data
336+
* @param header header
337+
* @param method method
338+
* @param followRedirects whether redirects should be followed
339+
* @return Response
340+
* @throws IOException IOException
341+
*/
342+
public Response requestForResponse(final String url, final Map<String, ?> form, final Map<String, String> header,
343+
final HTTPMethod method, final boolean followRedirects) throws IOException {
328344
Request.Builder requestBuilder = buildRequestBuilder(url, form, method);
329345
addHeader(requestBuilder, header);
330346
Request request = requestBuilder.build();
331-
return httpClient
347+
OkHttpClient client = followRedirects ? httpClient : httpClient.newBuilder()
348+
.followRedirects(false)
349+
.followSslRedirects(false)
350+
.build();
351+
return client
332352
.newCall(request)
333353
.execute();
334354
}

shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717

1818
package org.apache.shenyu.admin.utils;
1919

20+
import com.sun.net.httpserver.HttpServer;
2021
import okhttp3.FormBody;
2122
import okhttp3.HttpUrl;
2223
import okhttp3.Request;
24+
import okhttp3.Response;
2325
import org.junit.Assert;
2426
import org.junit.Test;
2527
import org.junit.jupiter.api.Assertions;
28+
29+
import java.net.InetSocketAddress;
2630
import java.io.File;
2731
import java.io.IOException;
32+
import java.nio.charset.StandardCharsets;
2833
import java.util.HashMap;
2934
import java.util.Map;
3035

@@ -35,6 +40,18 @@ public class HttpUtilsTest {
3540

3641
private static final String TEST_URL = "http://127.0.0.1/";
3742

43+
private static final String LOCALHOST_BASE_URL = "http://127.0.0.1:";
44+
45+
private static final String REDIRECT_PATH = "/swagger.json";
46+
47+
private static final String TARGET_PATH = "/internal";
48+
49+
private static final String TARGET_BODY = "target";
50+
51+
private static final int HTTP_STATUS_OK = 200;
52+
53+
private static final int HTTP_STATUS_REDIRECT = 302;
54+
3855
private final Map<String, Object> formMap = new HashMap<>();
3956

4057
{
@@ -113,4 +130,65 @@ public void fileUtilsToBytesByFileNotExistsTest() {
113130
File file = new File("");
114131
Assertions.assertThrows(IOException.class, () -> HttpUtils.FileUtils.toBytes(file));
115132
}
133+
134+
@Test
135+
public void requestForResponseShouldNotFollowRedirectsWhenExplicitlyDisabled() throws IOException {
136+
HttpServer targetServer = HttpServer.create(new InetSocketAddress(0), 0);
137+
targetServer.createContext(TARGET_PATH, exchange -> {
138+
byte[] body = TARGET_BODY.getBytes(StandardCharsets.UTF_8);
139+
exchange.sendResponseHeaders(HTTP_STATUS_OK, body.length);
140+
exchange.getResponseBody().write(body);
141+
exchange.close();
142+
});
143+
targetServer.start();
144+
145+
HttpServer redirectServer = HttpServer.create(new InetSocketAddress(0), 0);
146+
String redirectLocation = LOCALHOST_BASE_URL + targetServer.getAddress().getPort() + TARGET_PATH;
147+
redirectServer.createContext(REDIRECT_PATH, exchange -> {
148+
exchange.getResponseHeaders().add("Location", redirectLocation);
149+
exchange.sendResponseHeaders(HTTP_STATUS_REDIRECT, -1);
150+
exchange.close();
151+
});
152+
redirectServer.start();
153+
154+
HttpUtils httpUtils = new HttpUtils();
155+
String redirectUrl = LOCALHOST_BASE_URL + redirectServer.getAddress().getPort() + REDIRECT_PATH;
156+
try (Response response = httpUtils.requestForResponse(redirectUrl, new HashMap<>(), new HashMap<>(), HttpUtils.HTTPMethod.GET, false)) {
157+
Assert.assertEquals(HTTP_STATUS_REDIRECT, response.code());
158+
Assert.assertEquals(redirectLocation, response.header("Location"));
159+
} finally {
160+
redirectServer.stop(0);
161+
targetServer.stop(0);
162+
}
163+
}
164+
165+
@Test
166+
public void requestForResponseShouldFollowRedirectsByDefault() throws IOException {
167+
HttpServer targetServer = HttpServer.create(new InetSocketAddress(0), 0);
168+
targetServer.createContext(TARGET_PATH, exchange -> {
169+
byte[] body = TARGET_BODY.getBytes(StandardCharsets.UTF_8);
170+
exchange.sendResponseHeaders(HTTP_STATUS_OK, body.length);
171+
exchange.getResponseBody().write(body);
172+
exchange.close();
173+
});
174+
targetServer.start();
175+
176+
HttpServer redirectServer = HttpServer.create(new InetSocketAddress(0), 0);
177+
String redirectLocation = LOCALHOST_BASE_URL + targetServer.getAddress().getPort() + TARGET_PATH;
178+
redirectServer.createContext(REDIRECT_PATH, exchange -> {
179+
exchange.getResponseHeaders().add("Location", redirectLocation);
180+
exchange.sendResponseHeaders(HTTP_STATUS_REDIRECT, -1);
181+
exchange.close();
182+
});
183+
redirectServer.start();
184+
185+
HttpUtils httpUtils = new HttpUtils();
186+
String redirectUrl = LOCALHOST_BASE_URL + redirectServer.getAddress().getPort() + REDIRECT_PATH;
187+
try (Response response = httpUtils.requestForResponse(redirectUrl, new HashMap<>(), new HashMap<>(), HttpUtils.HTTPMethod.GET)) {
188+
Assert.assertEquals(HTTP_STATUS_OK, response.code());
189+
} finally {
190+
redirectServer.stop(0);
191+
targetServer.stop(0);
192+
}
193+
}
116194
}

0 commit comments

Comments
 (0)