资讯动态

Spring Boot Actuator字符绕过漏洞:Nginx与APISIX网关拦截配置实战

发布时间:2026/10/17 9:11:32 来源:尧图企业网站定制
1. 这个漏洞到底是怎么回事先说一个我最近处理的真实场景。某公司有个对外提供服务的Spring Boot应用开发阶段图方便把spring-boot-starter-actuator的监控端点全部打开了也没做权限控制。上线时运维图省事直接把8080端口通过Nginx反代暴露到了公网。结果没过多久安全扫描报告里就出现了一条中高风险/actuator/env接口可以被匿名访问而且通过一些特殊的字符组合还能绕过网关层已经配置的拦截规则。顶楼的同事一开始还以为是Nginx配置写错了试了curl http://域名/actuator/env发现确实返回403但换成curl http://域名/actuator%2fenv、curl http://域名/actuator;/env这种带特殊字符的路径居然又能访问到数据。这就是典型的“actuator字符绕过”问题在Nginx和APISIX这类反向代理网关上都可能出现。这篇文章就专门讲清楚两件事一是这种绕过为什么能成立二是在Nginx和APISIX上分别要怎么配置才能真正拦死。文章既有原理分析也有可以直接抄走的配置片段和验证命令适合运维、安全测试、后端开发参考。1.1 Actuator是什么为什么它暴露很危险Actuator是Spring Boot自带的监控模块提供了一堆生产环境可用的运维接口。/actuator/env可以查看环境变量和配置信息/actuator/heapdump可以下载JVM堆内存快照/actuator/mappings可以列出所有请求路径映射/actuator/beans可以查看Spring容器里所有Bean的详情。这几类接口本身是运维利器比如排查配置项没生效、看内存对象分布、确认接口路由冲突都能用上。但一旦暴露到公网就变成了信息泄露的漏洞源。/actuator/env泄露的可能包括数据库密码、Redis密码等敏感配置/actuator/heapdump里更是大概率藏着内存中的口令、Token、密钥。攻击者拿到这些东西后基本等于拿到内网的钥匙。行业内对Actuator暴露有个共识在公网环境下所有Actuator端点都默认视为敏感资源除非明确做了严格的认证授权否则必须封禁。网关层拦截是性价比最高的一层防护不用改动代码就能快速止血。但问题恰恰在于大多数网关配置只是简单匹配路径前缀这就给字符绕过留下了空间。1.2 字符绕过的原理网关和后台各看各的路径这里要讲清楚一个核心概念请求从客户端到后端服务中间要经过网关的URL解析、后端的Servlet容器解析、Spring MVC的路径匹配这三层对URL的理解并不是完全一致的。Nginx对location的匹配是基于解码并规范化后的URI比如%2f会被当成/重复的斜杠会被合并.和..会被处理。但Nginx并不是把原始请求直接透传给后端它对路径的解析方式和后端容器存在差异。后端TomcatSpring Boot默认内嵌容器解析URL时会处理路径参数。比如/actuator;/env里的分号Tomcat会把;env当作路径参数去掉实际匹配的路径是/actuator而不是/actuator/env这就可能导致Spring Security的放行规则匹配到/actuator而实际访问的又是/actuator/env的数据。更常见的问题是编码差异。Nginx的location规则匹配的是已解码的路径所以/actuator%2fenv经解码后变成/actuator/env理论上location /actuator应该能拦住。但Nginx在某些配置下比如用了proxy_pass带URI的情况或者某些版本的URI处理差异对编码字符的处理并不可靠。实测环境中最常见的现象是正常路径拦住了编码路径漏过去了。网关防绕过的关键思路不能用“规范化后的路径去做前缀匹配”因为网关规范化和后端规范化并不总是相同的。更可靠的做法是基于原始请求串$request_uri去做正则匹配在转发给后端之前直接识别并丢弃所有可疑请求。1.3 常见绕过姿势有哪些结合我在实际安全测试中见到的情况针对/actuator/env这类目标路径常见的绕过格式大概可以分这几类编码绕过/actuator%2fenv、/actuator%252fenv二次编码、/%61ctuator/env字母编码路径参数绕过/actuator;/env、/actuator/;/env、/actuator/..;/env大小写绕过/ACTUATOR/env、/Actuator/Env路径规范化绕过/actuator/./env、/foo/../actuator/env、//actuator//env混合绕过/actuator%3benv、/actuator%2e%2e;/env这些方式本质上都利用了一个差异网关用自己的一套规则解析路径判断“这不是actuator请求”而后端用另一套规则解析出“这就是actuator端口”于是请求被放行到后端数据泄露。理解了这一点下面在Nginx和APISIX上的配置才有依据而不是照抄一个deny规则就完事。2. Nginx侧防守基于原始请求拦下来Nginx是很多Java应用的第一道网关。单独看location /actuator { deny all; }这种配置很多人以为就够了实际测试下来你会发现绕过方式多得惊人。这一节讲清楚为什么不够以及正确的配置长什么样。2.1 为什么location直接封不住先看一个典型的错误配置server { listen 80; server_name example.com; location /actuator { deny all; return 403; } location / { proxy_pass http://backend; } }这个配置对curl http://example.com/actuator/env确实能返回403但问题在于第一location /actuator是前缀匹配它匹配的是Nginx解析后的URI。当请求是/actuator%2fenv时Nginx在解析URI阶段是否解码、怎么解码直接影响匹配结果。有些场景下Nginx会把%2f保留为编码形式导致location认为路径是/actuator%2fenv前缀匹配失败于是请求落到了location /被正常转发给后端。第二即使Nginx对%2f解码了location /actuator能匹配后端Tomcat在收到/actuator/env后依然会正常响应。也就是说location匹配成功了但拦截动作只对“正常写法”生效编码写法是否能被匹配完全取决于Nginx版本和配置。第三分号绕过是最典型的盲区。/actuator;/env在Nginx这里路径就是字面意义的/actuator;/envlocation /actuator前缀匹配完全不命中直接被location /转发出去。而后端Tomcat解析时分号后面的;/env被当成路径参数丢弃Spring MVC实际看到的路径是/actuator如果Actuator的端点路由配置允许以/actuator为前缀的子路径访问请求就能到达。所以结论很明确单纯用location做前缀匹配挡不住编码和分号类绕过必须要用更底层的变量。2.2 用$request_uri做正则拦截Nginx提供了$request_uri变量它的值就是客户端原始请求行中的URI部分不经过任何解码、规范化处理长什么样就是什么样。比如请求GET /actuator%2fenv HTTP/1.1$request_uri的值就是/actuator%2fenv原汁原味。用$request_uri配合正则就绕开了Nginx对路径的二次解析直接以原始字符串来识别。只要原始请求里出现可疑的actuator特征不管它后面用什么编码、加什么分号都能在转发前拦下来。这里给出一个我在Nginx上实际验证过的拦截正则# 放在 server 块内location 之前 if ($request_uri ~* (^|/)(actuator)([%;/]|$|/|%2f|%2F|%3b|%3B|%252f)) { return 403; }正则里的~*是大小写不敏感匹配这样/ACTUATOR也能被拦住。括号里的几个分支分别对应[;/]分号和路径普通分隔符$URI结束对应/actuator本身/正常的子路径分隔%2f|%2F编码的斜杠%3b|%3B编码的分号%252f二次编码的斜杠实际使用中我发现这个正则有个问题/actuator后面直接跟普通字符的情况没覆盖全。比如/actuatorabc这种虽然不是合法的端点路径但为了安全起见建议一并拦截。改进后的写法可以更宽松一些if ($request_uri ~* (^|/)actuator([^a-zA-Z]|$)) { return 403; }这个的意思是只要路径中出现actuator并且它的下一个字符不是字母就判定为可疑请求。这样的好处是覆盖面大误拦概率低因为正常业务路径里几乎不会有actuator这个单词。2.3 完整的server配置示例下面是一个我在生产环境用过的完整Nginx配置片段包含了基础拦截、备用拦截和正常转发server { listen 80; server_name example.com; # 第一层原始URI正则拦截覆盖编码绕过、分号绕过、大小写 if ($request_uri ~* (^|/)actuator([^a-zA-Z]|$)) { return 403; } # 第二层location精确拦截作为正常路径的兜底 location ~* ^/actuator { deny all; return 403; } # 如果用了Spring Boot的actuator但挂在不同context-path也要覆盖 # 比如 context-path 配了 /api就是 /api/actuator if ($request_uri ~* (^|/)api/(actuator|health|env|heapdump|mappings)([^a-zA-Z]|$)) { return 403; } location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里需要特别说明两点。第一return 403在if块里是被Nginx官方文档警告过的写法因为它属于“if中的return”执行顺序上有些反直觉但用在“请求到达location之前返回403”这个场景是完全没问题的。我在多个版本的Nginx1.18、1.20、1.22上实测过行为一致。第二location ~* ^/actuator是正则位置匹配优先级高于前缀匹配的location /因此正常写法/actuator/env无论如何都会先进这个正则location然后被拦截。两道防线配合一个管原始字符一个管规范路径漏洞的覆盖面就大了很多。2.4 验证绕过是否被拦下的方法配置改完之后必须验证不验证等于没配。我通常用一个循环脚本把各种绕过方式都测一遍base_urlhttp://127.0.0.1 # 模拟网关入口替换成你的测试域名 paths( /actuator/env /actuator/env/ /ACTUATOR/env /actuator%2fenv /actuator%252fenv /actuator;/env /actuator/;/env /actuator/..;/env /actuator/./env /foo/../actuator/env //actuator//env /%61ctuator/env /actuator%3benv ) for path in ${paths[]}; do code$(curl -s -o /dev/null -w %{http_code} ${base_url}${path}) echo $code $path done注意测试的时候要确保请求真正走了Nginx而不是绕过了网关直连后端。如果你本机直接执行上面的命令可能因为/etc/hosts或者代理设置导致请求没走Nginx结果失真。建议先在Nginx日志里确认访问来源。还有个容易被忽略的点测试分号绕过时curl默认不会对分号做特殊处理发送的请求行就是GET /actuator;/env HTTP/1.1这是正确的。但如果你通过浏览器地址栏输入某些浏览器会把分号后的内容过滤掉或者自动编码测出来的结果就不准。所以验证时尽量用curl别用浏览器。实测下来上面的配置对所有列出的路径都应该返回403而正常的业务路径比如/api/login不受影响。如果发现某个路径返回了200先确认Nginx配置是否生效nginx -t后nginx -s reload再看是不是有其他location优先级更高把请求抢走了。3. APISIX侧的配置路由与插件两条路如果说Nginx是基础设施层的反代那APISIX就是当前很火的云原生API网关。很多团队用APISIX统一管理微服务入口Spring Boot应用的请求也走它。APISIX本身功能比裸Nginx丰富但同样存在字符绕过的问题配置方式也不一样。3.1 理解APISIX的匹配机制APISIX的路由匹配默认使用uris字段做前缀匹配这个字段匹配的是Nginx变量$uri也就是经过规范化和解码后的路径。和Nginx的location一样直接用uris: [/actuator]去匹配对/actuator%2fenv、/actuator;/env这类请求根本不会命中请求会被更宽泛的路由接住然后转发到上游服务。这就是APISIX侧防绕过的难点路由层的匹配天然基于规范化路径而绕过方式恰恰是利用不同组件对路径规范化的差异。要防住就要借助APISIX提供的其他能力。APISIX的每个Route都支持vars参数这是一个基于Nginx变量的条件表达式可以在这里用request_uri做原始串的正则匹配效果和前面Nginx的if块一样。除了vars还可以用filters在APISIX 3.x版本中叫filter_func做更复杂的判断。3.2 用vars规则拦截request_uriAPISIX创建Route时通过vars字段可以定义一个条件列表只有条件全部满足的请求才会命中这条路由。我们可以创建一条专门拦截Actuator的高优先级路由让它把可疑请求直接返回403。下面是通过Admin API创建拦截路由的示例curl http://127.0.0.1:9180/apisix/admin/routes/999999 \ -H X-API-KEY: your-api-key \ -X PUT \ -d { uris: [/*], vars: [ [request_uri, ~~, (?i)(^|/)actuator([^a-zA-Z]|$)] ], plugins: { serverless-pre-function: { phase: rewrite, functions: [ return function(conf, ctx) return ngx.exit(403) end ] } }, priority: 100000 }这条配置有几个关键点第一uris用了/*表示匹配所有请求。由于这条路由的priority设置为100000比普通业务路由的默认优先级高得多因此只要vars条件满足它就会先命中。第二vars里的request_uri对应Nginx变量~~表示正则匹配大小写不敏感用(?i)正则表达式和前面Nginx部分完全一致覆盖编码、分号、大小写等绕过场景。第三serverless-pre-function插件执行ngx.exit(403)在rewrite阶段直接返回403请求根本不会到达upstream。这里要提醒一个APISIX版本差异在APISIX 2.x版本中这个插件叫serverless-pre-function在3.x版本中推荐使用script字段或者filters特性写法会有些区别。上面的写法在2.x和3.x的兼容模式下都能工作稳妥起见我还是沿用这个方案。如果不想建一条全量匹配的高优先级路由还有一个更轻量的思路在正常的业务路由上直接加vars条件把拦截逻辑合并进去。比如业务路由的vars增加一条[ [request_uri, !~~, (?i)(^|/)actuator([^a-zA-Z]|$)] ]意思是只有当请求不匹配Actuator特征时才走这条业务路由。这样就不需要额外建一条高优先级路由但问题在于如果有多个业务路由每个都要加上这个条件容易漏。所以我还是推荐独立建一条拦截路由的做法。3.3 用自定义插件脚本兜底拦截vars上面已经能解决大部分问题但有些情况下你还是会觉得不受控比如业务上确实需要以/actuator为前缀的合法的非Spring端点路径需要按请求头、按来源IP做更细粒度的放行需要在拦截前记录日志这种情况可以把手写Lua脚本放到serverless-pre-function里实现更灵活的拦截逻辑。下面是一个示例它把可疑请求写入日志后返回403同时对内网IP来源网段放行local ip_utils require(resty.ip_utils) return function(conf, ctx) local request_uri ngx.var.request_uri or local remote_addr ngx.var.remote_addr or -- 放行内网来源 if ip_utils.ip_in_cidr(remote_addr, 10.0.0.0/8) then return end -- 匹配actuator请求特征 if ngx.re.match(request_uri, (^|/)actuator([^a-zA-Z]|$), i) then ngx.log(ngx.ERR, [actuator-block] blocked request from , remote_addr, uri, request_uri) ngx.exit(403) end end注意用serverless-pre-function之前要确认APISIX实例上已经装了这个插件默认自带不需要额外装载。另外ngx.re.match是OpenResty自带的正则库性能足够不用自己去调string.find。脚本里我特意加了来源网段放行。原因是有些团队内部运维监控需要访问Actuator端点做健康检查内部IP不应该被网关误杀。但内网放行要控制好网段千万别用0.0.0.0/0这种写法那就等于没拦。3.4 多环境一致性的配置建议如果你在Dev、Test、Prod多套环境都部署了APISIX手工一条条创建拦截路由很容易出错尤其是正则这样容易写错的东西。建议用声明式配置管理把拦截路由的JSON定义放到代码仓库里通过CI/CD下发。比如用APISIX的runtime配置同步机制每次变更走Git提交 自动化发布确保多环境规则一致。另外APISIX的Admin API默认监听在9180端口一定要限制访问来源别暴露到公网。曾见过有人把Admin API暴露出来攻击者直接通过Admin API改路由规则把拦截路由删掉或者把请求转发到恶意地址反而扩大了风险面。4. 常见问题与排查技巧配置写完了不代表就高枕无忧。实际维护过程中还会有很多细节问题这一节整理几个我踩过的坑和对应的排查思路。4.1 为什么我的正则没生效这是最常遇到的问题。配置明明加了正则测试/actuator/env也返回了403但换个姿势就200了。查下来主要有几种原因第一Nginx的if块作用域不对。if放在location块内和放在server块内的语义不一样。虽然两个位置都能执行return但location内的if要等URI匹配到该location才会执行如果你的正则location本身没有匹配到这个绕过请求if根本不会跑。所以Nginx侧的拦截if要放在server块顶层确保它在所有location匹配之前执行。第二APISIX的vars字段值写错。vars的request_uri要写成字符串拼接的列表形式如果少写了引号或者变量名写错APISIX可能不会报错但实际效果是条件永远不满足或永远满足。建议用Admin API返回的Route配置检查一下实际下发的vars内容。第三优先级问题。APISIX的路由匹配顺序是先按priority从高到低同优先级按创建时间等顺序。如果你的拦截路由priority不够高业务路由优先匹配请求就会被转发走。我习惯把拦截路由priority设置成100000普通业务路由默认是0差异足够大。4.2 分号被编码了怎么办分号被编码成%3b后Nginx如果对URI解码$request_uri里的原始串依然是%3b但正则里如果用request_uri匹配还是能看到%3b这个特征。所以前面配置的正则覆盖了%3b和%3B。但有一个更隐蔽的情况如果客户端把分号做了二次编码变成%253b$request_uri里就是%253b正则里如果不加这个特征就漏了。所以我在正则里额外加了%252f同理也应该加%253b。建议把正则统一成if ($request_uri ~* (^|/)actuator([^a-zA-Z]|$|%2f|%2F|%3b|%3B|%252f|%253b)) { return 403; }不要怕正则太宽安全和可读性比起来安全优先。万一误伤了nginx日志里能查到原始URI排查也方便。4.3 网关拦截后正常业务路径会不会误伤这个担心合理。actuator这个词在正常业务路径里出现概率确实不高但也不是零比如某个内部系统的路由设计里就有一个/actuator/status/list这样的业务接口和Spring的Actuator共用了一个前缀。如果确实有合法业务路径带actuator字样上面那种宽泛正则就会误伤。解决办法是在正则里排除已知合法路径。比如Nginx可以多写一个判断# 先用正则拦截但排除指定业务前缀 if ($request_uri ~* (^|/)actuator([^a-zA-Z]|$)) { set $block_actuator 1; } if ($request_uri ~ (^|/)actuator/status/list) { set $block_actuator 0; } if ($block_actuator 1) { return 403; }注意Nginx的if不能直接嵌套所以要借助中间变量来实现“先拦截后排除”的效果。这种写法在APISIX里可以用两个vars条件组合实现逻辑更清晰vars: [ [request_uri, ~~, (?i)(^|/)actuator([^a-zA-Z]|$)], [request_uri, !~~, (?i)(^|/)actuator/status/list] ]APISIX的多个vars条件是AND关系同时满足才匹配这样就实现了排除。4.4 网关拦截后还有什么必须做的加固网关配置只是止血不是根治。我在排查这个问题的过程中发现单纯靠Nginx或APISIX拦截总会有新的绕过姿势出来因为网关和后端对URL的解析标准天然存在差异这是一个维度上很难完全对齐的问题。所以更稳妥的做法是多层防线一起上在Spring Boot应用侧把Actuator端口和管理端口分开配置使用一个独立的管理端口不经过公网反代。比如management.server.port9090只在内网监听。如果必须通过网关访问给Actuator端点加认证。引入spring-boot-starter-security对/actuator/**配置角色校验或者结合网关层的认证插件做双重验证。禁用不安全的端点通过management.endpoints.web.exposure.include只暴露必要的端点比如health,info把env、heapdump、mappings、beans等敏感端点全部排除。升级Spring Boot版本新版本对端点路径的规范化处理和权限控制有改进能在一定程度上缓解绕过问题。这里想特别强调一点网关只能防外部请求的绕过如果攻击者已经能直连内网、绕开网关那再多的网关规则都是形同虚设。所以网关防护是必要条件但不是充分条件。最理想的状态是Actuator的敏感端点压根不存在公网可达的路径网关上的拦截规则只是最后一道保险。5. 写在最后的一点实操体会这篇文章里讲到的配置是我在一周内从发现问题到完成加固的真实经历。整个过程最耗时间的不是写正则而是验证各种绕过姿势的时候发现“明明配置了却没拦住”的情况反复出现。后来我养成了一个习惯每次改完网关配置第一件事就是跑一遍那十几条curl测试命令确认所有绕过路径都返回403之后才敢把配置同步到生产。如果你正被这个问题困扰建议不要只照着网上某一篇文章抄一个正则就完事而是先在测试环境里把你自己业务实际可能面对的绕过路径列出来逐条验证确认网关行为符合预期。同时把Spring Boot端的加固也做了两边一起弄才能把这个漏洞真正堵上。从长期看网关层拦截这类“字符差异型”漏洞本质上是一种持续对抗攻击者会发现新的编码规则、新的容器解析差异我们也只能不断更新规则。比较好的缓解方式是尽量减少暴露面。Actuator这类管理端点如果业务上不是非暴露不可就让它彻底隐身在内网。能少一个公网入口就少一分被绕过的风险。这个道理放在任何管理接口上都适用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑