1. SpringBoot未授权访问漏洞到底是怎么来的
聊这个话题之前,先把话撂在前头:SpringBoot 本身没那么多"漏洞",很多被贴上"未授权访问漏洞"标签的问题,本质上是开发者把本该关在屋里的东西,直接敞开摆到了公网上。Actuator、Swagger、Druid 监控页、H2 控制台、Nacos 控制台,这些东西出厂默认是为了让开发和运维"图个方便",谁想到上线时忘了收,就成了别人手里的钥匙串。我做了几年企业侧的应用安全评审,见过最离谱的一个生产环境,/actuator/env直接把数据库连接串连账号密码一起吐出来,还是明文的。
所以这篇文章不打算写成"漏洞利用大全",市面上那种东西已经够多了。我更想从一个写代码也做安全的角度,把 SpringBoot 生态里几类高频的未授权访问问题拆开:它们为什么会存在、原理是什么、在什么条件下会被真正利用、以及工程上到底该怎么收口。适合谁看?如果你是后端开发、运维、或者刚入行的安全测试,想系统搞明白"为什么我的项目会被人扫出来这些接口",那基本能对上号。全文的立场是防御视角,涉及检测的部分都建立在你有明确授权的前提下。
1.1 约定优于配置带来的副产物
SpringBoot 最核心的设计哲学是"约定优于配置",起步依赖一加,自动配置一开,一个能跑起来的 Web 服务几分钟就出来了。这套机制在提升效率的同时,也悄悄帮你"默认打开"了一堆东西。比如你引入spring-boot-starter-actuator,不写任何配置,它就会在/actuator路径下暴露若干端点;你引入springfox或springdoc,Swagger UI 就自动挂上了;你引入 Druid 的 starter 并且开了监控,/druid/index.html就活了。
这里面有个很关键的认知差:开发阶段这些端点确实方便,本地调试、看健康状态、翻接口文档,都靠它。问题出在打包部署环节——很多人做配置隔离时,只换了数据库地址、改了日志级别,唯独忘了application-prod.yml里压根没有对 Actuator 的暴露范围做限制。于是本地和生产用的是同一套默认行为,测试环境的"方便"被原封不动带到了线上。
再叠加一层现实因素:现在大量项目是"能跑就行"的交付模式,甲方催进度,乙方赶上线,安全评审往往排在最后甚至直接省略。SpringBoot 的默认配置又恰好是"先开后收"的思路——它不会主动提醒你"这个端点很危险",最多在启动日志里打一行映射记录,而这一行淹没在几十行启动日志里,根本没人看。
1.2 风险不是单点的,而是一条链
很多人评估这类问题时会犯一个错:把每个暴露的端点当成孤立事件来看,觉得"不就是泄露点信息嘛,能有多大事"。实际在真实攻防里,未授权访问往往是攻击链的第一环,它负责信息收集,后面的提权和横向移动都建立在这上面。我把常见的危害层级整理成一张表,你对号入座看看自己项目处在哪一档:
| 暴露内容 | 典型端点 | 直接危害 | 可延伸的攻击链 |
|---|---|---|---|
| 健康状态、版本信息 | /actuator/health、/actuator/info | 泄露框架版本,便于匹配已知 CVE | 版本指纹识别 |
| 请求映射、Bean 列表 | /actuator/mappings、/actuator/beans | 暴露全部接口路径 | 接口爆破、越权测试 |
| 配置属性、环境变量 | /actuator/env、/actuator/configprops | 数据库口令、AK/SK、密钥泄露 | 直连数据库、云资源接管 |
| 堆内存转储 | /actuator/heapdump | 内存中的明文凭据、会话令牌 | 凭据复用、会话劫持 |
| 日志级别动态修改 | /actuator/loggers | 打开 DEBUG 记录敏感数据 | 配合日志读取获取信息 |
| JMX 桥接 | /actuator/jolokia | 可达 MBean 操作 | 部分场景下直达 RCE |
看到没,从最轻的版本指纹到最重的 RCE,其实是一条渐进的路。这就是为什么我说不要孤立看待:一个/actuator/env泄露的 RabbitMQ 口令,可能让你从"只能读配置"直接跳到"能往消息队列投递任意任务"。
注意:很多团队做安全加固时只堵了 RCE 这类"重"的口子,觉得信息泄露无所谓。但在内网横向场景里,一条泄露的凭据比一个需要复杂条件的 RCE 好用得多,也更难被审计发现。
2. Actuator:重灾区里的重灾区
要说 SpringBoot 未授权访问,Actuator 绝对排第一。这套端点机制本来是给运维做应用健康监控用的,设计上确实考虑过安全——SpringBoot 2.x 之后默认只暴露health和info两个端点。但现实是,大量项目为了"看得全",直接把它配成了include: "*",等于把所有端点一次性摆上台面。
2.1 端点清单与敏感度分级
先把家底摸清楚。SpringBoot 2.x 常见的 Web 端点大致有这些,我按敏感度做了分级:
| 端点 | 作用 | 敏感度 | 说明 |
|---|---|---|---|
/actuator | 端点索引 | 低 | 告诉你有哪些端点开着,信息收集起点 |
/actuator/health | 健康检查 | 低 | 配置show-details: always后会泄露组件详情 |
/actuator/info | 应用信息 | 低 | 常被塞进 Git 提交号、构建时间 |
/actuator/mappings | 请求映射 | 中 | 全量接口路径,越权测试的地图 |
/actuator/beans | Bean 列表 | 中 | 泄露内部类结构 |
/actuator/threaddump | 线程堆栈 | 中 | 可能含内部调用链和路径 |
/actuator/loggers | 日志级别 | 中 | 可 POST 动态改级别 |
/actuator/configprops | 配置属性 | 高 | 结构化配置,含敏感值 |
/actuator/env | 环境变量 | 高 | 系统属性、启动参数、配置项 |
/actuator/heapdump | 堆转储 | 高 | 完整内存快照,可离线分析 |
/actuator/jolokia | JMX 桥接 | 高 | 依赖引入后可达 MBean |
这里有个坑我要单独提。很多人以为只要把include收窄就万事大吉,但忽略了health端点本身的show-details配置。默认是never,可有些项目为了让运维看数据库连接池状态,改成了always,结果/actuator/health会把各个组件的详细状态、甚至数据库地址都暴露出来。这个端点因为"看起来人畜无害",反而最容易被放过。
2.2 heapdump:那个最容易被忽视的"明文密码库"
如果只让我挑一个端点来强调,我会选heapdump。原因很简单:/env泄露的是配置,而heapdump泄露的是运行时内存——内存里有什么?数据库连接池里缓存的明文口令、JWT 的签名密钥、用户会话对象、缓存里还没过期的 Token。这些东西在配置文件里可能还是加密的,到了内存里往往就已经是解密后的明文了。
heapdump端点的实现原理是触发一次 JVM 的堆转储(Heap Dump),把整个堆内存序列化成 HPROF 二进制格式从 HTTP 响应流返回。这个文件本身可以用 Eclipse MAT、VisualVM 这类工具打开,也可以用命令行工具做字符串提取。很多安全扫描器判定这个端点存在时,其实只做了一次 HEAD 或 GET 请求看响应头是不是application/octet-stream,并不下载完整文件——但这不代表它不危险,恰恰相反,它是最容易被遗漏在扫描报告里、却危害极大的那种。
我处理过一个实际案例:某个项目的/actuator/heapdump是开放的,从里面提取出了一个 Redis 的密码,而那个 Redis 恰好是公网可访问的,且密码就是生产环境通用密码。一条链下来,从"一个监控端点"直接打穿到缓存层。自那以后,我评审任何 SpringBoot 项目,heapdump是必查项。
提示:
heapdump在 SpringBoot 1.x 里的路径是/dump,2.x 挪到了/actuator/heapdump。做资产梳理时两个路径都要覆盖,别只扫 2.x 的。
2.3 /env 加 /refresh 与 SpEL,老版本的组合拳
在 SpringBoot 1.x 时代,有一个经典组合:POST /env修改配置属性,然后POST /refresh触发配置重载。这俩单独看都不算致命,但连起来就麻烦了。攻击者可以往eureka.client.serviceUrl.defaultZone这类配置里塞一个自己控制的地址,触发 Spring Cloud 的配置拉取,进而结合 XStream 反序列化漏洞执行代码。这套打法在当年影响面很广,核心问题出在边界的模糊:一个"运维用的配置刷新接口",被设计成了可以写任意配置项。
2.x 之后,/env变成了只读(GET 能读,写被限制),/refresh也默认关闭,但历史项目里仍然大量存在老版本。除此之外,Spring Cloud Gateway 在特定版本下存在 SpEL 表达式注入(对应 CVE-2022-22947),攻击者可以通过 POST 向/actuator/gateway/routes添加一个带恶意 SpEL 的路由,再触发刷新,从而执行命令。虽然是老漏洞了,但因为网关组件部署广泛,至今仍有存量。
这类问题的共性原理是:把"配置写入"这个能力暴露成了 HTTP 接口。配置在 Spring 体系里不只是静态数据,它可以影响 Bean 的创建、触发远程拉取、甚至参与表达式求值。所以只要写配置的通道是通且无认证的,攻击面就远不止"改个参数"那么简单。
2.4 检测与验证的正确姿势
前提说清楚:下面这些方法只在你拥有明确授权的资产上使用。授权范围外的扫描和验证都可能触碰法律边界,这不是套话,是真实存在的风险。
对自己负责的服务做检测,我一般分三步走。第一步是端点枚举,直接访问/actuator看返回的_links索引,这一步信息最全,很多没做收敛的服务会在这里把所有开着的端点列出来。第二步是敏感度确认,对/env、/configprops、heapdump这几个重点端点做只读验证——注意是只读,确认它是否真的返回了内容,不要做任何写操作。第三步是内容筛查,对拿到的内容做敏感字段匹配,重点看password、secret、key、token、datasource这些关键词。
命令行上,一条curl -s http://目标/actuator | jq就能拿到端点索引,比翻文档快得多。如果是批量资产,写个简单的循环遍历常见路径列表即可。不要迷信现成的扫描器,很多工具对 Actuator 的识别逻辑很粗糙,只测几个固定路径,反而漏掉改过management.endpoints.web.base-path的服务。
还有一个小技巧:端点路径可以被改。有些项目为了"防止被扫",把 base-path 从/actuator改成了别的。这种"藏起来"的思路我不推荐——安全靠的是收敛权限而不是隐藏路径,但检测时确实要留意这种改法,可以从/actuator/mappings或者 Swagger 文档里反推真实的端点前缀。
3. Swagger、Druid、H2 Console:那些"顺手打开"的调试后门
Actuator 之外,还有一批"开发时顺手加上、上线忘了摘"的东西。它们不是 SpringBoot 框架自带的,但几乎每个真实项目都会引入一两个,所以必须单独说。
3.1 Swagger 文档暴露出来的攻击地图
Swagger(现叫 OpenAPI)本身是个好东西,把接口文档自动生成出来,前后端联调效率翻倍。问题在于它的访问控制往往是零。生产环境的/swagger-ui.html或/doc.html一旦对外开放,等于把自己所有接口的路径、参数结构、请求方法全画成一张地图贴出来。
对攻击者来说,这比猜接口路径高效太多了。有了完整的接口清单,接下来就是逐个测试未授权和越权:哪些接口没做鉴权、哪些接口能改别人的 ID、哪些接口的参数能被注入。很多"越权漏洞"之所以能被快速批量发现,前提就是文档把那层遮羞布掀了。
加固上,简单的做法是环境隔离,在生产环境的配置里直接关掉 Swagger。用 springdoc 的话可以这样处理,把文档只在非生产环境启用:
springdoc: api-docs: enabled: false swagger-ui: enabled: false如果你确实需要在预发布环境保留文档,那就加一层认证,比如把文档路径塞进 Spring Security 的过滤链里,要求登录后才能访问。别用"改个路径让别人猜不到"这种隐藏手段,那顶多增加一点扫描成本,挡不住有心人。
3.2 Druid 监控页与 H2 控制台
Druid 是国内用得非常多的数据库连接池,它自带的监控页面/druid/index.html功能很全:SQL 监控、连接池状态、URI 监控、Session 监控。默认访问是没认证的,需要你在配置里显式打开登录控制。更细思极恐的是,Druid 的 Session 监控会把正在活跃的 HTTP 会话列出来,配合一些老版本里存在的 Session 伪造问题,理论上可以做会话层面的操作。所以这页面的敏感度不比 Actuator 低。
H2 是个嵌入式数据库,很多时候被用作开发环境的内存库,它的 Web 控制台/h2-console一旦在生产开着,就是个大问题。历史上 H2 控制台有个很出名的 JNDI 问题,攻击者可以借助它能被配置 JDBC URL 的特性,加载远程类导致代码执行。这个问题的核心不是 H2 本身设计有多差,而是"一个能连数据库的管理控制台被暴露在无认证的公网"。
这两类东西的共同特征是:它们的访问控制不是框架默认给的,而是需要开发者手动加。一旦忘了,就是完全裸奔。我的建议很直接——生产环境永远不要开 Druid 监控页和 H2 控制台,如果用 Druid 又想留监控,就把监控数据接到内部的监控系统(比如 Prometheus 抓 Druid 暴露的指标端点),而不是开放那个 HTML 页面。
3.3 Jolokia 与 Spring Cloud Gateway Actuator
Jolokia 是一个把 JMX 操作转成 HTTP 的桥接组件,SpringBoot 里引入它之后会暴露/actuator/jolokia。它本身是个中性工具,但在未授权访问的场景下,能通过 HTTP 调用 MBean 意味着可操作的范围大大扩展——包括动态修改日志配置、触发一些原本只能通过 JMX 客户端做的高危操作。在特定条件下,配合被管理的应用里的其他组件,能达到 RCE。所以jolokia端点被安全扫描器单独列为高危不是没道理的。
Spring Cloud Gateway 这边,前面提到的 SpEL 注入是个典型。它的/actuator/gateway/routes端点允许动态增删路由,正常情况下需要认证。但在出问题的版本里,构造恶意路由后触发刷新,就能执行 SpEL 表达式。这类问题的修复其实很简单——升级版本、把 gateway 相关 actuator 端点关掉就行,麻烦的是存量资产排查,很多团队根本不知道自己有多少个网关实例还跑在受影响版本上。
注意:这些组件端点的风险是叠加的。一个服务同时开着 Actuator、Swagger、Druid 监控,等于把三条信息泄露的通道全打开,任何一条被利用都够呛。加固时不要挑着关,要按"生产环境零暴露"的原则整体收敛。
4. 集成组件带进来的未授权访问面
SpringBoot 的价值很大程度上在于"集成生态",你能几分钟接上 Nacos、Redis、MongoDB、消息队列。但便捷的另一面是:这些中间件自身的未授权问题,会顺着集成关系被带进来,而且很多人只关注 SpringBoot 应用本身,忽略了它背后的组件。
4.1 Nacos 未授权与配置中心的连带风险
Nacos 作为配置中心和注册中心,在 Spring Cloud Alibaba 体系里几乎是标配。它自身的控制台在某些版本和配置下存在未授权访问的问题,特别是命名空间(namespaces)相关的接口。一旦 Nacos 控制台能被未授权访问,后果比单纯的"看到几个配置项"要严重得多——它管的是整个微服务的配置。攻击者能读到数据库连接、MQ 地址这类信息,甚至能修改配置,让下游服务在下次拉取配置时加载恶意内容。
评估 Nacos 的时候,光看应用层的 SpringBoot 配置是不够的,要顺着它连的配置中心地址往上游查。判断一个 Nacos 是不是被正确加固,我看两点:一是控制台是否强制登录、有没有改掉默认口令;二是 namespace 的权限隔离有没有做,不同环境的配置是不是物理隔离的。默认的 public namespace 里什么都有,这是最常见的坑。
4.2 Redis 和 MongoDB 在 SpringBoot 场景下的裸奔
这两个是"未授权访问"的老熟人了。它们的默认配置里,启动后如果没设密码、又监听了公网地址,那就是直接对外开放。搜索热词里出现springboot, mongodb未授权访问漏洞和redis在springboot中的使用,说明这两者在 SpringBoot 项目里是高频集成对象,也高频出现在扫描报告里。
原理不复杂。Redis 未授权就是因为你连上去不需要密码,能执行CONFIG SET、SAVE、SLAVEOF这些命令;MongoDB 未授权类似,默认端口 27017 无认证就能读写任意数据库。对 SpringBoot 应用来说,这层风险常常被应用侧的连接池掩盖住了——开发者本地用redis://localhost连得好好的,生产换了地址却忘了在服务端加requirepass,或者加了密码但没改bind只监听本地。加固的核心就是三件事:设强密码、限制监听地址、在中间件前面加网络层访问控制。SpringBoot 侧只是客户端,亡羊补牢要补在正确的墙上。
4.3 依赖版本治理,从 log4j 那课学到什么
提到log4j漏洞(Log4Shell),很多人的第一反应是"那是日志组件的锅"。但站在 SpringBoot 项目视角,这课真正教会我们的是:你依赖树里的每一个第三方组件,都在你的暴露面上。log4j 的问题之所以杀伤力大,是因为它被嵌套在无数个依赖里,很多项目自己都不知道用了它。
对未授权访问这类问题也是一样。SpringBoot 项目的pom.xml或build.gradle里层层传递的依赖,可能带进来一个默认开放管理接口的组件。我的做法是定期跑一次依赖树梳理,用mvn dependency:tree或 Gradle 的dependencies任务,把各版本的组件捞出来对照已知问题清单。别等扫描器报了再临时抓瞎,那会儿往往已经在生产环境暴露很久了。
更实际一点的操作:把安全相关的依赖版本管理纳入构建流程,比如用依赖管理插件做一个统一的版本约束,发现某个传递依赖带进来老版本时,直接在dependencyManagement里强制覆盖。
5. 从配置到架构的加固清单
前面讲了那么多"是什么"和"为什么危险",这一节讲怎么收口。我不喜欢那种"十大加固建议"式的清单,太笼统,落地时用不上。按我的经验,加固要分三层做:应用配置层、网络架构层、工程流程层。三层缺一不可,只在配置层使劲,架构上没隔离,一样会出问题。
5.1 应用配置层,最小化暴露的模板
最直接的收口是在 SpringBoot 配置里把不该开的都关了。下面这个模板我一般直接发给开发团队,按需调整:
management: endpoints: web: exposure: include: health,info base-path: /internal-monitor endpoint: health: show-details: never env: enabled: false heapdump: enabled: false configprops: enabled: false beans: enabled: false mappings: enabled: false这里几个细节值得说。include是白名单机制,只列你真正需要的,别用*。base-path改成非默认路径能减少被扫描器撞到的概率,但这不是安全措施,别当依赖。show-details: never是防止健康检查泄露组件信息。对于确实不需要的端点,直接enabled: false比指望include拦住更保险——两处都收,双保险。
再补一句,include控制的是 Web 暴露,如果你想彻底关掉某类端点,用enabled: false。因为如果通过 JMX 也能访问的话,只改 Web 暴露是不够的,enabled是从根本上禁用。生产环境我通常直接禁用敏感端点的enabled。
5.2 网络层与网关层的纵深防御
配置层解决的是"应用愿意暴露多少",网络层解决的是"别人能不能摸到"。这两层是乘法关系。我见过配置做得挺干净的项目,结果因为放在了没有访问控制的公网,被人扫到别的老接口打了个措手不及。
我的建议是:所有运维管理类端点(Actuator、Druid 监控、Swagger 文档)都不对公网开放,只允许内网运维网段访问,或者在网关层加统一认证。如果服务确实部署在公网,那至少要在反向代理(Nginx 之类)上对这些路径做拦截,比如:
location ~* ^/(actuator|druid|swagger|h2-console) { deny all; return 404; }注意这里返回 404 比返回 403 更好,403 会明确告诉扫描器"这个路径存在但没权限",而 404 让它以为路径压根不存在,减少被盯上的概率。当然,这只是增加攻击成本,不能替代真正的权限控制。真正的访问控制还是要靠认证和网络隔离。
5.3 把安全配置纳入工程流程
最后这层最容易被人忽略,也最重要。上面这些配置,如果每次上线都靠人肉记,迟早会有人忘。我的做法是把它工程化:
一是做配置模板的强制校验,比如在 CI 里加一个检查,扫描application-prod.yml是否包含危险配置(include: "*"、show-details: always、Swagger 的enabled: true),命中了就构建失败。二是把生产环境的配置单独维护,用application-prod.yml覆盖默认行为,而不是在application.yml里靠 profile 开关切来切去,容易漏。三是建立上线前自检清单,把"扫一遍生产地址的 Actuator、Swagger 路径"作为发布流程的一步。
这套做法的好处是把安全从"个人意识"变成"流程约束"。靠人总会出错,靠流程能稳定输出。我在几个团队推行过,最开始开发会觉得麻烦,但等扫出来一批生产环境的暴露端点之后,就没那么抗拒了。
6. 常见问题排查速查表与实操心得
聊完方法论,落到实操上,我把常见的问题和排查思路整理成表,方便你对着自查。这一节里也会穿插一些我自己踩过的坑,都是文档里不会写的。
6.1 排查速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
扫描器报/actuator/env未授权 | 端点暴露范围过宽 | 查management.endpoints.web.exposure.include |
/actuator/health泄露数据库信息 | show-details被设为 always | 改回never或when-authorized |
| Swagger 在生产可访问 | 文档未按环境关闭 | 检查 springdoc/springfox 的启用配置 |
| Druid 监控页裸奔 | 未开登录控制,或默认口令 | 检查stat-view-servlet的login-username/password |
| 报 Nacos 未授权 | 控制台未认证或权限隔离缺失 | 检查控制台鉴权开关、namespace 权限 |
| 报 Redis/MongoDB 未授权 | 中间件未设口令、监听公网 | 直连测试redis-cli -h、mongosh连接鉴权 |
| Nginx 拦了但应用仍可直接访问 | 拦截只做在代理层,直接 IP 可绕过 | 检查应用监听地址和防火墙规则 |
这张表的用法是先定位现象,再顺着排查方向去看具体配置。有一点要强调:代理层的拦截不能替代应用本身的收敛,如果应用端口本身在公网可达,绕过代理直接访问是分分钟的事。所以排查时要同时确认"应用监听在哪个网卡/端口"和"这个端口有没有对外放行"。
6.2 我踩过的几个坑
坑一:以为改了 base-path 就安全了。早期我做过一次加固,把/actuator改成了一个自认为很隐蔽的路径,觉得能挡住大部分扫描。结果没过多久就发现,因为服务同时开着 Swagger,扫描器从接口文档里反推出了端点前缀。这件事让我彻底明白,隐藏路径是安全剧场,真正有效的是认证和网络隔离。
坑二:窄化 include 后忘了 health 的细节。有一次我自信地把include收成了只留health,觉得已经够干净。后来做复查才发现,这个环境的show-details是always,健康检查把 Redis、MySQL 的详细状态和地址全吐出来了。从那以后我养成习惯,改 Actuator 配置时一定连health的细节配置一起检查。
坑三:用扫描器"通过了"当验收标准。有段时间我把"扫描器不报未授权问题"当作加固完成的标志。直到一次真实演练,测试方用改过路径的 Actuator 直接拿到了配置——扫描器只测默认路径,压根没覆盖到自定义前缀。这件事的教训是:验收标准不能依赖工具,要拿自己的实际配置去验证,把自己当成攻击者,想想如果我是对方会从哪个入口进。
坑四:忽略了依赖带进来的老组件。客户的一个老项目,SpringBoot 版本升到了新版本,本以为万事大吉,结果扫描发现某个传递依赖带进来的旧版组件里还开着管理端点。版本治理这件事不能只看主框架版本,依赖树里的每一个都要照顾到。
坑五:把 Redis、MongoDB 当成"基础设施的锅"。应用侧配置做得再干净,如果连的后端存储本身裸奔,前端收口就是徒劳。我们排查流程里现在固定加一步:从应用配置里提取所有中间件连接地址,反向验证这些地址的访问控制。这一步经常能揪出应用层压根不知道的问题。
写到这里,关于 SpringBoot 未授权访问这一块,我个人的核心体会其实就一句话:这类问题极少是"框架的锅",绝大多数是配置和流程没跟上。框架提供了能力和开关,怎么配、什么时候关、谁来检查,全都取决于团队的工程成熟度。与其追着每个新爆出来的 CVE 补丁跑,不如把"生产环境零暴露、运维端点必认证、依赖版本有治理"这三条变成习惯。真做到了,你会发现扫描报告里这类条目会肉眼可见地变少。