CVE-2023-44794
Description
Sa-Token <=1.36.0, when used with Spring Boot >=2.3.1 or Spring >=5.3.0, allows privilege escalation via crafted URLs due to differing URI pattern matching.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Sa-Token <=1.36.0, when used with Spring Boot >=2.3.1 or Spring >=5.3.0, allows privilege escalation via crafted URLs due to differing URI pattern matching.
Root
Cause Sa-Token versions 1.36.0 and earlier, when integrated with Spring Boot 2.3.1+ or Spring 5.3.0+, are vulnerable to authentication bypass due to differences in URI pattern matching. Sa-Token uses AntPathMatcher for route interception, while Spring Boot's default matching strategy in those versions is PathPatternParser. This mismatch allows an attacker to craft a URL that Sa-Token interprets as a non-protected path and Spring interprets as a protected path, bypassing access controls [2].
Exploitation
An attacker can send a specially crafted HTTP request containing path traversal or encoding tricks (e.g., using URL-encoded characters or double slashes) to access protected endpoints like /admin/** without the required permissions. No authentication or special network position is required; the attack is remote and simple to execute [2].
Impact
Successful exploitation enables an unprivileged user to escalate privileges and access admin-level functionality, potentially leading to full system compromise depending on the application's configuration. The vulnerability does not require any prior authentication [2].
Mitigation
The issue is fixed in Sa-Token version 1.37.0. Users unable to upgrade can set the Spring Boot configuration spring.mvc.pathmatch.matching-strategy=ant_path_matcher to align URI matching behavior and close the bypass vector [2][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
cn.dev33:sa-token-coreMaven | < 1.37.0 | 1.37.0 |
Affected products
2- Dromara/SaTokendescription
Patches
133 files changed · +688 −79
sa-token-core/src/main/java/cn/dev33/satoken/application/ApplicationInfo.java+45 −0 added@@ -0,0 +1,45 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.application; + +import cn.dev33.satoken.util.SaFoxUtil; + +/** + * 应用全局信息 + * + * @author click33 + * @since 1.31.0 + */ +public class ApplicationInfo { + + /** + * 应用前缀 + */ + public static String routePrefix; + + /** + * 为指定 path 裁剪掉 routePrefix 前缀 + * @param path 指定 path + * @return / + */ + public static String cutPathPrefix(String path) { + if(! SaFoxUtil.isEmpty(routePrefix) && ! routePrefix.equals("/") && path.startsWith(routePrefix)){ + path = path.substring(routePrefix.length()); + } + return path; + } + +} \ No newline at end of file
sa-token-core/src/main/java/cn/dev33/satoken/exception/RequestPathInvalidException.java+46 −0 added@@ -0,0 +1,46 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.exception; + +/** + * 一个异常:代表请求 path 无效或非法 + * + * @author click33 + * @since 1.37.0 + */ +public class RequestPathInvalidException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 8243974276159004739L; + + /** 具体无效的 path */ + private final String path; + + /** + * @return 具体无效的 path + */ + public String getPath() { + return path; + } + + public RequestPathInvalidException(String message, String path) { + super(message); + this.path = path; + } + +}
sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaCheckRequestPathFunction.java+39 −0 added@@ -0,0 +1,39 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.fun.strategy; + +import cn.dev33.satoken.exception.RequestPathInvalidException; + +/** + * 函数式接口:校验请求 path 的算法 + * + * <p> 如果属于无效请求 path,则抛出异常 RequestPathInvalidException </p> + * + * @author click33 + * @since 1.37.0 + */ +@FunctionalInterface +public interface SaCheckRequestPathFunction { + + /** + * 执行函数 + * @param path 请求 path + * @param extArg1 扩展参数1 + * @param extArg2 扩展参数2 + */ + void run(String path, Object extArg1, Object extArg2); + +} \ No newline at end of file
sa-token-core/src/main/java/cn/dev33/satoken/fun/strategy/SaRequestPathInvalidHandleFunction.java+37 −0 added@@ -0,0 +1,37 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.fun.strategy; + +import cn.dev33.satoken.exception.RequestPathInvalidException; + +/** + * 函数式接口:当请求 path 校验不通过时处理方案的算法 + * + * @author click33 + * @since 1.37.0 + */ +@FunctionalInterface +public interface SaRequestPathInvalidHandleFunction { + + /** + * 执行函数 + * @param e 请求 path 无效的异常对象 + * @param extArg1 扩展参数1 + * @param extArg2 扩展参数2 + */ + void run(RequestPathInvalidException e, Object extArg1, Object extArg2); + +} \ No newline at end of file
sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java+44 −0 modified@@ -18,6 +18,7 @@ import cn.dev33.satoken.SaManager; import cn.dev33.satoken.annotation.*; import cn.dev33.satoken.basic.SaBasicUtil; +import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.fun.strategy.*; import cn.dev33.satoken.session.SaSession; @@ -329,6 +330,49 @@ private SaStrategy() { return new StpLogic(loginType); }; + /** + * 请求 path 不允许出现的字符 + */ + public static String[] INVALID_CHARACTER = { + "//", "\\", + "%2e", "%2E", // . + "%2f", "%2F", // / + "%5c", "%5C", // \ + "%25" // 空格 + }; + + /** + * 校验请求 path 的算法 + */ + public SaCheckRequestPathFunction checkRequestPath = (requestPath, extArg1, extArg2) -> { + + // 不允许为null + if(requestPath == null) { + throw new RequestPathInvalidException("非法请求:null", null); + } + // 不允许包含非法字符 + for (String item : INVALID_CHARACTER) { + if (requestPath.contains(item)) { + throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); + } + } + // 不允许出现跨目录 + if(requestPath.contains("/.") || requestPath.contains("\\.")) { + throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath); + } + }; + + + /** + * 当请求 path 校验不通过时处理方案的算法,自定义示例: + * <pre> + * SaStrategy.instance.requestPathInvalidHandle = (e, extArg1, extArg2) -> { + * // 自定义处理逻辑 ... + * }; + * </pre> + */ + public SaRequestPathInvalidHandleFunction requestPathInvalidHandle = null; + // ----------------------- 重写策略 set连缀风格
sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java+22 −0 modified@@ -186,6 +186,28 @@ private SaTokenConsts() { */ public static final int ASSEMBLY_ORDER = -100; + /** + * 请求 path 校验过滤器的注册顺序 + */ + public static final int PATH_CHECK_FILTER_ORDER = -1000; + + /** + * Content-Type key + */ + public static final String CONTENT_TYPE_KEY = "Content-Type"; + + /** + * Content-Type text/plain; charset=utf-8 + */ + public static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain; charset=utf-8"; + + /** + * Content-Type application/json;charset=UTF-8 + */ + public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json;charset=UTF-8"; + + + // =================== 废弃 ===================
sa-token-demo/sa-token-demo-springboot3-redis/src/main/java/com/pj/test/TestController.java+12 −2 modified@@ -1,10 +1,11 @@ package com.pj.test; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.spring.SpringMVCUtil; +import cn.dev33.satoken.util.SaResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import cn.dev33.satoken.util.SaResult; - /** * 测试专用Controller * @author click33 @@ -27,4 +28,13 @@ public SaResult test2() { return SaResult.ok(); } + // 测试 浏览器访问: http://localhost:8081/test/getRequestPath + @RequestMapping("getRequestPath") + public SaResult getRequestPath() { + System.out.println("-------------- 测试请求 path 获取"); + System.out.println("request.getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); + System.out.println("saRequest.getRequestPath() " + SaHolder.getRequest().getRequestPath()); + return SaResult.ok(); + } + }
sa-token-demo/sa-token-demo-springboot3-redis/src/main/resources/application.yml+1 −1 modified@@ -19,7 +19,7 @@ sa-token: # 是否输出操作日志 is-log: true -spring: +spring: data: # redis配置 redis:
sa-token-demo/sa-token-demo-test/pom.xml+1 −0 modified@@ -11,6 +11,7 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.14</version> + <!--<version>2.3.0.RELEASE</version>--> <!-- <version>1.5.9.RELEASE</version> --> <relativePath/> </parent>
sa-token-demo/sa-token-demo-test/src/main/java/com/pj/current/NotFoundHandle.java+1 −1 modified@@ -23,5 +23,5 @@ public Object error(HttpServletRequest request, HttpServletResponse response) th response.setStatus(200); return SaResult.get(404, "not found", null); } - + }
sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java+11 −0 modified@@ -1,5 +1,7 @@ package com.pj.test; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.stp.SaLoginConfig; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaFoxUtil; @@ -41,4 +43,13 @@ public SaResult test2() { return SaResult.ok(); } + // 测试 浏览器访问: http://localhost:8081/test/getRequestPath + @RequestMapping("getRequestPath") + public SaResult getRequestPath() { + System.out.println("------------ 测试访问路径获取 "); + System.out.println("SpringMVCUtil.getRequest().getRequestURI() " + SpringMVCUtil.getRequest().getRequestURI()); + System.out.println("SaHolder.getRequest().getRequestPath() " + SaHolder.getRequest().getRequestPath()); + return SaResult.ok(); + } + }
sa-token-demo/sa-token-demo-test/src/main/resources/application.yml+1 −1 modified@@ -19,7 +19,7 @@ sa-token: # 是否输出操作日志 is-log: true -spring: +spring: # redis配置 redis: # Redis数据库索引(默认为0)
sa-token-demo/sa-token-demo-webflux-springboot3/src/main/java/com/pj/SaTokenWebfluxSpringboot3Application.java+1 −2 modified@@ -1,10 +1,9 @@ package com.pj; +import cn.dev33.satoken.SaManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import cn.dev33.satoken.SaManager; - /** * Sa-Token整合webflux 示例 (springboot3) *
sa-token-demo/sa-token-demo-webflux-springboot3/src/main/resources/application.yml+1 −1 modified@@ -19,7 +19,7 @@ sa-token: # 是否输出操作日志 is-log: true -spring: +spring: # redis配置 redis: # Redis数据库索引(默认为0)
sa-token-demo/sa-token-demo-webflux/src/main/resources/application.yml+1 −1 modified@@ -19,7 +19,7 @@ sa-token: # 是否输出操作日志 is-log: true -spring: +spring: # redis配置 redis: # Redis数据库索引(默认为0)
sa-token-starter/sa-token-jakarta-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java+2 −1 modified@@ -16,6 +16,7 @@ package cn.dev33.satoken.servlet.model; import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.servlet.error.SaServletErrorCode; @@ -124,7 +125,7 @@ public String getCookieValue(String name) { */ @Override public String getRequestPath() { - return request.getServletPath(); + return ApplicationInfo.cutPathPrefix(request.getRequestURI()); } /**
sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaPathCheckFilterForReactor.java+56 −0 added@@ -0,0 +1,56 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.reactor.filter; + +import cn.dev33.satoken.exception.RequestPathInvalidException; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 校验请求 path 是否合法 + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.PATH_CHECK_FILTER_ORDER) +public class SaPathCheckFilterForReactor implements WebFilter { + + @Override + public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { + + // 校验本次请求 path 是否合法 + try { + SaStrategy.instance.checkRequestPath.run(exchange.getRequest().getPath().toString(), exchange, null); + } catch (RequestPathInvalidException e) { + if(SaStrategy.instance.requestPathInvalidHandle == null) { + exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); + return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes()))); + } else { + SaStrategy.instance.requestPathInvalidHandle.run(e, exchange, null); + } + return Mono.empty(); + } + + // 向下执行 + return chain.filter(exchange); + } + +}
sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java+11 −12 modified@@ -15,28 +15,27 @@ */ package cn.dev33.satoken.reactor.filter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import cn.dev33.satoken.filter.SaFilter; -import org.springframework.core.annotation.Order; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; - import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; +import cn.dev33.satoken.filter.SaFilter; import cn.dev33.satoken.filter.SaFilterAuthStrategy; import cn.dev33.satoken.filter.SaFilterErrorStrategy; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.reactor.error.SaReactorSpringBootErrorCode; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * Reactor 全局鉴权过滤器 * <p> @@ -154,8 +153,8 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { // 2. 写入输出流 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); - if(exchange.getResponse().getHeaders().getFirst("Content-Type") == null) { - exchange.getResponse().getHeaders().set("Content-Type", "text/plain; charset=utf-8"); + if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) { + exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes())));
sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java+6 −6 modified@@ -16,16 +16,16 @@ package cn.dev33.satoken.reactor.model; -import org.springframework.http.HttpCookie; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilterChain; - import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.http.HttpCookie; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; import java.util.ArrayList; import java.util.List; @@ -113,7 +113,7 @@ public String getCookieValue(String name) { */ @Override public String getRequestPath() { - return request.getURI().getPath(); + return ApplicationInfo.cutPathPrefix(request.getPath().toString()); } /**
sa-token-starter/sa-token-reactor-spring-boot3-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java+12 −2 modified@@ -15,9 +15,9 @@ */ package cn.dev33.satoken.reactor.spring; -import org.springframework.context.annotation.Bean; - import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.reactor.filter.SaPathCheckFilterForReactor; +import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token 所需要的 Bean @@ -37,4 +37,14 @@ public SaTokenContext getSaTokenContextForSpringReactor() { return new SaTokenContextForSpringReactor(); } + /** + * 请求 path 校验过滤器 + * + * @return / + */ + @Bean + public SaPathCheckFilterForReactor saPathCheckFilterForReactor() { + return new SaPathCheckFilterForReactor(); + } + }
sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaPathCheckFilterForReactor.java+56 −0 added@@ -0,0 +1,56 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.reactor.filter; + +import cn.dev33.satoken.exception.RequestPathInvalidException; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 校验请求 path 是否合法 + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.PATH_CHECK_FILTER_ORDER) +public class SaPathCheckFilterForReactor implements WebFilter { + + @Override + public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { + + // 校验本次请求 path 是否合法 + try { + SaStrategy.instance.checkRequestPath.run(exchange.getRequest().getPath().toString(), exchange, null); + } catch (RequestPathInvalidException e) { + if(SaStrategy.instance.requestPathInvalidHandle == null) { + exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); + return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes()))); + } else { + SaStrategy.instance.requestPathInvalidHandle.run(e, exchange, null); + } + return Mono.empty(); + } + + // 向下执行 + return chain.filter(exchange); + } + +}
sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/filter/SaReactorFilter.java+2 −2 modified@@ -153,8 +153,8 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { // 2. 写入输出流 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); - if(exchange.getResponse().getHeaders().getFirst("Content-Type") == null) { - exchange.getResponse().getHeaders().set("Content-Type", "text/plain; charset=utf-8"); + if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) { + exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes())));
sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/model/SaRequestForReactor.java+2 −1 modified@@ -17,6 +17,7 @@ import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.reactor.context.SaReactorHolder; import cn.dev33.satoken.reactor.context.SaReactorSyncHolder; @@ -112,7 +113,7 @@ public String getCookieValue(String name) { */ @Override public String getRequestPath() { - return request.getURI().getPath(); + return ApplicationInfo.cutPathPrefix(request.getPath().toString()); } /**
sa-token-starter/sa-token-reactor-spring-boot-starter/src/main/java/cn/dev33/satoken/reactor/spring/SaTokenContextRegister.java+11 −0 modified@@ -15,6 +15,7 @@ */ package cn.dev33.satoken.reactor.spring; +import cn.dev33.satoken.reactor.filter.SaPathCheckFilterForReactor; import org.springframework.context.annotation.Bean; import cn.dev33.satoken.context.SaTokenContext; @@ -37,4 +38,14 @@ public SaTokenContext getSaTokenContextForSpringReactor() { return new SaTokenContextForSpringReactor(); } + /** + * 请求 path 校验过滤器 + * + * @return / + */ + @Bean + public SaPathCheckFilterForReactor saPathCheckFilterForReactor() { + return new SaPathCheckFilterForReactor(); + } + }
sa-token-starter/sa-token-servlet/src/main/java/cn/dev33/satoken/servlet/model/SaRequestForServlet.java+12 −12 modified@@ -15,20 +15,20 @@ */ package cn.dev33.satoken.servlet.model; -import java.io.IOException; -import java.util.*; - -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.application.ApplicationInfo; import cn.dev33.satoken.context.model.SaRequest; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.servlet.error.SaServletErrorCode; import cn.dev33.satoken.util.SaFoxUtil; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + /** * 对 SaRequest 包装类的实现(Servlet 版) * @@ -41,15 +41,15 @@ public class SaRequestForServlet implements SaRequest { * 底层Request对象 */ protected HttpServletRequest request; - + /** * 实例化 - * @param request request对象 + * @param request request对象 */ public SaRequestForServlet(HttpServletRequest request) { this.request = request; } - + /** * 获取底层源对象 */ @@ -125,7 +125,7 @@ public String getCookieValue(String name) { */ @Override public String getRequestPath() { - return request.getServletPath(); + return ApplicationInfo.cutPathPrefix(request.getRequestURI()); } /**
sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/filter/SaPathCheckFilterForJakartaServlet.java+68 −0 added@@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.filter; + +import cn.dev33.satoken.exception.RequestPathInvalidException; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.annotation.Order; + +import java.io.IOException; + +/** + * 校验请求 path 是否合法 + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.PATH_CHECK_FILTER_ORDER) +public class SaPathCheckFilterForJakartaServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + // 校验本次请求 path 是否合法 + try { + HttpServletRequest req = (HttpServletRequest) request; + SaStrategy.instance.checkRequestPath.run(req.getRequestURI(), request, response); + } catch (RequestPathInvalidException e) { + if(SaStrategy.instance.requestPathInvalidHandle == null) { + response.setContentType("text/plain; charset=utf-8"); + response.getWriter().print(e.getMessage()); + response.getWriter().flush(); + } else { + SaStrategy.instance.requestPathInvalidHandle.run(e, request, response); + } + return; + } + + // 向下执行 + chain.doFilter(request, response); + } + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void destroy() { + } + + + +}
sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java+8 −14 modified@@ -15,25 +15,19 @@ */ package cn.dev33.satoken.filter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.springframework.core.annotation.Order; - import cn.dev33.satoken.error.SaSpringBootErrorCode; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaTokenConsts; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; +import jakarta.servlet.*; +import org.springframework.core.annotation.Order; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * Servlet 全局鉴权过滤器 @@ -146,7 +140,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(response.getContentType() == null) { - response.setContentType("text/plain; charset=utf-8"); + response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } response.getWriter().print(result); return;
sa-token-starter/sa-token-spring-boot3-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java+12 −2 modified@@ -15,9 +15,9 @@ */ package cn.dev33.satoken.spring; -import org.springframework.context.annotation.Bean; - import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.filter.SaPathCheckFilterForJakartaServlet; +import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token 框架所需要的 Bean @@ -37,4 +37,14 @@ public SaTokenContext getSaTokenContextForSpringInJakartaServlet() { return new SaTokenContextForSpringInJakartaServlet(); } + /** + * 请求 path 校验过滤器 + * + * @return / + */ + @Bean + public SaPathCheckFilterForJakartaServlet saPathCheckFilterForJakartaServlet() { + return new SaPathCheckFilterForJakartaServlet(); + } + }
sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/context/path/ApplicationContextPathLoading.java+68 −0 added@@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.spring.context.path; + +import cn.dev33.satoken.application.ApplicationInfo; +import cn.dev33.satoken.util.SaFoxUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +/** + * 应用上下文路径加载器 + * + * @author click33 + * @since 1.37.0 + */ +public class ApplicationContextPathLoading implements ApplicationRunner { + + @Value("${server.servlet.context-path:}") + String contextPath; + + @Value("${spring.mvc.servlet.path:}") + String servletPath; + + @Override + public void run(ApplicationArguments args) throws Exception { + + String routePrefix = ""; + + if(SaFoxUtil.isNotEmpty(contextPath)) { + if(! contextPath.startsWith("/")){ + contextPath = "/" + contextPath; + } + if (contextPath.endsWith("/")) { + contextPath = contextPath.substring(0, contextPath.length() - 1); + } + routePrefix += contextPath; + } + + if(SaFoxUtil.isNotEmpty(servletPath)) { + if(! servletPath.startsWith("/")){ + servletPath = "/" + servletPath; + } + if (servletPath.endsWith("/")) { + servletPath = servletPath.substring(0, servletPath.length() - 1); + } + routePrefix += servletPath; + } + + if(SaFoxUtil.isNotEmpty(routePrefix) && ! routePrefix.equals("/") ){ + ApplicationInfo.routePrefix = routePrefix; + } + } + +} \ No newline at end of file
sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanRegister.java+11 −1 modified@@ -15,6 +15,7 @@ */ package cn.dev33.satoken.spring; +import cn.dev33.satoken.spring.context.path.ApplicationContextPathLoading; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -50,5 +51,14 @@ public SaTokenConfig getSaTokenConfig() { public SaJsonTemplate getSaJsonTemplateForJackson() { return new SaJsonTemplateForJackson(); } - + + /** + * 应用上下文路径加载器 + * @return / + */ + @Bean + public ApplicationContextPathLoading getApplicationContextPathLoading() { + return new ApplicationContextPathLoading(); + } + }
sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaPathCheckFilterForServlet.java+68 −0 added@@ -0,0 +1,68 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.filter; + +import cn.dev33.satoken.exception.RequestPathInvalidException; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * 校验请求 path 是否合法 + * + * @author click33 + * @since 1.37.0 + */ +@Order(SaTokenConsts.PATH_CHECK_FILTER_ORDER) +public class SaPathCheckFilterForServlet implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + // 校验本次请求 path 是否合法 + try { + HttpServletRequest req = (HttpServletRequest) request; + SaStrategy.instance.checkRequestPath.run(req.getRequestURI(), request, response); + } catch (RequestPathInvalidException e) { + if(SaStrategy.instance.requestPathInvalidHandle == null) { + response.setContentType("text/plain; charset=utf-8"); + response.getWriter().print(e.getMessage()); + response.getWriter().flush(); + } else { + SaStrategy.instance.requestPathInvalidHandle.run(e, request, response); + } + return; + } + + // 向下执行 + chain.doFilter(request, response); + } + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void destroy() { + } + + + +}
sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/filter/SaServletFilter.java+8 −15 modified@@ -15,26 +15,19 @@ */ package cn.dev33.satoken.filter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -import org.springframework.core.annotation.Order; - import cn.dev33.satoken.error.SaSpringBootErrorCode; import cn.dev33.satoken.exception.BackResultException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.StopMatchException; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.util.SaTokenConsts; +import org.springframework.core.annotation.Order; + +import javax.servlet.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * Servlet 全局鉴权过滤器 @@ -147,7 +140,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(response.getContentType() == null) { - response.setContentType("text/plain; charset=utf-8"); + response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } response.getWriter().print(result); return;
sa-token-starter/sa-token-spring-boot-starter/src/main/java/cn/dev33/satoken/spring/SaTokenContextRegister.java+12 −2 modified@@ -15,9 +15,9 @@ */ package cn.dev33.satoken.spring; -import org.springframework.context.annotation.Bean; - import cn.dev33.satoken.context.SaTokenContext; +import cn.dev33.satoken.filter.SaPathCheckFilterForServlet; +import org.springframework.context.annotation.Bean; /** * 注册 Sa-Token 框架所需要的 Bean @@ -37,4 +37,14 @@ public SaTokenContext getSaTokenContextForSpring() { return new SaTokenContextForSpring(); } + /** + * 请求 path 校验过滤器 + * + * @return / + */ + @Bean + public SaPathCheckFilterForServlet saPathCheckFilterForServlet() { + return new SaPathCheckFilterForServlet(); + } + }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.