diff --git a/src/main/java/com/yunhai/platform/gateway/filter/SanitizeFilter.java b/src/main/java/com/yunhai/platform/gateway/filter/SanitizeFilter.java index 552da89..13dd121 100644 --- a/src/main/java/com/yunhai/platform/gateway/filter/SanitizeFilter.java +++ b/src/main/java/com/yunhai/platform/gateway/filter/SanitizeFilter.java @@ -22,7 +22,8 @@ import java.io.IOException; * 含 {@code ..}(含编码形态 {@code %2e%2e})、路径含双斜杠 {@code //}、 * 含控制字符(0x00-0x1f,含空字节)。 *

- * 通过后把原始 URI(逐字节不改写——T0.12 forwardUrl 与 T1.2 canonical 都消费原始形态) + * 通过后把业务 URI(ctx 非空时已剥 context-path;ctx 为空时即原始 URI,逐字节不改写—— + * T0.12 forwardUrl 与 T1.2 canonical 都消费此形态) * 与剥掉 target 首段后的路径存 request attribute,后续过滤器与转发器一律取此值。 */ @Component @@ -41,7 +42,7 @@ public class SanitizeFilter implements Filter { } HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; - String uri = req.getRequestURI(); + String uri = stripContextPath(req.getRequestURI(), req.getContextPath()); String decoded = percentDecode(uri); if (isRejected(decoded)) { EnvelopeWriter.writeBare(res, 400); @@ -101,4 +102,18 @@ public class SanitizeFilter implements Filter { int second = uri.indexOf('/', 1); return second < 0 ? uri : uri.substring(second); } + + /** + * 剥 context-path:Servlet 规范下 getRequestURI() = ctx + servletPath + pathInfo, + * 而网关的业务路径空间以 target 为首段——ctx 非空时须先剥离,下游(白名单/target 判定/ + * 台账/验签路径)看到的才是与 ctx 解耦的业务 URI。ctx 为空的应用 getContextPath() 返回 "", + * 此处零行为变化(向后兼容)。 + */ + static String stripContextPath(String uri, String contextPath) { + if (uri == null || contextPath == null || contextPath.isEmpty()) { + return uri; + } + String prefix = contextPath + "/"; + return uri.startsWith(prefix) ? uri.substring(contextPath.length()) : uri; + } } diff --git a/src/test/java/com/yunhai/platform/gateway/filter/SanitizeFilterTest.java b/src/test/java/com/yunhai/platform/gateway/filter/SanitizeFilterTest.java index ae95d96..3d00bfd 100644 --- a/src/test/java/com/yunhai/platform/gateway/filter/SanitizeFilterTest.java +++ b/src/test/java/com/yunhai/platform/gateway/filter/SanitizeFilterTest.java @@ -9,8 +9,9 @@ import org.springframework.mock.web.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; /** - * SanitizeFilter 五用例(T1.1 TDD):路径穿越(明文/编码)、双斜杠、控制字符均裸 400; - * 正常路径放行且两个 attribute 契约已设。 + * SanitizeFilter 九用例(T1.1 TDD):路径穿越(明文/编码)、双斜杠、控制字符均裸 400; + * 正常路径放行且两个 attribute 契约已设。ctx 非空时先剥 context-path 再判定, + * 前缀相似不误剥,ctx 为空零行为变化。 */ class SanitizeFilterTest { @@ -58,4 +59,51 @@ class SanitizeFilterTest { assertThat(req.getAttribute(SanitizeFilter.ATTR_STRIPPED_PATH)) .as("stripped.path = 剥 target 首段").isEqualTo("/oapi/x"); } + + @Test + void ctx非空_剥除后存业务URI() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest("GET", "/open-gateway/notice/oapi/x"); + req.setContextPath("/open-gateway"); + MockHttpServletResponse res = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + new SanitizeFilter().doFilter(req, res, chain); + assertThat(res.getStatus()).as("ctx 内正常路径放行").isEqualTo(200); + assertThat(req.getAttribute(SanitizeFilter.ATTR_SANITIZED_URI)) + .as("sanitized.uri = 业务 URI(已去 ctx)").isEqualTo("/notice/oapi/x"); + assertThat(req.getAttribute(SanitizeFilter.ATTR_STRIPPED_PATH)) + .as("stripped.path 基于业务 URI 剥 target").isEqualTo("/oapi/x"); + } + + @Test + void ctx非空_前缀相似不误剥() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest("GET", "/open-gateway-other/oapi/x"); + req.setContextPath("/open-gateway"); + MockHttpServletResponse res = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + new SanitizeFilter().doFilter(req, res, chain); + assertThat(req.getAttribute(SanitizeFilter.ATTR_SANITIZED_URI)) + .as("前缀相似不剥,交由 target 判定 404").isEqualTo("/open-gateway-other/oapi/x"); + } + + @Test + void ctx非空_剥后双斜杠仍拒() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest("GET", "/open-gateway//x"); + req.setContextPath("/open-gateway"); + MockHttpServletResponse res = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + new SanitizeFilter().doFilter(req, res, chain); + assertThat(res.getStatus()).as("剥 ctx 后 // 仍裸 400").isEqualTo(400); + } + + @Test + void stripContextPath_边界() { + assertThat(SanitizeFilter.stripContextPath("/userbase/oapi/x", "")).isEqualTo("/userbase/oapi/x"); + assertThat(SanitizeFilter.stripContextPath("/userbase/oapi/x", null)).isEqualTo("/userbase/oapi/x"); + assertThat(SanitizeFilter.stripContextPath(null, "/open-gateway")).isNull(); + assertThat(SanitizeFilter.stripContextPath("/open-gateway", "/open-gateway")) + .as("裸 ctx 不剥(无尾斜杠),原样走 404 语义").isEqualTo("/open-gateway"); + assertThat(SanitizeFilter.stripContextPath("/open-gateway/x", "/open-gateway")).isEqualTo("/x"); + assertThat(SanitizeFilter.stripContextPath("/open-gateway-other/x", "/open-gateway")) + .isEqualTo("/open-gateway-other/x"); + } }