Spring Boot应用安全加固实战:Actuator未授权与ScriptEngine注入漏洞修复

Spring Boot应用安全加固实战:Actuator未授权与ScriptEngine注入漏洞修复
1. 项目概述一次典型的Spring Boot应用安全加固实战最近在帮一个朋友的公司做内部系统的安全评估他们用的是基于nbcio-boot这个开源框架搭建的后台管理系统。这个框架本身功能挺全整合了Spring Boot、MyBatis-Plus、Flowable工作流这些主流技术栈开发效率很高。但在做代码审计时我发现了两个比较典型且危险的安全漏洞一个是Actuator端点的未授权访问另一个是ScriptEngine的代码注入风险。这两个问题在Spring Boot生态里其实挺常见的但很多开发团队在追求功能快速上线时很容易忽略掉。今天我就把这次审计和修复的过程详细拆解一下尤其是针对nbcio-boot这个具体框架的上下文希望能给正在使用类似技术栈或者关心应用安全的同学一些直接的参考。你不需要是安全专家只要对Spring Boot开发有基本了解就能看懂并应用到自己的项目里。简单来说这次要处理的核心就两件事第一防止任何人不用登录就能访问到/actuator下面的健康检查、内存信息、线程堆栈等敏感接口这些信息泄露可能导致攻击者摸清系统底细第二堵住通过ScriptEngine执行任意代码的口子这相当于给攻击者开了一个在服务器上直接运行命令的后门危害极大。修复的思路并不复杂关键是理解漏洞产生的原理以及如何在nbcio-boot现有的项目结构中找到最合适、侵入性最小的修补点。2. 漏洞原理深度剖析为什么会出现这些问题在动手修复之前我们必须先把这两个漏洞的来龙去脉搞清楚。知其然更要知其所以然这样以后遇到类似问题你才能举一反三。2.1 Actuator未授权访问便利性与安全性的失衡Spring Boot Actuator是个非常棒的生产就绪特性模块它提供了一系列HTTP或JMX端点Endpoints让你能监控和管理正在运行的应用。比如查看健康状态/actuator/health、查看环境变量/actuator/env、查看映射的URL/actuator/mappings甚至能优雅关闭应用/actuator/shutdown默认关闭。在nbcio-boot的默认配置或者很多快速启动的教程里为了图方便开发者可能会在application.yml里简单地写上management: endpoints: web: exposure: include: * # 暴露所有端点或者直接依赖了spring-boot-starter-actuator但没有做任何安全配置。问题就出在这里这些暴露的端点默认是没有接入应用自身的安全框架如Spring Security的。它们运行在一个独立的管理上下文里。这意味着即使你的业务接口/api/user/login有严格的JWT或Session校验攻击者依然可以直接访问http://你的服务器:端口/actuator/env拿到数据库连接密码、第三方API密钥等所有环境变量访问/actuator/heapdump可以下载整个JVM堆内存快照用MAT等工具分析能还原出敏感数据/actuator/mappings会泄露所有控制器路径相当于给了攻击者一份完整的API地图。漏洞的本质Actuator端点的暴露范围management.endpoints.web.exposure.include和对其的访问控制认证与授权是两件独立的事情。只配置了暴露没配置保护就等于把后门敞开了。2.2 ScriptEngine注入漏洞动态执行的“双刃剑”nbcio-boot框架中为了支持动态流程表达式或者某些灵活的脚本配置功能可能会引入或使用Java的ScriptEngineManager。例如允许在流程定义中写一段Groovy或JavaScript逻辑来判断审批条件。一个存在风险的代码片段可能长这样PostMapping(/eval) public String evaluateScript(RequestParam String script) { ScriptEngineManager manager new ScriptEngineManager(); ScriptEngine engine manager.getEngineByName(javascript); try { // 危险直接执行用户传入的脚本 return engine.eval(script).toString(); } catch (ScriptException e) { return Error: e.getMessage(); } }或者更隐蔽一些从数据库、配置文件、前端传入的某个参数最终被拼接成脚本字符串交给了ScriptEngine执行。漏洞的本质ScriptEngine.eval()或engine.createScript().eval()这类方法如果其输入源参数、请求体、数据库字段完全或部分由用户控制且没有经过严格的净化和沙箱隔离就等同于允许攻击者在服务器上执行任意代码。JavaScript引擎可以调用Java类Groovy引擎功能更强大几乎能完成任何操作比如“java.lang.Runtime.getRuntime().exec(‘rm -rf /’)“。在nbcio-boot的上下文中需要重点审计与工作流Flowable、规则引擎、动态报表生成相关的服务类查找所有使用javax.script.ScriptEngine或org.springframework.scripting包的地方。3. 修复方案设计与实施精准施策最小化改动理解了漏洞原理修复目标就明确了对Actuator我们要么限制暴露的端点要么给暴露的端点加上安全锁对ScriptEngine我们要彻底杜绝不可信数据流入执行引擎。下面我结合nbcio-boot的项目结构给出具体的修复步骤。3.1 Actuator未授权访问漏洞修复对于Actuator的修复通常有四种策略按推荐度排序策略一严格限制暴露的端点最推荐。只暴露真正需要的、不敏感的端点。策略二将Actuator端点纳入统一安全框架。让Spring Security来管理它的访问权限。策略三通过管理端口Management Port隔离。让Actuator运行在另一个端口并通过网络策略如防火墙控制访问。策略四完全禁用Actuator的Web端点。如果完全不需要HTTP访问这是最安全的。对于nbcio-boot这类内部管理系统策略一结合策略二是最佳实践。以下是具体操作步骤1审查并收紧暴露配置打开nbcio-boot的配置文件通常是application.yml或application-prod.yml找到management.endpoints.web.exposure部分。将其修改为仅包含必要的端点。health和info通常是安全的可以暴露给监控系统。management: endpoints: web: exposure: include: health,info # 只暴露健康和基础信息端点 exclude: * # 明确排除所有其他端点优先级更高 endpoint: health: show-details: when_authorized # 健康详情只在授权后显示 shutdown: enabled: false # 生产环境务必禁用shutdown端点这个配置确保了只有/actuator/health和/actuator/info可以通过网络访问并且health的详细信息如各个组件的状态默认不显示。步骤2集成Spring Security进行访问控制nbcio-boot通常已经集成了Spring Security和JWT。我们需要让Security的过滤器链也覆盖到Actuator的路径。在你的Security配置类可能叫WebSecurityConfig或SecurityConfig中修改configure(HttpSecurity http)方法Override protected void configure(HttpSecurity http) throws Exception { http .cors().and().csrf().disable() // 根据你的配置保持 .authorizeRequests() // 公开接口如登录、注册、Swagger等 .antMatchers(/auth/login, /doc.html, /webjars/**, /swagger-resources/**).permitAll() // 对Actuator端点进行权限控制只允许特定角色如ADMIN访问 .antMatchers(/actuator/**).hasRole(ADMIN) // 其他所有请求都需要认证 .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }关键点是这一行.antMatchers(/actuator/**).hasRole(ADMIN)。它要求访问任何/actuator/下的路径用户必须拥有ROLE_ADMIN权限。这样只有管理员才能访问这些监控接口。步骤3验证修复效果重启应用。在未登录的情况下访问http://localhost:8080/actuator/health。应该能访问因为health我们允许公开且配置了show-details: when_authorized此时只返回{status:UP}。尝试访问http://localhost:8080/actuator/env或/actuator/mappings。预期结果应该是被重定向到登录页或者返回403/401错误而不是直接返回敏感信息。用管理员账号登录后再次访问/actuator/env应该可以正常看到信息。注意如果你的监控系统如Prometheus需要拉取/actuator/prometheus端点你需要为其配置专门的认证方式。一种常见做法是使用HTTP Basic Auth并为监控系统创建一个专用账号只有必要端点权限在Security配置中为该路径单独设置httpBasic()认证。切勿为了方便而直接permitAll()。3.2 ScriptEngine注入漏洞修复修复ScriptEngine漏洞的核心原则是永远不要执行来自不可信源的脚本。如果业务上确实需要动态执行脚本必须建立严格的沙箱环境。步骤1全局搜索定位风险点在nbcio-boot项目根目录下使用IDE的全局搜索或命令行grep功能搜索以下关键词ScriptEngineScriptEngineManagereval(javax.scriptGroovyScriptEngine(如果用了Groovy)NashornScriptEngine(如果用了JavaScript)重点检查Controller、Service中处理前端参数、流程表达式解析、规则计算的相关代码。步骤2评估并实施修复方案根据搜索到的代码上下文选择以下一种或多种方案方案A彻底移除或禁用动态脚本功能如果非核心如果这个功能使用频率极低或者有更安全的替代方案如使用Spring EL表达式最安全的方式是直接注释或删除相关代码。与产品经理确认该功能的必要性。方案B实施输入白名单校验如果脚本内容来自用户输入但格式相对固定例如只能是一个简单的数学表达式或比较逻辑可以建立严格的白名单。public boolean isSafeScript(String userInput) { // 定义只允许出现的字符或模式例如数字、加减乘除、括号、有限的变量名 String safePattern ^[a-zA-Z0-9\\s\\-*/()!|]$; if (!userInput.matches(safePattern)) { throw new SecurityException(脚本包含非法字符); } // 进一步可以检查是否包含危险关键词 String[] dangerousKeywords {Runtime, ProcessBuilder, exec, ClassLoader, System.exit}; for (String keyword : dangerousKeywords) { if (userInput.contains(keyword)) { throw new SecurityException(脚本包含危险操作); } } return true; }在调用engine.eval()之前先调用isSafeScript(userInput)进行校验。注意这种方法防御能力有限绕过方法很多仅适用于非常简单的场景。方案C使用沙箱环境最彻底但最复杂对于必须执行不可信脚本的场景必须构建一个沙箱。Java本身没有完美的脚本沙箱但可以通过以下组合拳来增强安全使用Java SecurityManager这是一个几乎被废弃的特性配置复杂且影响性能不推荐作为首选。使用第三方安全沙箱库例如对于Groovy可以使用groovy-sandbox。它可以拦截脚本中对危险方法的调用。在独立进程中运行脚本将脚本执行任务提交到一个独立的、权限被严格限制的“Worker”进程或容器中通过进程间通信获取结果。这是最安全的方案但架构复杂。使用受限的ClassLoader创建一个自定义的ClassLoader只加载有限的、安全的类库如基本的数学、字符串类禁止加载java.lang.Runtime、java.lang.ProcessBuilder等危险类。// 一个简化的示例展示思路 public class SecureScriptEngine { private ScriptEngine engine; private ClassLoader secureLoader; public SecureScriptEngine() { // 1. 创建受限的ClassLoader secureLoader new ClassLoader(getClass().getClassLoader()) { Override public Class? loadClass(String name) throws ClassNotFoundException { if (name.startsWith(java.lang.Process) || name.startsWith(java.lang.Runtime)) { throw new ClassNotFoundException(禁止加载危险类: name); } // 可以加载其他安全类如java.lang.Math, java.util.Date等 return super.loadClass(name); } }; // 2. 用这个ClassLoader创建ScriptEngine ScriptEngineManager manager new ScriptEngineManager(secureLoader); engine manager.getEngineByName(javascript); // 3. 移除危险的内置对象如果引擎支持 if (engine instanceof Invocable) { // 尝试从引擎的绑定中移除危险对象但并非所有引擎都支持 engine.put(java, null); } } public Object evalSecure(String script) throws ScriptException { // 设置当前线程的上下文类加载器为我们安全的那个 Thread.currentThread().setContextClassLoader(secureLoader); try { return engine.eval(script); } finally { // 恢复原来的ClassLoader Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } } }请注意上面的示例只是一个思路演示要构建一个真正安全的沙箱极其困难需要深入理解Java安全模型和脚本引擎的内部机制。对于生产环境强烈建议采用方案A移除或方案D使用安全的表达式引擎。方案D替换为安全的表达式引擎这是最推荐的修复方案。如果业务逻辑是进行条件判断、数值计算、属性访问等完全可以用更安全、功能明确的表达式引擎替代万能的ScriptEngine。Spring Expression Language (SpEL)Spring自带与Spring生态集成好功能强大且相对安全可通过StandardEvaluationContext限制可访问的属性和方法。Apache Commons JEXL轻量级表达式语言。MVEL或OGNL功能强但需注意其历史安全漏洞使用时要保持版本最新并严格限制上下文。例如将原来的ScriptEngine调用改为SpEL// 原危险代码 // ScriptEngine engine manager.getEngineByName(javascript); // Double result (Double) engine.eval(price * quantity * (1 - discount)); // 使用SpEL ExpressionParser parser new SpelExpressionParser(); StandardEvaluationContext context new StandardEvaluationContext(); context.setVariable(price, 100.0); context.setVariable(quantity, 2); context.setVariable(discount, 0.1); Expression exp parser.parseExpression(#price * #quantity * (1 - #discount)); Double result exp.getValue(context, Double.class);SpEL可以通过StandardEvaluationContext的setRootObject和setVariable严格控制数据源无法直接执行任意系统命令安全性高得多。步骤3代码审查与测试修复完成后必须进行严格的代码审查和测试。单元测试为修改后的代码编写测试用例包括正常用例和恶意输入用例尝试注入Runtime.getRuntime().exec(‘calc’)等。集成测试模拟真实请求测试相关接口是否还能被注入。使用SAST工具扫描利用SonarQube、Fortify等静态应用安全测试工具再次扫描项目确认相关漏洞告警已消除。4. 加固延伸与最佳实践构建纵深防御修复了这两个具体漏洞并不意味着系统就高枕无忧了。安全是一个持续的过程。结合这次审计我建议在nbcio-boot或任何Spring Boot项目中建立以下纵深防御习惯4.1 安全配置清单Spring Boot专项永远使用最新稳定版本的Spring Boot及依赖安全漏洞会不断被修复保持更新是第一道防线。定期检查pom.xml或build.gradle中的依赖版本。审慎管理application.properties/yml禁用敏感端点和功能除了Actuator还有spring.jackson.default-property-inclusion、spring.mvc.log-resolved-exception等可能泄露信息的配置。强化数据库连接不要在配置文件中明文写密码使用JNDI或环境变量或者集成Vault等密钥管理工具。关闭开发时便利功能生产环境务必设置spring.devtools.restart.enabledfalse和spring.jackson.serialization.FAIL_ON_EMPTY_BEANStrue视情况而定。正确配置Spring Security使用BCryptPasswordEncoder等强哈希算法存储密码。启用CSRF保护对于有状态服务如使用Session。设置安全的HTTP响应头如Content-Security-Policy、X-Content-Type-Options、X-Frame-Options。Spring Security默认会配置一些但可以更严格。对于REST API明确禁用不必要的认证方式如表单登录使用无状态的JWT或OAuth2。输入验证与输出编码在Controller层使用Valid注解配合JSR-303 Bean Validation进行输入校验。对所有从用户获取并最终显示在HTML、XML、JSON中的数据进行适当的输出编码防止XSS。模板引擎如Thymeleaf通常会自动编码但直接使用response.getWriter().print()时要格外小心。4.2 依赖组件安全nbcio-boot集成了Flowable、Redis、Nacos等组件这些组件本身也可能存在未授权访问或配置不当漏洞。Flowable检查REST API如果启用是否受到保护。确保流程定义文件上传等接口有权限控制。Redis务必设置密码requirepass并禁止绑定在0.0.0.0使用127.0.0.1或内网IP。nbcio-boot的Redis配置应在生产环境中指向受保护的实例。Nacos如果作为配置中心确保其控制台有强密码并且namespace、dataId等配置信息不易被猜测防止未授权读取配置。关注Nacos相关的安全公告。4.3 建立安全开发流程将安全扫描纳入CI/CD在GitLab CI、Jenkins等流水线中集成依赖漏洞扫描如OWASP Dependency-Check、Trivy和SAST工具如SonarQube每次提交都自动检查。定期进行人工代码审计每季度或每次大版本发布前安排专门的安全代码审查重点关注身份认证、权限校验、文件上传、数据库操作、命令执行、反序列化等高风险点。进行渗透测试可以邀请外部白帽子或使用Burp Suite、AWVS等工具进行定期的黑盒/灰盒测试。5. 问题排查与修复验证实录在实际修复和后续验证过程中你可能会遇到一些典型问题。这里记录几个我踩过的坑和解决方法问题1配置了Spring Security但/actuator/health端点也被拦截导致监控系统无法访问。排查检查Security配置的antMatchers顺序。Spring Security的匹配规则是从上到下的第一个匹配的规则生效。.authorizeRequests() .antMatchers(/actuator/health).permitAll() // 这一行必须放在 /actuator/** 规则之前 .antMatchers(/actuator/**).hasRole(ADMIN) ...如果顺序反了/actuator/health会先匹配到/actuator/**规则从而要求ADMIN权限。问题2使用了SpEL替换ScriptEngine但某些复杂的业务逻辑表达式不知道如何转换。解决不要试图用SpEL 1:1还原复杂的JavaScript脚本。SpEL的核心是表达式求值不是脚本执行。你需要重构业务逻辑将复杂的脚本逻辑拆解能用Java代码实现的部分就写在Service层。SpEL只负责简单的属性访问、条件判断和计算。扩展SpEL函数如果确实需要一些特殊函数可以通过StandardEvaluationContext的registerFunction方法注册自定义的静态方法让SpEL调用。这样函数逻辑是受控的Java代码安全得多。public class SpELSecurityUtils { public static String safeFormat(String template, Object... args) { // 实现一个安全的格式化函数 return String.format(template, args); } } // 在配置中注册 StandardEvaluationContext context new StandardEvaluationContext(); context.registerFunction(format, SpELSecurityUtils.class.getDeclaredMethod(safeFormat, String.class, Object[].class));问题3修复后如何确认ScriptEngine漏洞已彻底堵死验证方法黑盒测试使用Burp Suite或Postman向所有可能传入脚本参数的接口发送Payload如scriptjava.lang.Runtime.getRuntime().exec(calc)Windows或scriptjava.lang.Runtime.getRuntime().exec(/bin/sh -c echo test)Linux。观察响应是错误信息如“脚本包含非法字符”还是系统真的执行了命令弹出计算器或返回命令结果。重要请在隔离的测试环境进行白盒测试在修复后的代码关键点如evalSecure方法入口打上断点用调试模式启动传入恶意Payload观察代码是否走到了安全校验或沙箱逻辑脚本是否被拒绝或限制执行。依赖检查确保项目中不再包含不安全的、老版本的脚本引擎库。例如NashornJDK内置的JS引擎在较新JDK中已被移除如果项目依赖了第三方JS引擎库需确认其安全性。问题4Actuator端点权限控制生效了但Swagger文档页面如/doc.html也暴露了所有接口信息怎么办解决Swagger或Knife4j的UI页面和API文档JSON本身也是需要保护的资源。在Security配置中确保只允许在开发或测试环境开启或同样加以权限控制。# application-prod.yml springfox: documentation: enabled: false # 生产环境禁用Swagger自动生成 knife4j: enable: false # 如果用了Knife4j也禁用或者在Security配置中将Swagger的资源路径也限制为管理员访问.antMatchers(/doc.html, /swagger-ui.html, /swagger-resources/**, /webjars/**, /v2/api-docs/**).hasRole(ADMIN)安全加固不是一劳永逸的事情尤其是像nbcio-boot这样功能丰富的集成框架在享受其开发便利的同时必须对每个引入的组件和默认配置保持警惕。这次针对Actuator和ScriptEngine的修复核心思路就是“最小暴露原则”和“不信任任何用户输入”。把这两条原则贯彻到日常开发的每一个设计决策和代码编写中才能从根本上提升系统的安全水位。