From efbadde4ed298c4b0aa74b916aff2d7b451a97b5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 26 Jul 2026 12:05:16 +0000 Subject: [PATCH] fix(security): harden GHSA-997r Cloudreve SSRF residual paths Disable redirect following on CE/Ce4 attacker-controlled requests and stop echoing upstream response bodies in client-facing JSON errors. Add assertPublicHost regression coverage for the advisory PoC hosts. Co-authored-by: qaiu --- .../src/main/java/cn/qaiu/parser/PanBase.java | 3 +- .../java/cn/qaiu/parser/impl/Ce4Tool.java | 5 ++- .../main/java/cn/qaiu/parser/impl/CeTool.java | 12 ++--- .../cn/qaiu/parser/AssertPublicHostTest.java | 44 +++++++++++++++++++ 4 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 parser/src/test/java/cn/qaiu/parser/AssertPublicHostTest.java diff --git a/parser/src/main/java/cn/qaiu/parser/PanBase.java b/parser/src/main/java/cn/qaiu/parser/PanBase.java index 8a9702f..5b230ff 100644 --- a/parser/src/main/java/cn/qaiu/parser/PanBase.java +++ b/parser/src/main/java/cn/qaiu/parser/PanBase.java @@ -382,9 +382,10 @@ public abstract class PanBase implements IPanTool, Closeable { log.error("响应gzip解压或JSON解析失败: {}", e.getMessage()); fail("响应gzip解压或JSON解析失败: {}", e.getMessage()); } else { + // 上游响应体可能来自内网探测目标,仅写日志,避免经 HTTP 500 回传给调用方 String bodyPreview = responseBodyPreview(res); log.error("解析失败: json格式异常: {}", bodyPreview); - fail("解析失败: json格式异常: {}", bodyPreview); + fail("解析失败: json格式异常"); } return JsonObject.of(); } diff --git a/parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java b/parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java index a481700..2763962 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java @@ -111,7 +111,8 @@ public class Ce4Tool extends PanBase { private void requestShareDetail(String baseUrl, String key, String pwd, String path) { String shareApiUrl = baseUrl + SHARE_API_PATH + key; - HttpRequest httpRequest = clientSession.getAbs(shareApiUrl); + // 禁止跟随重定向:防止公网 host 302 到内网/元数据绕过 assertPublicHost + HttpRequest httpRequest = clientNoRedirects.getAbs(shareApiUrl); if (pwd != null && !pwd.isEmpty()) { httpRequest.addQueryParam("password", pwd); } @@ -232,7 +233,7 @@ public class Ce4Tool extends PanBase { .put("uris", new JsonArray().add(filePath)) .put("download", true); - clientSession.postAbs(fileUrlApi) + clientNoRedirects.postAbs(fileUrlApi) .putHeader("Content-Type", "application/json") .sendJsonObject(requestBody) .onSuccess(res -> { diff --git a/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java b/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java index 204899e..205fec3 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java @@ -78,7 +78,8 @@ public class CeTool extends PanBase { private void tryV4Ping(String baseUrl, String key, String pwd) { String pingUrlV4 = baseUrl + PING_API_V4_PATH; - clientSession.getAbs(pingUrlV4).send().onSuccess(res -> { + // 禁止跟随重定向:assertPublicHost 只校验初始 host,自动 30x 会绕过 SSRF 防护 + clientNoRedirects.getAbs(pingUrlV4).send().onSuccess(res -> { if (res.statusCode() == 200) { try { JsonObject json = asJson(res); @@ -108,7 +109,7 @@ public class CeTool extends PanBase { private void tryV3Ping(String baseUrl, String key, String pwd) { String pingUrlV3 = baseUrl + PING_API_V3_PATH; - clientSession.getAbs(pingUrlV3).send().onSuccess(res -> { + clientNoRedirects.getAbs(pingUrlV3).send().onSuccess(res -> { if (res.statusCode() == 200) { try { JsonObject json = asJson(res); @@ -139,7 +140,7 @@ public class CeTool extends PanBase { */ private void verifyV3AndParse(String baseUrl, String key, String pwd) { String shareApiUrl = baseUrl + SHARE_API_PATH + key; - HttpRequest httpRequest = clientSession.getAbs(shareApiUrl); + HttpRequest httpRequest = clientNoRedirects.getAbs(shareApiUrl); if (pwd != null && !pwd.isEmpty()) { httpRequest.addQueryParam("password", pwd); } @@ -175,7 +176,7 @@ public class CeTool extends PanBase { */ private void tryV4ShareApi(String baseUrl, String key, String pwd) { String shareApiUrl = baseUrl + "/api/v4/share/info/" + key; - HttpRequest httpRequest = clientSession.getAbs(shareApiUrl); + HttpRequest httpRequest = clientNoRedirects.getAbs(shareApiUrl); if (pwd != null && !pwd.isEmpty()) { httpRequest.addQueryParam("password", pwd); } @@ -291,7 +292,8 @@ public class CeTool extends PanBase { } private void getDownURL(String shareApiUrl) { - clientSession.putAbs(shareApiUrl) + // PUT 默认不跟随重定向,但仍统一使用 no-redirect 客户端避免配置漂移 + clientNoRedirects.putAbs(shareApiUrl) .putHeader("Referer", shareLinkInfo.getShareUrl()) .send().onSuccess(res -> { JsonObject jsonObject = asJson(res); diff --git a/parser/src/test/java/cn/qaiu/parser/AssertPublicHostTest.java b/parser/src/test/java/cn/qaiu/parser/AssertPublicHostTest.java new file mode 100644 index 0000000..def1b9f --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/AssertPublicHostTest.java @@ -0,0 +1,44 @@ +package cn.qaiu.parser; + +import org.junit.Test; + +import java.io.IOException; +import java.net.URL; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * GHSA-997r-7xx2-p9x6 regression: Cloudreve generic parser must reject + * hosts that resolve to loopback / private / link-local / metadata ranges + * before any outbound request. + */ +public class AssertPublicHostTest { + + @Test + public void rejectsLoopbackAndPrivateHosts() throws Exception { + String[] blocked = { + "http://127.0.0.1.nip.io/s/poc", + "http://localhost/s/poc", + "http://10.0.0.1/s/poc", + "http://192.168.1.1/s/poc", + "http://172.16.0.1/s/poc", + "http://169.254.169.254/s/poc", + "http://[::1]/s/poc" + }; + for (String raw : blocked) { + try { + PanBase.assertPublicHost(new URL(raw)); + fail("expected block for " + raw); + } catch (IOException expected) { + assertTrue(expected.getMessage().contains("不允许访问") + || expected.getMessage().contains("无法解析")); + } + } + } + + @Test + public void allowsPublicHost() throws Exception { + PanBase.assertPublicHost(new URL("https://example.com/s/demo")); + } +}