鉴权绕过
分号、路径穿越、双斜杠——鉴权逻辑判断的是「路径长什么样」,而路由匹配的是「请求落到哪个处理器」。
一类很常见的错误写法
// 白名单:登录接口、静态资源不需要鉴权
if (requestURI.endsWith("/login") || requestURI.endsWith(".js") || requestURI.endsWith(".css")) {
return true; // 放行
}
这段代码的意图是「静态资源放行」,但 requestURI 是客户端可控的完整路径,包括查询串之前的一切。
于是 admin/list.do;.js 这个请求:
- 鉴权代码看到它
endsWith(".js"),放行; - 路由框架按
;之前的部分匹配,落到admin/list.do的处理器上。
两边都按自己的规则正确执行,结果就是未授权访问。
分号为什么特殊
; 在 URI 规范里是路径参数(matrix parameter)的分隔符。Servlet 规范要求容器忽略它后面的部分来定位资源,但 getRequestURI() 返回的是原始字符串,不处理 ;。
这个差异是所有这类绕过的根。
可以试的几种形态
/admin/list.do;.js
/admin/list.do;.css
/admin/list.do;jsessionid=x
/admin/../admin/list.do
//admin/list.do
/./admin/list.do
/admin/list.do%00.js (老容器)
不同框架的处理不一样。Spring 的老版本对 ; 的处理有已知问题,Shiro、Struts2 也各有各的绕过姿势。
这类问题在 Java 项目里出现得最多,因为 getRequestURI() 太常用,而它的返回值又太「原始」。
怎么发现
没有通用的扫描器规则,只能靠理解和手测。
几个切入点:
- 看有没有静态资源白名单。凡是按后缀放行的鉴权逻辑,都值得试一遍。
- 看登录页、静态资源、错误页这些「不需要登录」的路径,在后面拼
;加别的东西。 - 把已登录时的正常请求,去掉 cookie,加上后缀变体,看是否仍然返回数据。
第 3 条最直接:用登录态抓一个后台接口,退出登录,再带 ;.js 请求一次。返回 200 且有数据,就是它。
同类问题:越权
鉴权绕过是「根本不检查」,越权是「检查了但检查错了」。
越权更常见,也更容易漏。测试时固定三个视角:
- 水平:A 用户能不能读 B 用户的数据。改
id、改userId、改手机号。 - 垂直:普通用户能不能调管理员接口。拿普通账号的 cookie 去请求后台路径。
- 跨租户:多租户系统里,A 公司能不能访问 B 公司的数据。改
tenantId、改子域。
这三种都需要两个账号,一个高权限一个低权限。测试前就跟客户要,别等到发现了再申请。
修复
- 鉴权按「资源」判断,不按「路径字符串」判断。
- 白名单用精确匹配,不用
endsWith、contains。 - 用框架的
getServletPath()而不是getRequestURI(),前者已经处理过路径参数。 - 网关层做一次统一鉴权,应用层再做一次。两层都过才算通过。
参考
- 《几个有趣的 bypass 挖洞案例》,公众号 渗透安全HackTwo