














CVE-2026-65182 是 Apache Tomcat 中一个重要的安全约束绕过漏洞,由声明式安全约束(security-constraint)的处理缺陷导致。当为较长路径定义的约束,出现在为较短子路径定义的更严格约束之前时,可能导致更严格的规则被绕过。
是什么:web.xml 里用多条“访问规则”控制谁能访问哪个路径。当一条“长路径(拒绝)”和一条“短路径(放行)”重叠时,Tomcat 会挑错规则去执行,把本应拒绝的路径放行。
根因:Tomcat 挑选“最长匹配”规则时,同一个规则里后写的短路径会把先写长路径的匹配长度调小,于是本该生效的“拒绝”被当成“不够长”而淘汰,改由“放行”生效。
怎么验证:构造满足L2 < L3 < L1的配置,对深层路径发未认证请求。9.0.120 返回200(越权),而修复版应返回401/403。
受影响范围(谨慎表述):并非“有公开子路径就受影响”,而是同时满足以下三点才可能——同一规则(collection)内多个 URL Pattern 且深层在前、长短路径之间夹有一条介入的放行规则、该拒绝规则先于放行规则声明。具体边界见第 2.1 节。
每次请求进来,Tomcat 会先找出一条“管这个 URL 的访问规则”去执行。找规则这一步有个小 bug:本应生效的“拒绝访问”会被错误筛掉。先给一个不依赖源码的模型,再逐步进入源码。
关键的一句话是:同一个规则里既写了长路径、又写了短路径,且短路径写在后面时,短路径会把该规则“代表的最长匹配值”调小。
拒绝规则(声明在前):
/level1/level2/level3/* (深层)
/level1/* (浅层兜底,写在后面)
Tomcat 实际认为它代表的最长匹配:
/level1/* ← 被短路径调小了,本应是深层那一条
此时若目标 uri 落在两者之间、且由另一条规则放行(如 /level1/level2/* 为放行):
放行规则会被误当成“最长”而生效 → 越权
整篇内容的机制都围绕这一句展开,后面只是用源码把它坐实。
先交代“谁在管这件事”:URL→Servlet 的映射由Mapper负责;而访问规则是否放行,是在请求进入应用后的鉴权环节里检查的,两者相互独立。
安全约束的匹配不在Mapper中,而是在 Context 管道的认证 Valve 里:
Coyote Adapter (Adapter.service)
└─ StandardEngineValve → StandardHostValve → StandardContextValve
└─ 各类 ContextValve(含 AuthenticatorBase.invoke())
├─ AuthenticatorBase.invoke
│ ├─ L523 realm.findSecurityConstraints(request, context) ← 漏洞入口
│ ├─ L556 realm.hasUserDataPermission(...) (transport-guarantee)
│ ├─ L605 doAuthenticate / authenticateJaspic (认证,可选)
│ └─ L635 realm.hasResourcePermission(...) ← 最终授权判定
源码位置:
认证入口:AuthenticatorBase.java:/tomcat/java/org/apache/catalina/authenticator/AuthenticatorBase.java#L519-L639
约束求值:RealmBase.findSecurityConstraints:tomcat/java/org/apache/catalina/realm/RealmBase.java#L553-L693
授权判定:RealmBase.hasResourcePermission:tomcat/java/org/apache/catalina/realm/RealmBase.java#L795-L859
AuthenticatorBase.java#L519-L536(含 L523 调用点 + “无约束直接放行”分支
规则按写在配置里的先后顺序存在 Context 里,Tomcat 不做排序。多个规则重叠时,它靠“谁的路径最长谁说了算”来判断用哪条。
约束在 Context 中按声明顺序保存(context.findConstraints()),无排序。
“最长匹配优先”由变量longest实现:一旦发现更长匹配,就用results.clear()丢弃之前的较短结果,最后只保留最长匹配的那条约束进授权判定。
正是这个“只留最长”的逻辑缺陷构成了绕过基础。

漏洞函数findSecurityConstraints中,对每个SecurityCollection的模式循环(9.0.120):
// RealmBase.java:644-659 (9.0.120)
boolean matched = false;
int length = -1;
for (String pattern : patterns) {
if (pattern.startsWith("/") && pattern.endsWith("/*") && pattern.length() >= longest) {
if (pattern.length() == 2) { // "/*"
matched = true;
length = pattern.length();
} else if (pattern.regionMatches(0, uri, 0, pattern.length() - 1)
|| (pattern.length() - 2 == uri.length()
&& pattern.regionMatches(0, uri, 0, pattern.length() - 2))) {
matched = true;
length = pattern.length(); // ← 后出现的短模式覆盖 length
}
}
}
if (matched) {
if (length > longest) { // 用被低估的 length 更新全局 longest
found = false;
if (results != null) results.clear();
longest = length;
}
...
}
问题出在length:同一个集合内,后遍历到的短模式会覆盖前面长模式得到的值。以拒绝规则[ /level1/level2/level3/* , /level1/* ]为例,Tomcat 先算出 25,又被后写的/level1/*覆盖成 9;随后长度 16 的放行规则/level1/level2/*因大于 9 而“反超”生效,拒绝规则被挤掉,受保护目录被匿名放行。
根因循环 RealmBase.java#L619-L681(L645/L647/L651/L655/L661
修复文件清单:
| 文件 | 改动 |
|---|---|
java/org/apache/catalina/realm/RealmBase.java | 核心修复:通配模式匹配门控 |
test/.../TestRealmBase.java | 新增回归测试testOverlappingConstraints |
test/.../TesterRequest.java | 支撑测试(支持自定义 URI) |
webapps/docs/changelog.xml | 变更说明 |
核心 diff:
// RealmBase.java:647
for (String pattern : patterns) {
- if (pattern.startsWith("/") && pattern.endsWith("/*") && pattern.length() >= longest) {
+ if (pattern.startsWith("/") && pattern.endsWith("/*") && pattern.length() >= longest &&
+ pattern.length() >= length) {
...
git diff 9.0.120 vs 9.0.121

| 维度 | 修改前(9.0.120) | 修改后(9.0.121) |
|---|---|---|
| 门控条件 | 仅pattern.length() >= longest | 增加pattern.length() >= length |
同集合内length更新 | 后者覆盖前者 | 只能被更长的模式更新 |
全局longest | 可能被低估(如 25→9) | 保持真实最长匹配(25) |
| 结果选择 | 放行约束可能反超、拒绝被挤出 | 拒绝被正确保留 |
一个“拒绝”规则里常常会同时写长路径和短路径(例如“整站要登录”+“深层目录更要控”)。只要中间恰好夹了一条更短的“放行”路径,Tomcat 就可能把放行当“最长”去执行。
用长度关系概括:把拒绝规则的最长路径长度叫 L1、较短兜底路径长度叫 L2,把夹在中间的放行路径长度叫 L3。当三者满足L2 < L3 < L1时,绕过才有可能成立。
L2 < L3 < L1
L2 < L3:拒绝的length被覆盖为L2后,放行L3才可能反超胜出。
L3 < L1:修复后拒绝保留真实的L1,仍能盖过放行。
最小必要配置(与官方回归测试同构):
拒绝约束的单个web-resource-collection内、/*两模式「深层在前、浅层在后」、带空<auth-constraint>;
放行约束模式为子路径、无<auth-constraint>;
声明顺序:拒绝在前、放行在后;
请求 URI 同时命中深层与浅层。
| 情形 | 结果 |
|---|---|
| 拒绝集合只有一个命中模式 | L1胜出 → 拒绝(无覆盖) |
L3 >= L1(放行比拒绝深层还长) | 放行本就是最长匹配,属合理放行 |
L3 <= L2(放行 ≤ 浅层) | 放行无法反超被低估的longest=L2 |
| 声明顺序颠倒(拒绝在放行之后) | 拒绝先以真实L1占位,放行无法胜出 |
绕过成立需同时具备三点:同一拒绝集合内有序多模式、长度介于其间的放行子路径、拒绝先于放行声明,缺一不可。
URI = /level1/level2/level3/index.jsp
【9.0.120 · 漏洞版】 【9.0.121 · 修复版】
遍历 outer 集合(拒绝): 遍历 outer 集合(拒绝):
/level1/level2/level3/* → len=25 匹配 /level1/level2/level3/* → len=25 匹配 → length=25
/level1/* → len=9 覆盖 length /level1/* → 9 < length(25) → 被门控跳过
└ length=9 → longest=9, 结果=[outerDeny] └ length=25 → longest=25, 结果=[outerDeny]
遍历 inner 集合(放行): 遍历 inner 集合(放行):
/level1/level2/* → len=16 >= longest(9) /level1/level2/* → len=16 < longest(25) → 跳过
└ longest=16, results.clear() └ 结果仍=[outerDeny]
结果=[innerAllow]
└ 拒绝约束被挤出,匹配到放行约束
hasResourcePermission(innerAllow) hasResourcePermission(outerDeny)
→ 无 auth-constraint → 放行 200 越权 → 空角色且 auth-constraint → 拒绝 403
根因循环源码 RealmBase.java#L619-L681(突出 L647/L651/L655
某系统同一 Context 同时部署公开内容与后台数据目录,访问控制依赖声明式约束:
<!-- 拒绝:深层路径先声明,浅层兜底后声明 -->
<security-constraint>
<web-resource-collection>
<url-pattern>/app/private/data/*</url-pattern> <!-- 深层, 先声明 -->
<url-pattern>/app/*</url-pattern> <!-- 浅层兜底, 后声明 -->
</web-resource-collection>
<auth-constraint/>
</security-constraint>
<!-- 放行:/app/private/* 下的部分开放资源(长度介于上面两者之间) -->
<security-constraint>
<web-resource-collection>
<url-pattern>/app/private/*</url-pattern>
</web-resource-collection>
<!-- 无 auth-constraint -->
</security-constraint>
未认证请求:
curl -i http://victim/app/private/data/index.jsp
漏洞版(9.0.120):
HTTP/1.1 200 OK
...(受限业务数据)...
修复版(9.0.121):/app/private/data/*保持 len 20,放行len 14无法反超 →401/403。
同集合内/app/private/data/*(len20)被/app/*(len6)覆盖 →longest=6;放行/app/private/*(len14,满足6<14<20)被误判为最长,拒绝被丢弃,匿名访问成功。
白盒审计(能拿到配置/源码时)
按声明顺序枚举约束,抓“同一 collection 多/*模式 + 嵌套放行子路径”的组合:
# 提取约束及其 url-pattern,用 -B/-A 保留声明顺序
grep -nE '<security-constraint>|<url-pattern>|<auth-constraint>' WEB-INF/web.xml
人工或脚本核对是否存在满足L2 < L3 < L1的三元组,且拒绝先于放行声明。这是触发前提。
黑盒验证(无法读配置时)
先定位“看似应受保护”的深层路径(如/app/private/data/、/admin/…/manage/)。
对深层路径与“疑似公开子路径”分别发未认证请求,比较响应码与响应体差异。
若某深层路径在匿名且无跳转下返回200 + 业务内容,而相邻同级别路径返回401/403,即可作为待复核的命中点。
需要注意的是:黑盒场景下“该不该公开”需结合应用特点判断,命中点需以两版行为差或边界公式印证,避免误报。
优先选择满足L2 < L3 < L1且拒绝先声明的组合,更容易触发。
请求 URI 必须同时命中深层与浅层模式(落在深层子树的任意资源)。
保持匿名:不带Authorization/Cookie,排除“本身允许匿名”的干扰;同时试GET/POST双方法(约束可按 HTTP 方法过滤,方法不同结论可能不同)。
判定标准(避免误报):
绝对:同一份配置下,修复版返回401/403,而漏洞版返回200。
相对:深层路径匿名可达,而相邻同级受保护路径被拦。
确认修复语义:仅当放行模式长度“介于”拒绝集合深浅两层之间才构成绕过,否则属合理放行。
方法级差异:约束可按 HTTP 方法限定,同一路径下GET与POST的绕过结论可能不同。
/*兜底模式:模式恰为/*(len 2)走独立分支,与多层/*混排时可构造不同覆盖组合。
多集合 / 多放行:一个拒绝集合对多个放行约束、或多个拒绝集合并存时,results.clear()的相互影响可能产生更多变化。
跨分支交叉验证:9.0 / 10.1 / 11.0 共用同一段逻辑,若某分支行为存在差异,也可作为交叉验证靶点。
与路径归一化的交互:uri取自解码后的请求路径;若上游存在路径归一化/重写(如 RewriteValve),可能与约束匹配产生交互,值得单独研究。
环境:OpenJDK 17.0.20 + Apache Ant 1.10.14,源码仓库 tomcat tag 9.0.120。
9.0.120:ant compile+ant test-compile编译主源码(1840)与测试类(865);将官方testOverlappingConstraints回填后运行 → FAIL(AssertionError位于TestRealmBase.java:986,level3 的assertFalse返回 true ⇒ 放行)。
9.0.121:ant clean compile test-compile后运行官方自带同一测试 → PASS(Failures: 0)。
9.0.120 FAIL TestRealmBase.testOverlappingConstraints → level3 越权放行(应 403 反 200)
9.0.121 PASS Failures: 0 → level1/2/3 行为全部正确
官方回归测试 testOverlappingConstraints(TestRealmBase.java#L945-L987,9.0.121 版本
# 1) 准备
apt-get install -y ant
# 2) 9.0.120(漏洞版)
git -C /root/tomcat checkout 9.0.120
cd /root/tomcat && ant compile test-compile
# 将官方 testOverlappingConstraints 回填到 TestRealmBase,重跑:
# java -cp output/classes:output/testclasses:<junit4.13.2>:<hamcrest> \
# org.junit.runner.JUnitCore ... → 断言失败 ⇒ 漏洞成立
# 3) 9.0.121(修复版)
git checkout 9.0.121 && ant clean compile test-compile
# 官方自带 TestRealmBase.testOverlappingConstraints → 通过 ⇒ 已修复
这类缺陷不依赖具体数据,只要满足长度关系与声明顺序即可触发,属于授权选择逻辑的普遍性问题,可对其它“最长前缀选择”的授权/匹配器做同类审计。
7.x / 8.5.x / 9.x / 10.1.x / 11.x 共用该段逻辑,修复需要逐分支打补丁(这里给出 9.0.x 的b2c56ec)。
可对“约束集合(模式组合/顺序/方法)× 请求路径”做组合化的测试,系统排查同类排序/选择缺陷。
| 分支 | 升级目标 |
|---|---|
| 9.0.x | 9.0.121 |
| 10.1.x | 10.1.58 |
| 11.0.x | 11.0.25 |
影响版本:9.0.0.M1~9.0.120、10.1.0-M1~10.1.57、11.0.0-M1~11.0.24,以及已 EOL 的 8.5.0~8.5.100、7.0.0~7.0.109。修复 commit(9.0.x):b2c56ec8f20c66773a1a813034ffcdb60841f1cf。
将「长路径宽松约束在前、短路径严格约束在后」的组合调整为更严格约束前置声明,并审查同一web-resource-collection内多个/*模式的顺序。
枚举约束并保留声明顺序。
定位“同一 collection 含 ≥2 个/*模式”且“深层在前、浅层在后”的约束。
核对是否存在L2 < L3 < L1的嵌套放行子路径。
确认拒绝先于放行声明。
探针:对深层路径未认证访问,预期401/403;若返回200并含业务内容,则确认受影响。
grep -nE '<security-constraint>|<url-pattern>|<auth-constraint>' WEB-INF/web.xml
curl -i http://<host>:<port>/app/private/data/index.jsp # 不带凭证
本报告结论基于官方公告、源码(tag 9.0.120 / 9.0.121)及本地一次真实编译的端到端测试;未使用公开 PoC。
9.0.120 的失败结果通过「回填官方回归测试」触发原生未修复字节码得出;9.0.121 直接使用官方自带测试。
URI 来源细节:TesterRequest.java#L84-L96
报告生成环境:Apache Tomcat 源码仓库(9.0.120 / 9.0.121),OpenJDK 17,Apache Ant 1.10.14。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。