1. 项目概述从一次内部安全扫描说起上周团队在做季度安全审计自动化扫描工具突然弹出一个中危告警指向我们项目中一个再熟悉不过的依赖——Hutool。告警信息很简短“Hutool组件存在潜在的SPEL表达式注入风险可能导致远程命令执行”。我心里咯噔一下Hutool可是我们Java后端项目的“瑞士军刀”从字符串处理、日期转换到HTTP客户端、加密解密几乎无处不在。如果它真有问题影响面可就大了。这个告警立刻把我拉回了现实在享受开源组件带来的便捷时我们是否对其潜在的安全风险保持了足够的警惕这次我就以Hutool这个具体的案例为切入点和大家深度拆解一下“SPEL表达式注入”这个听起来有点学术但实际危害巨大的漏洞。无论你是正在使用Hutool的开发者还是关心应用安全的架构师理解这个漏洞的原理、影响和修复方案都至关重要。这不仅仅是一个工具的问题更是我们如何看待和治理第三方依赖安全性的一个缩影。2. Hutool与SPEL风险的交汇点2.1 Hutool是什么为什么它如此流行Hutool是一个小而全的Java工具类库它的设计哲学是“减少代码搜索成本避免重复造轮子”。我最初接触它是因为被其简洁的API设计所吸引。比如想计算两个日期之间的天数差用原生JDK你得写好几行还可能掉进Calendar的坑里而用Hutool一行DateUtil.betweenDay(start, end, true)就搞定了。这种“开箱即用”的体验让它在中小型项目和个人开发者中迅速流行起来。它的模块覆盖非常广核心工具StrUtil字符串、DateUtil日期、NumberUtil数字等解决了日常开发中80%的琐碎问题。扩展模块HTTP客户端、缓存、JSON/XML解析、加密解密、Excel读写等。特别是其Excel模块因为API简单避免了直接操作POI的复杂性被大量用于报表导出功能。第三方集成对Spring、JWT、模板引擎等的封装。正是这种“全能”特性使得一旦Hutool核心出现安全漏洞几乎所有使用它的模块都可能成为攻击入口风险被急剧放大。2.2 SPEL表达式强大的双刃剑SPEL全称Spring Expression Language是Spring框架提供的一种强大的表达式语言。它最初的设计目的是为了在Spring的XML配置和注解中能够动态地求值和管理Bean对象。你可以把它想象成一个能在运行时“计算”的迷你脚本引擎。它的能力非常强大访问对象属性#user.name调用方法#article.getTitle()数学运算T(java.lang.Math).random() * 100.0逻辑判断#age 18 ? ‘adult’ : ‘minor’在Spring Security、Spring Data、Thymeleaf等框架中SPEL被广泛用于实现权限控制(PreAuthorize(“hasRole(‘ADMIN’)”))、查询映射(Query(“select u from User u where u.name ?#{[0]}”))等高级功能。注意SPEL的强大之处在于它能够通过T()操作符直接调用Java类的静态方法甚至通过new操作符实例化对象。这扇“后门”一旦对不可信输入开放后果不堪设想。2.3 风险交汇Hutool在哪里用到了SPEL问题就出在Hutool的某些“高级”或“集成”功能上。根据漏洞分析和社区讨论风险点主要集中在配置解析和模板渲染相关场景。一个典型的危险模式是Hutool的某个工具方法为了提供灵活性允许开发者在配置字符串中嵌入SPEL表达式并在内部使用Spring的StandardEvaluationContext来解析它。StandardEvaluationContext是SPEL的“完全体”解析上下文它拥有对表达式完整的访问权限包括执行任意代码。例如假设有一个配置项用于定义文件保存路径原本设计是/data/${app.name}/upload其中${app.name}打算被替换为应用名。但如果解析器错误地或默认地使用了SPEL并且这个配置项的值来自用户输入如HTTP请求参数、上传的配置文件那么攻击者就可以传入/data/#{T(java.lang.Runtime).getRuntime().exec(‘calc’)}/upload这样的恶意字符串。关键在于Hutool作为工具库其默认行为应该是最安全的。如果某个功能确实需要SPEL的动态能力它必须作为一个明确的、可选的特性并且默认关闭同时给出醒目的安全警告。而风险往往源于默认开启、文档未明确警示、且输入源不可控。3. 漏洞原理深度拆解与复现3.1 漏洞产生的核心链条要理解这个漏洞我们需要把它拆解成一个清晰的攻击链条。这个链条由三个关键环节构成缺一不可存在SPEL解析入口Hutool的某个类或方法内部调用了Spring的SpelExpressionParser并且使用了StandardEvaluationContext作为上下文。这是漏洞的“发动机”。输入源可控传递给这个SPEL解析器的字符串其全部或部分内容可以被外部攻击者控制。这是攻击的“燃料”。常见来源包括HTTP请求参数GET/POST、请求头、Cookie、从数据库或配置中心读取的未经验证的数据、用户上传的文件内容等。安全上下文缺失解析过程没有进行任何白名单过滤、没有对T()和new等危险操作符进行限制、也没有使用更安全的SimpleEvaluationContext替代StandardEvaluationContext。这是漏洞的“保险丝缺失”。当这三个条件同时满足时攻击者就可以精心构造一个SPEL表达式作为输入这个表达式在服务器端被Hutool的相关代码解析并执行从而达到在服务器上执行任意命令的目的。3.2 搭建一个简易的复现环境为了让大家有更直观的感受我们来模拟一个可能存在风险的场景。请注意以下代码仅为教学演示漏洞原理切勿在生产环境中使用。假设我们有一个使用Hutool的简易配置中心客户端它从远端拉取一段配置字符串并用Hutool的某个假设的ConfigUtil.evalDynamicConfig方法来处理其中的动态变量。import cn.hutool.core.util.StrUtil; // 假设存在这样一个不安全的工具方法模拟漏洞点 public class UnsafeConfigParser { private static final SpelExpressionParser parser new SpelExpressionParser(); public static Object parseExpression(String configExpr) { // 危险操作使用StandardEvaluationContext且直接解析未经验证的输入 StandardEvaluationContext context new StandardEvaluationContext(); // 这里可能会为了“功能强大”而设置一些根对象但反而扩大了攻击面 // context.setRootObject(someBean); Expression exp parser.parseExpression(configExpr); return exp.getValue(context); } } // 一个脆弱的服务类 Service public class VulnerableService { public String processConfig(String userProvidedConfig) { // 开发者本意是让用户配置简单的占位符如“server.port${port}” // 但直接传入了用户控制的字符串 try { Object result UnsafeConfigParser.parseExpression(userProvidedConfig); return “Config processed: ” result; } catch (Exception e) { return “Error processing config”; } } }3.3 发动攻击构造恶意SPEL载荷现在攻击者向VulnerableService的processConfig接口发送了一个HTTP请求参数userProvidedConfig为以下内容T(java.lang.Runtime).getRuntime().exec(‘open -a Calculator’)或者为了绕过简单的字符串检测可能会使用编码或拼接T(String).getClass().forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(null).exec(“calc”)当这个字符串被UnsafeConfigParser.parseExpression处理时SpelExpressionParser将其解析为一个SPEL表达式。StandardEvaluationContext提供了完整的反射和类加载权限。表达式T(java.lang.Runtime).getRuntime()成功获取到当前JVM的Runtime对象。.exec(‘calc’)方法被调用在服务器上启动计算器程序Windows或打开计算器应用macOS。这就完成了一次远程命令执行RCE。如果服务器权限较高攻击者可以执行任意命令如wget下载木马、curl外传数据、rm -rf删除文件等直接导致服务器被完全控制。实操心得在复现这类漏洞时我通常会在Payload中使用ping命令并带上一个唯一标识的DNS记录如ping -c 1 your-unique-subdomain.dnslog.cn通过查看DNS解析日志来确认漏洞是否存在且可利用这是一种无副作用且隐蔽的验证方式。绝对不要在测试环境中使用rm、format等危险命令。4. 影响范围与严重性评估4.1 哪些项目和版本受影响这是一个需要具体分析的问题。Hutool本身是一个工具集并非所有版本、所有模块都必然存在SPEL解析功能。风险通常与特定功能绑定。高危功能模块需要重点关注与“表达式解析”、“动态配置”、“模板渲染”相关的模块。例如处理自定义格式字符串的工具、集成Spring环境变量解析的工具等。开发者需要审查自己项目中是否用到了这类功能。版本范围如果Hutool官方确认了某个特定漏洞如CVE编号则会指明影响的版本范围例如v5.x 至 v5.x.x。在官方未明确前风险存在于所有默认或不当使用StandardEvaluationContext解析用户输入的版本中。关键在于使用方式而非绝对版本号。间接依赖风险你的项目可能没有直接显式使用Hutool的风险API但你所依赖的另一个第三方库比如某个快速开发框架内部使用了有风险的Hutool版本。这种传递性依赖更隐蔽需要用mvn dependency:tree或Gradle的依赖分析工具来排查。4.2 实际危害场景推演假设漏洞真实存在并被利用攻击者能做什么我们推演几个场景供应链攻击攻击者发现某个流行开源项目A使用了有漏洞的Hutool版本处理用户上传的配置文件。于是他伪装成普通用户在A项目提供的在线配置生成页面输入恶意SPEL代码并生成配置文件。当其他用户下载并使用这个“有毒”的配置文件时攻击者的代码就在受害用户的本地或服务器环境中执行了。这种攻击利用了信任链危害极大。SaaS平台配置劫持一个提供在线服务的SaaS平台允许用户通过JSON或YAML自定义工作流规则。平台后端使用Hutool来解析这些规则中的条件表达式。攻击者注册账号后在规则条件中注入SPEL载荷当平台后台引擎解析并执行该规则时攻击者就获得了在SaaS平台服务器上执行命令的能力可以窃取其他用户数据、破坏服务。内部管理系统沦陷公司内部的一个运维管理系统使用Hutool渲染报表模板。报表的筛选条件参数来自URL。攻击者可能是内部人员或通过XSS等手段构造一个包含恶意SPEL的URL管理员或任何有权限的用户点击后命令就在管理后台服务器执行内网防线瞬间被突破。4.3 为什么这个风险容易被忽视“工具类”的心理盲区开发者普遍认为工具类库是“无害”的辅助代码不像Web框架、数据库驱动那样处于安全攻防的前线。对其安全审查往往不严格。功能强大掩盖了复杂性Hutool设计初衷是简化开发但为了满足各种场景其内部实现可能变得复杂。一个简单的get方法背后可能隐藏着一条复杂的解析链。开发者只看API文档的表面功能很难洞察其内部实现的安全边界。默认不安全的配置如果SPEL解析功能默认开启且使用StandardEvaluationContext那么开发者在不了解其风险的情况下很容易就踩坑。安全应该是最优的默认配置。文档缺失警告如果官方文档没有在相关功能的显著位置标注“警告此功能涉及表达式解析请勿传入不可信输入”开发者无从知晓风险。5. 代码审计与排查实战指南5.1 如何定位项目中的风险点光知道原理不够我们得在自己的项目里把它找出来。我总结了一套“三步排查法”第一步依赖扫描锁定版本使用命令检查项目中引入的Hutool具体版本# Maven项目 mvn dependency:tree | grep hutool # 或使用专门的工具 mvn org.owasp:dependency-check-maven:check # Gradle项目 gradle dependencies | grep hutool记录下确切的版本号然后去Hutool的GitHub仓库、安全公告平台如CNVD、NVD搜索该版本是否有相关的CVE漏洞信息。第二步全局代码搜索关键词在你的IDE或代码仓库中全局搜索以下关键词SpelExpressionParserStandardEvaluationContextparseExpressiongetValue(与Expression对象连用时)Value(Spring注解但注意其解析也可能涉及SPEL不过这是Spring核心的责任)搜索Hutool中可能与表达式、模板、配置动态解析相关的类名如包含Eval、Expression、Template、Placeholder等字眼的类。第三步动态数据流分析这是最关键的一步。找到上述代码位置后需要人工或借助工具如静态应用安全测试SAST工具进行数据流分析溯源找到传入parseExpression方法的那个字符串变量我们叫它source。追根逆向追踪source这个变量的值是从哪里来的。一直追溯到它的源头可能是HttpServletRequest.getParameter(...)RequestParam String configFileInputStream.read(...)读取的用户上传文件RedisTemplate.opsForValue().get(...)从外部存储读取数据库查询结果ResultSet.getString(...)判定如果最终源头是任何来自系统外部的、用户可控的输入那么这个点就是高危风险点。5.2 重点审查的Hutool工具类根据社区讨论和常见模式以下或类似功能的Hutool类需要额外关注工具类/方法 (示例)可能的风险场景审查要点配置相关工具解析外部配置文件yml, properties时如果支持${...}或#{}动态语法且后端使用SPEL。检查配置值是否可能来自不可信源如云端同步、用户上传。模板引擎封装对Velocity、Freemarker、Thymeleaf等模板引擎的封装类。模板引擎本身可能支持表达式需确认Hutool的封装是否引入了额外的解析层。检查模板内容是否用户可控渲染上下文是否被过度暴露。自定义字符串格式化/解析工具某些工具可能为了高级替换功能内部采用了表达式解析。搜索代码中是否使用了Hutool进行复杂的字符串“计算”或“替换”。与Spring集成的工具类为了方便在非Spring环境中使用Spring特性如环境变量解析而封装的工具。确认这些工具类在解析Spring EL时是否使用了安全的上下文。排查技巧除了代码搜索在测试环境或预发布环境中可以尝试在可能的输入点注入一些无害的探测Payload如${11}或#{11}观察返回结果或日志中是否出现了计算后的结果2。如果出现了就证明该入口存在表达式解析行为需要进一步评估安全性。5.3 使用SAST工具进行辅助扫描人工审计耗时耗力对于大型项目可以借助静态应用安全测试工具进行初步筛查。许多商业或开源SAST工具都内置了检测“表达式注入”漏洞的规则。SonarQube配置相应的Java安全规则包可以对代码进行扫描。SpotBugs(配合Find Security Bugs插件)一款优秀的开源静态字节码分析工具可以检测出潜在的SPEL注入模式。商业SAST产品如Checkmarx、Fortify、Coverity等它们有更强大的数据流分析引擎。需要注意的是工具扫描会产生大量告警其中包含误报。需要安全人员或资深开发对告警进行人工研判特别是要完成上面提到的“数据流溯源”确认从用户输入到危险函数的路径是否通畅。6. 修复方案与安全加固实践6.1 立即缓解措施如果确认存在风险点且暂时无法升级版本或修改代码可以考虑以下临时缓解措施输入严格过滤在所有识别出的风险入口对用户输入进行强过滤。禁止输入中包含T(、new、#{、${、反引号、分号等危险字符。但黑名单方式总有可能被绕过例如使用Unicode编码、大小写变形等。// 示例简单的黑名单过滤不推荐作为唯一手段 public static boolean containsDangerousExpr(String input) { String[] blacklist {“T(“, “new “, “#{“, “${“, “”, “;”, “java.lang.Runtime”}; for (String s : blacklist) { if (input.contains(s)) { return true; } } return false; }沙箱环境运行如果业务必须执行动态逻辑考虑在独立的、权限受到严格限制的沙箱环境如单独的Docker容器、使用Java SecurityManager或更现代的jdk.jshell进行隔离中执行这些操作。但这会引入显著的复杂性和性能开销。6.2 根本解决方案升级与安全编码方案一升级Hutool版本如果官方已修复这是最推荐的做法。关注Hutool官方GitHub仓库的Release Notes和安全公告。如果官方发布了修复该漏洞的版本例如v5.8.x应尽快安排升级测试。 升级后务必回归测试使用了相关功能模块的业务代码确保兼容性。方案二修改代码使用SimpleEvaluationContext如果风险代码在自己的项目中且必须使用SPEL功能那么最核心的修复是将StandardEvaluationContext替换为SimpleEvaluationContext。SimpleEvaluationContext是Spring 4.2引入的受限上下文旨在支持数据绑定和简单的属性查找但明确禁止了类型引用T(...)、构造函数调用new、Bean引用等危险操作。// 修复后的安全解析方法 import org.springframework.expression.spel.support.SimpleEvaluationContext; public class SafeConfigParser { private static final SpelExpressionParser parser new SpelExpressionParser(); public static Object parseExpressionSafely(String configExpr) { // 创建SimpleEvaluationContext并仅暴露必要的属性访问权限 SimpleEvaluationContext context SimpleEvaluationContext.forReadOnlyDataBinding() .withRootObject(null) // 根据实际情况设置根对象 .build(); // 尝试解析 Expression exp parser.parseExpression(configExpr); // 在受限上下文中求值 return exp.getValue(context); } }使用SimpleEvaluationContext后之前那个恶意的T(Runtime).exec表达式在getValue时会直接抛出异常如SpelEvaluationException: EL1004E: Method call: Method exec(java.lang.String) cannot be found on type java.lang.Runtime从而有效阻断了攻击。方案三彻底移除或重构风险功能重新评估业务场景是否真的需要如此动态的表达式功能很多时候一个简单的占位符替换如使用StrUtil.format或MessageFormat就能满足需求。如果不需要直接移除对SPEL的依赖改用更简单、更可控的实现方式是消除风险最彻底的方法。6.3 建立长期的依赖安全治理流程修复一个漏洞是“治标”建立流程才是“治本”。软件物料清单管理使用cyclonedx-maven-plugin等工具生成SBOM清晰掌握所有直接和传递依赖。自动化漏洞扫描集成CI/CD将OWASP Dependency-Check、Snyk、GitHub Dependabot等工具集成到持续集成流水线中每次构建都自动检查依赖是否有已知漏洞。定期依赖升级机制不要长期使用一个古老的版本。制定计划定期如每季度评估和升级主要依赖项到稳定版本。安全编码规范在团队内部明确规范禁止将任何用户可控的输入直接传递给表达式解析器、脚本引擎、反序列化接口等危险功能。代码审查重点在Code Review时将“外部输入处理”和“第三方库风险API调用”作为重点审查项。7. 从Hutool漏洞看开源组件安全治理这次对Hutool潜在SPEL风险的深入分析不仅仅是为了解决一个具体工具的问题更是为我们敲响了开源组件安全治理的警钟。在现代软件开发中我们站在巨人的肩膀上但巨人的肩膀上也可能有裂缝。我个人的体会是对待像Hutool这样深入项目肌理的工具库我们需要从“使用者”思维转变为“共治者”思维。这意味着主动关注订阅你核心依赖项目的GitHub Release、安全邮件列表。不要等到漏洞爆发才行动。深度理解对于提供“强大”或“灵活”功能如表达式解析、动态加载、反射调用的组件花点时间了解其实现原理和安全边界。阅读其关键功能的源码并不像想象中那么难。最小化使用只引入你真正需要的模块。如果只是用Hutool做日期转换就不要引入整个hutool-all依赖。这能有效减少攻击面。贡献社区如果你发现了问题或有了改进方案积极向开源社区提交Issue或PR。你今天修复的漏洞可能会保护成千上万个项目。最后分享一个在应急响应时的小技巧当安全扫描告警或出现安全事件时第一时间去项目依赖树的“叶子节点”那些你几乎没听过的、深层次的传递依赖里找问题往往比在顶层框架中寻找更快。很多严重漏洞都隐藏在这些不起眼的工具库中。保持警惕安全开发是我们每个构建数字世界的人的责任。
Hutool SPEL表达式注入漏洞深度解析与安全加固实践
发布时间:2026/6/30 11:58:20
1. 项目概述从一次内部安全扫描说起上周团队在做季度安全审计自动化扫描工具突然弹出一个中危告警指向我们项目中一个再熟悉不过的依赖——Hutool。告警信息很简短“Hutool组件存在潜在的SPEL表达式注入风险可能导致远程命令执行”。我心里咯噔一下Hutool可是我们Java后端项目的“瑞士军刀”从字符串处理、日期转换到HTTP客户端、加密解密几乎无处不在。如果它真有问题影响面可就大了。这个告警立刻把我拉回了现实在享受开源组件带来的便捷时我们是否对其潜在的安全风险保持了足够的警惕这次我就以Hutool这个具体的案例为切入点和大家深度拆解一下“SPEL表达式注入”这个听起来有点学术但实际危害巨大的漏洞。无论你是正在使用Hutool的开发者还是关心应用安全的架构师理解这个漏洞的原理、影响和修复方案都至关重要。这不仅仅是一个工具的问题更是我们如何看待和治理第三方依赖安全性的一个缩影。2. Hutool与SPEL风险的交汇点2.1 Hutool是什么为什么它如此流行Hutool是一个小而全的Java工具类库它的设计哲学是“减少代码搜索成本避免重复造轮子”。我最初接触它是因为被其简洁的API设计所吸引。比如想计算两个日期之间的天数差用原生JDK你得写好几行还可能掉进Calendar的坑里而用Hutool一行DateUtil.betweenDay(start, end, true)就搞定了。这种“开箱即用”的体验让它在中小型项目和个人开发者中迅速流行起来。它的模块覆盖非常广核心工具StrUtil字符串、DateUtil日期、NumberUtil数字等解决了日常开发中80%的琐碎问题。扩展模块HTTP客户端、缓存、JSON/XML解析、加密解密、Excel读写等。特别是其Excel模块因为API简单避免了直接操作POI的复杂性被大量用于报表导出功能。第三方集成对Spring、JWT、模板引擎等的封装。正是这种“全能”特性使得一旦Hutool核心出现安全漏洞几乎所有使用它的模块都可能成为攻击入口风险被急剧放大。2.2 SPEL表达式强大的双刃剑SPEL全称Spring Expression Language是Spring框架提供的一种强大的表达式语言。它最初的设计目的是为了在Spring的XML配置和注解中能够动态地求值和管理Bean对象。你可以把它想象成一个能在运行时“计算”的迷你脚本引擎。它的能力非常强大访问对象属性#user.name调用方法#article.getTitle()数学运算T(java.lang.Math).random() * 100.0逻辑判断#age 18 ? ‘adult’ : ‘minor’在Spring Security、Spring Data、Thymeleaf等框架中SPEL被广泛用于实现权限控制(PreAuthorize(“hasRole(‘ADMIN’)”))、查询映射(Query(“select u from User u where u.name ?#{[0]}”))等高级功能。注意SPEL的强大之处在于它能够通过T()操作符直接调用Java类的静态方法甚至通过new操作符实例化对象。这扇“后门”一旦对不可信输入开放后果不堪设想。2.3 风险交汇Hutool在哪里用到了SPEL问题就出在Hutool的某些“高级”或“集成”功能上。根据漏洞分析和社区讨论风险点主要集中在配置解析和模板渲染相关场景。一个典型的危险模式是Hutool的某个工具方法为了提供灵活性允许开发者在配置字符串中嵌入SPEL表达式并在内部使用Spring的StandardEvaluationContext来解析它。StandardEvaluationContext是SPEL的“完全体”解析上下文它拥有对表达式完整的访问权限包括执行任意代码。例如假设有一个配置项用于定义文件保存路径原本设计是/data/${app.name}/upload其中${app.name}打算被替换为应用名。但如果解析器错误地或默认地使用了SPEL并且这个配置项的值来自用户输入如HTTP请求参数、上传的配置文件那么攻击者就可以传入/data/#{T(java.lang.Runtime).getRuntime().exec(‘calc’)}/upload这样的恶意字符串。关键在于Hutool作为工具库其默认行为应该是最安全的。如果某个功能确实需要SPEL的动态能力它必须作为一个明确的、可选的特性并且默认关闭同时给出醒目的安全警告。而风险往往源于默认开启、文档未明确警示、且输入源不可控。3. 漏洞原理深度拆解与复现3.1 漏洞产生的核心链条要理解这个漏洞我们需要把它拆解成一个清晰的攻击链条。这个链条由三个关键环节构成缺一不可存在SPEL解析入口Hutool的某个类或方法内部调用了Spring的SpelExpressionParser并且使用了StandardEvaluationContext作为上下文。这是漏洞的“发动机”。输入源可控传递给这个SPEL解析器的字符串其全部或部分内容可以被外部攻击者控制。这是攻击的“燃料”。常见来源包括HTTP请求参数GET/POST、请求头、Cookie、从数据库或配置中心读取的未经验证的数据、用户上传的文件内容等。安全上下文缺失解析过程没有进行任何白名单过滤、没有对T()和new等危险操作符进行限制、也没有使用更安全的SimpleEvaluationContext替代StandardEvaluationContext。这是漏洞的“保险丝缺失”。当这三个条件同时满足时攻击者就可以精心构造一个SPEL表达式作为输入这个表达式在服务器端被Hutool的相关代码解析并执行从而达到在服务器上执行任意命令的目的。3.2 搭建一个简易的复现环境为了让大家有更直观的感受我们来模拟一个可能存在风险的场景。请注意以下代码仅为教学演示漏洞原理切勿在生产环境中使用。假设我们有一个使用Hutool的简易配置中心客户端它从远端拉取一段配置字符串并用Hutool的某个假设的ConfigUtil.evalDynamicConfig方法来处理其中的动态变量。import cn.hutool.core.util.StrUtil; // 假设存在这样一个不安全的工具方法模拟漏洞点 public class UnsafeConfigParser { private static final SpelExpressionParser parser new SpelExpressionParser(); public static Object parseExpression(String configExpr) { // 危险操作使用StandardEvaluationContext且直接解析未经验证的输入 StandardEvaluationContext context new StandardEvaluationContext(); // 这里可能会为了“功能强大”而设置一些根对象但反而扩大了攻击面 // context.setRootObject(someBean); Expression exp parser.parseExpression(configExpr); return exp.getValue(context); } } // 一个脆弱的服务类 Service public class VulnerableService { public String processConfig(String userProvidedConfig) { // 开发者本意是让用户配置简单的占位符如“server.port${port}” // 但直接传入了用户控制的字符串 try { Object result UnsafeConfigParser.parseExpression(userProvidedConfig); return “Config processed: ” result; } catch (Exception e) { return “Error processing config”; } } }3.3 发动攻击构造恶意SPEL载荷现在攻击者向VulnerableService的processConfig接口发送了一个HTTP请求参数userProvidedConfig为以下内容T(java.lang.Runtime).getRuntime().exec(‘open -a Calculator’)或者为了绕过简单的字符串检测可能会使用编码或拼接T(String).getClass().forName(“java.lang.Runtime”).getMethod(“getRuntime”).invoke(null).exec(“calc”)当这个字符串被UnsafeConfigParser.parseExpression处理时SpelExpressionParser将其解析为一个SPEL表达式。StandardEvaluationContext提供了完整的反射和类加载权限。表达式T(java.lang.Runtime).getRuntime()成功获取到当前JVM的Runtime对象。.exec(‘calc’)方法被调用在服务器上启动计算器程序Windows或打开计算器应用macOS。这就完成了一次远程命令执行RCE。如果服务器权限较高攻击者可以执行任意命令如wget下载木马、curl外传数据、rm -rf删除文件等直接导致服务器被完全控制。实操心得在复现这类漏洞时我通常会在Payload中使用ping命令并带上一个唯一标识的DNS记录如ping -c 1 your-unique-subdomain.dnslog.cn通过查看DNS解析日志来确认漏洞是否存在且可利用这是一种无副作用且隐蔽的验证方式。绝对不要在测试环境中使用rm、format等危险命令。4. 影响范围与严重性评估4.1 哪些项目和版本受影响这是一个需要具体分析的问题。Hutool本身是一个工具集并非所有版本、所有模块都必然存在SPEL解析功能。风险通常与特定功能绑定。高危功能模块需要重点关注与“表达式解析”、“动态配置”、“模板渲染”相关的模块。例如处理自定义格式字符串的工具、集成Spring环境变量解析的工具等。开发者需要审查自己项目中是否用到了这类功能。版本范围如果Hutool官方确认了某个特定漏洞如CVE编号则会指明影响的版本范围例如v5.x 至 v5.x.x。在官方未明确前风险存在于所有默认或不当使用StandardEvaluationContext解析用户输入的版本中。关键在于使用方式而非绝对版本号。间接依赖风险你的项目可能没有直接显式使用Hutool的风险API但你所依赖的另一个第三方库比如某个快速开发框架内部使用了有风险的Hutool版本。这种传递性依赖更隐蔽需要用mvn dependency:tree或Gradle的依赖分析工具来排查。4.2 实际危害场景推演假设漏洞真实存在并被利用攻击者能做什么我们推演几个场景供应链攻击攻击者发现某个流行开源项目A使用了有漏洞的Hutool版本处理用户上传的配置文件。于是他伪装成普通用户在A项目提供的在线配置生成页面输入恶意SPEL代码并生成配置文件。当其他用户下载并使用这个“有毒”的配置文件时攻击者的代码就在受害用户的本地或服务器环境中执行了。这种攻击利用了信任链危害极大。SaaS平台配置劫持一个提供在线服务的SaaS平台允许用户通过JSON或YAML自定义工作流规则。平台后端使用Hutool来解析这些规则中的条件表达式。攻击者注册账号后在规则条件中注入SPEL载荷当平台后台引擎解析并执行该规则时攻击者就获得了在SaaS平台服务器上执行命令的能力可以窃取其他用户数据、破坏服务。内部管理系统沦陷公司内部的一个运维管理系统使用Hutool渲染报表模板。报表的筛选条件参数来自URL。攻击者可能是内部人员或通过XSS等手段构造一个包含恶意SPEL的URL管理员或任何有权限的用户点击后命令就在管理后台服务器执行内网防线瞬间被突破。4.3 为什么这个风险容易被忽视“工具类”的心理盲区开发者普遍认为工具类库是“无害”的辅助代码不像Web框架、数据库驱动那样处于安全攻防的前线。对其安全审查往往不严格。功能强大掩盖了复杂性Hutool设计初衷是简化开发但为了满足各种场景其内部实现可能变得复杂。一个简单的get方法背后可能隐藏着一条复杂的解析链。开发者只看API文档的表面功能很难洞察其内部实现的安全边界。默认不安全的配置如果SPEL解析功能默认开启且使用StandardEvaluationContext那么开发者在不了解其风险的情况下很容易就踩坑。安全应该是最优的默认配置。文档缺失警告如果官方文档没有在相关功能的显著位置标注“警告此功能涉及表达式解析请勿传入不可信输入”开发者无从知晓风险。5. 代码审计与排查实战指南5.1 如何定位项目中的风险点光知道原理不够我们得在自己的项目里把它找出来。我总结了一套“三步排查法”第一步依赖扫描锁定版本使用命令检查项目中引入的Hutool具体版本# Maven项目 mvn dependency:tree | grep hutool # 或使用专门的工具 mvn org.owasp:dependency-check-maven:check # Gradle项目 gradle dependencies | grep hutool记录下确切的版本号然后去Hutool的GitHub仓库、安全公告平台如CNVD、NVD搜索该版本是否有相关的CVE漏洞信息。第二步全局代码搜索关键词在你的IDE或代码仓库中全局搜索以下关键词SpelExpressionParserStandardEvaluationContextparseExpressiongetValue(与Expression对象连用时)Value(Spring注解但注意其解析也可能涉及SPEL不过这是Spring核心的责任)搜索Hutool中可能与表达式、模板、配置动态解析相关的类名如包含Eval、Expression、Template、Placeholder等字眼的类。第三步动态数据流分析这是最关键的一步。找到上述代码位置后需要人工或借助工具如静态应用安全测试SAST工具进行数据流分析溯源找到传入parseExpression方法的那个字符串变量我们叫它source。追根逆向追踪source这个变量的值是从哪里来的。一直追溯到它的源头可能是HttpServletRequest.getParameter(...)RequestParam String configFileInputStream.read(...)读取的用户上传文件RedisTemplate.opsForValue().get(...)从外部存储读取数据库查询结果ResultSet.getString(...)判定如果最终源头是任何来自系统外部的、用户可控的输入那么这个点就是高危风险点。5.2 重点审查的Hutool工具类根据社区讨论和常见模式以下或类似功能的Hutool类需要额外关注工具类/方法 (示例)可能的风险场景审查要点配置相关工具解析外部配置文件yml, properties时如果支持${...}或#{}动态语法且后端使用SPEL。检查配置值是否可能来自不可信源如云端同步、用户上传。模板引擎封装对Velocity、Freemarker、Thymeleaf等模板引擎的封装类。模板引擎本身可能支持表达式需确认Hutool的封装是否引入了额外的解析层。检查模板内容是否用户可控渲染上下文是否被过度暴露。自定义字符串格式化/解析工具某些工具可能为了高级替换功能内部采用了表达式解析。搜索代码中是否使用了Hutool进行复杂的字符串“计算”或“替换”。与Spring集成的工具类为了方便在非Spring环境中使用Spring特性如环境变量解析而封装的工具。确认这些工具类在解析Spring EL时是否使用了安全的上下文。排查技巧除了代码搜索在测试环境或预发布环境中可以尝试在可能的输入点注入一些无害的探测Payload如${11}或#{11}观察返回结果或日志中是否出现了计算后的结果2。如果出现了就证明该入口存在表达式解析行为需要进一步评估安全性。5.3 使用SAST工具进行辅助扫描人工审计耗时耗力对于大型项目可以借助静态应用安全测试工具进行初步筛查。许多商业或开源SAST工具都内置了检测“表达式注入”漏洞的规则。SonarQube配置相应的Java安全规则包可以对代码进行扫描。SpotBugs(配合Find Security Bugs插件)一款优秀的开源静态字节码分析工具可以检测出潜在的SPEL注入模式。商业SAST产品如Checkmarx、Fortify、Coverity等它们有更强大的数据流分析引擎。需要注意的是工具扫描会产生大量告警其中包含误报。需要安全人员或资深开发对告警进行人工研判特别是要完成上面提到的“数据流溯源”确认从用户输入到危险函数的路径是否通畅。6. 修复方案与安全加固实践6.1 立即缓解措施如果确认存在风险点且暂时无法升级版本或修改代码可以考虑以下临时缓解措施输入严格过滤在所有识别出的风险入口对用户输入进行强过滤。禁止输入中包含T(、new、#{、${、反引号、分号等危险字符。但黑名单方式总有可能被绕过例如使用Unicode编码、大小写变形等。// 示例简单的黑名单过滤不推荐作为唯一手段 public static boolean containsDangerousExpr(String input) { String[] blacklist {“T(“, “new “, “#{“, “${“, “”, “;”, “java.lang.Runtime”}; for (String s : blacklist) { if (input.contains(s)) { return true; } } return false; }沙箱环境运行如果业务必须执行动态逻辑考虑在独立的、权限受到严格限制的沙箱环境如单独的Docker容器、使用Java SecurityManager或更现代的jdk.jshell进行隔离中执行这些操作。但这会引入显著的复杂性和性能开销。6.2 根本解决方案升级与安全编码方案一升级Hutool版本如果官方已修复这是最推荐的做法。关注Hutool官方GitHub仓库的Release Notes和安全公告。如果官方发布了修复该漏洞的版本例如v5.8.x应尽快安排升级测试。 升级后务必回归测试使用了相关功能模块的业务代码确保兼容性。方案二修改代码使用SimpleEvaluationContext如果风险代码在自己的项目中且必须使用SPEL功能那么最核心的修复是将StandardEvaluationContext替换为SimpleEvaluationContext。SimpleEvaluationContext是Spring 4.2引入的受限上下文旨在支持数据绑定和简单的属性查找但明确禁止了类型引用T(...)、构造函数调用new、Bean引用等危险操作。// 修复后的安全解析方法 import org.springframework.expression.spel.support.SimpleEvaluationContext; public class SafeConfigParser { private static final SpelExpressionParser parser new SpelExpressionParser(); public static Object parseExpressionSafely(String configExpr) { // 创建SimpleEvaluationContext并仅暴露必要的属性访问权限 SimpleEvaluationContext context SimpleEvaluationContext.forReadOnlyDataBinding() .withRootObject(null) // 根据实际情况设置根对象 .build(); // 尝试解析 Expression exp parser.parseExpression(configExpr); // 在受限上下文中求值 return exp.getValue(context); } }使用SimpleEvaluationContext后之前那个恶意的T(Runtime).exec表达式在getValue时会直接抛出异常如SpelEvaluationException: EL1004E: Method call: Method exec(java.lang.String) cannot be found on type java.lang.Runtime从而有效阻断了攻击。方案三彻底移除或重构风险功能重新评估业务场景是否真的需要如此动态的表达式功能很多时候一个简单的占位符替换如使用StrUtil.format或MessageFormat就能满足需求。如果不需要直接移除对SPEL的依赖改用更简单、更可控的实现方式是消除风险最彻底的方法。6.3 建立长期的依赖安全治理流程修复一个漏洞是“治标”建立流程才是“治本”。软件物料清单管理使用cyclonedx-maven-plugin等工具生成SBOM清晰掌握所有直接和传递依赖。自动化漏洞扫描集成CI/CD将OWASP Dependency-Check、Snyk、GitHub Dependabot等工具集成到持续集成流水线中每次构建都自动检查依赖是否有已知漏洞。定期依赖升级机制不要长期使用一个古老的版本。制定计划定期如每季度评估和升级主要依赖项到稳定版本。安全编码规范在团队内部明确规范禁止将任何用户可控的输入直接传递给表达式解析器、脚本引擎、反序列化接口等危险功能。代码审查重点在Code Review时将“外部输入处理”和“第三方库风险API调用”作为重点审查项。7. 从Hutool漏洞看开源组件安全治理这次对Hutool潜在SPEL风险的深入分析不仅仅是为了解决一个具体工具的问题更是为我们敲响了开源组件安全治理的警钟。在现代软件开发中我们站在巨人的肩膀上但巨人的肩膀上也可能有裂缝。我个人的体会是对待像Hutool这样深入项目肌理的工具库我们需要从“使用者”思维转变为“共治者”思维。这意味着主动关注订阅你核心依赖项目的GitHub Release、安全邮件列表。不要等到漏洞爆发才行动。深度理解对于提供“强大”或“灵活”功能如表达式解析、动态加载、反射调用的组件花点时间了解其实现原理和安全边界。阅读其关键功能的源码并不像想象中那么难。最小化使用只引入你真正需要的模块。如果只是用Hutool做日期转换就不要引入整个hutool-all依赖。这能有效减少攻击面。贡献社区如果你发现了问题或有了改进方案积极向开源社区提交Issue或PR。你今天修复的漏洞可能会保护成千上万个项目。最后分享一个在应急响应时的小技巧当安全扫描告警或出现安全事件时第一时间去项目依赖树的“叶子节点”那些你几乎没听过的、深层次的传递依赖里找问题往往比在顶层框架中寻找更快。很多严重漏洞都隐藏在这些不起眼的工具库中。保持警惕安全开发是我们每个构建数字世界的人的责任。