在 Spring Boot 4.1.1 上把 actuator 端点全开,想拿数据库密码已经没那么容易了。heapdump 从 Spring Boot 3.5 起默认 access 为 none,management.endpoints.web.exposure.include=* 不会再把它放出来;/actuator/env 的值从 3.0 起就默认打码成星号。这两条默认值在 3.x 就已生效,4.x 沿用至今。
真要泄露,得自己把 heapdump 的 access 和 env 的 show-values 都额外拨开。下面用 Spring Boot 4.1.1 把几种配置各跑一遍,输出原样贴出。
实验项目
项目从 start.spring.io 生成,Spring Boot 4.1.1,依赖是 Spring Web MVC 和 Actuator,运行在 JDK 21.0.12.1。为了有个像样的秘密可泄露,加了一个从配置读数据库密码的 Bean,密码走环境变量注入:
@Component
public class DbClient {
private final String url;
private final String password;
public DbClient(@Value("${app.db.url}") String url,
@Value("${app.db.password}") String password) {
this.url = url;
this.password = password;
}
public String url() {
return url;
}
}
spring.application.name=actdemo
app.db.url=jdbc:postgresql://db.internal:5432/orders
app.db.password=${DB_PASSWORD}
启动时设 DB_PASSWORD=Orders-Pr0d-9f3k,这是演示用的假密码,下面的命令都拿它当靶子。
把端点全开,看 heapdump 在不在
$ DB_PASSWORD=Orders-Pr0d-9f3k java -jar target/actdemo-0.0.1-SNAPSHOT.jar --server.port=18101 --management.endpoints.web.exposure.include=*
$ curl -s localhost:18101/actuator | python3 -c 'import json,sys;print(sorted(json.load(sys.stdin)["_links"]))'
['beans', 'conditions', 'configprops', 'configprops-prefix', 'env', 'env-toMatch', 'health', 'health-path', 'info', 'loggers', 'loggers-name', 'mappings', 'metrics', 'metrics-requiredMetricName', 'sbom', 'sbom-id', 'scheduledtasks', 'self', 'threaddump']
$ curl -s 'localhost:18101/actuator/env/app.db.password'
{"activeProfiles":[],"defaultProfiles":["default"],"property":{"source":"Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'","value":"******"},"propertySources":[{"name":"server.ports"},{"name":"commandLineArgs"},{"name":"servletConfigInitParams"},{"name":"servletContextInitParams"},{"name":"systemProperties"},{"name":"systemEnvironment"},{"name":"random"},{"name":"Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'","property":{"origin":"class path resource [application.properties] from actdemo-0.0.1-SNAPSHOT.jar - 3:17","value":"******"}},{"name":"applicationInfo"},{"name":"Management Server"}]}
$ curl -s localhost:18101/actuator/heapdump -o hd.bin -w 'http=%{http_code} size=%{size_download}\n'
http=404 size=101
include=* 放出来一大堆端点,env、beans、configprops、threaddump 都在。列表里没有 heapdump 和 shutdown。这两个端点的默认 access 是 none,不可访问的端点根本不会注册,exposure 里写了也没用。startup 不在列表里是另一个原因,它要求应用配置 BufferingApplicationStartup。
直接请求 heapdump 拿到 404。env 里那条密码的 value 是 ******。
这两道默认值是分几步加上的。Spring Boot 3.0 把 env 和 configprops 改成默认全部打码,开关是 management.endpoint.env.show-values 和 management.endpoint.configprops.show-values,默认 never。2.x 只按键名打码,名字里带 password、secret、token 之类的键会被遮住,其他键照样明文。
3.4 引入了 management.endpoint.<id>.access 和 management.endpoints.access.default,取代原来的 .enabled 系列属性,4.0 把旧属性彻底删掉。3.5 再把 heapdump 的默认 access 改成 none,此后要用它必须同时 expose 并配置 access。在 3.4 及更早的版本里,include=* 会连 heapdump 一起放出来,一个公网可达的 actuator 就能让人把整个堆下载走。4.1.1 的 HeapDumpWebEndpoint 类上标的正是 @WebEndpoint(id = "heapdump", defaultAccess = NONE)。这一版的其他改动可以看之前整理的 Spring Boot 3.5 新特性。
env 的打码挡不挡得住
env 的脱敏由 management.endpoint.env.show-values 控制,默认 never。设成 when-authorized,在没有配认证和授权的情况下,值照样是星号:
$ DB_PASSWORD=Orders-Pr0d-9f3k java -jar target/actdemo-0.0.1-SNAPSHOT.jar --server.port=18102 --management.endpoints.web.exposure.include=* --management.endpoint.env.show-values=when-authorized
$ curl -s 'localhost:18102/actuator/env/app.db.password'
{"activeProfiles":[],"defaultProfiles":["default"],"property":{"source":"Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'","value":"******"},"propertySources":[{"name":"server.ports"},{"name":"commandLineArgs"},{"name":"servletConfigInitParams"},{"name":"servletContextInitParams"},{"name":"systemProperties"},{"name":"systemEnvironment"},{"name":"random"},{"name":"Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'","property":{"origin":"class path resource [application.properties] from actdemo-0.0.1-SNAPSHOT.jar - 3:17","value":"******"}},{"name":"applicationInfo"},{"name":"Management Server"}]}
改成 always,明文就出来了。这一组只暴露 env:
$ DB_PASSWORD=Orders-Pr0d-9f3k java -jar target/actdemo-0.0.1-SNAPSHOT.jar --server.port=18107 --management.endpoints.web.exposure.include=env --management.endpoint.env.show-values=always
$ curl -s localhost:18107/actuator/env/app.db.password
{"activeProfiles":[],"defaultProfiles":["default"],"property":{"source":"Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'","value":"Orders-Pr0d-9f3k"},"propertySources":[{"name":"server.ports"},{"name":"commandLineArgs"},{"name":"servletConfigInitParams"},{"name":"servletContextInitParams"},{"name":"systemProperties"},{"name":"systemEnvironment"},{"name":"random"},{"name":"Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'","property":{"origin":"class path resource [application.properties] from actdemo-0.0.1-SNAPSHOT.jar - 3:17","value":"Orders-Pr0d-9f3k"}},{"name":"applicationInfo"},{"name":"Management Server"}]}
确实要让人看明文,用 when-authorized,再配上认证和 management.endpoint.env.roles,不要直接设成 always。
什么都不配是什么样
生产里更常见的是根本没碰 exposure 配置:
$ DB_PASSWORD=Orders-Pr0d-9f3k java -jar target/actdemo-0.0.1-SNAPSHOT.jar --server.port=18103
$ curl -s localhost:18103/actuator | python3 -c 'import json,sys;print(sorted(json.load(sys.stdin)["_links"]))'
['health', 'health-path', 'self']
$ curl -s -o /dev/null -w 'heapdump http=%{http_code}\n' localhost:18103/actuator/heapdump
heapdump http=404
$ curl -s -o /dev/null -w 'env http=%{http_code}\n' localhost:18103/actuator/env
env http=404
Web 默认只暴露 health,env 和 heapdump 都是 404。
真要泄露,得自己拨两道开关
heapdump 要能访问,光 expose 不够,还得显式把它的 access 设成 unrestricted。下面这组把 heapdump 和 env 都暴露出来,heapdump 的 access 设成 unrestricted,env 的值设成 always:
$ DB_PASSWORD=Orders-Pr0d-9f3k java -jar target/actdemo-0.0.1-SNAPSHOT.jar --server.port=18105 --management.endpoints.web.exposure.include=heapdump,env --management.endpoint.heapdump.access=unrestricted --management.endpoint.env.show-values=always
$ curl -s localhost:18105/actuator/heapdump -o hd.bin -w 'http=%{http_code} size=%{size_download}\n'
http=200 size=32166169
$ strings hd.bin | grep -c Orders-Pr0d-9f3k
6
heapdump 返回 200,下载到一个约 32MB 的 HPROF 文件,strings 在里面数到 6 次明文密码。堆里是活数据,命中次数每次跑都不一样,这次是 6 次。用 jmap 导出堆再放进 Eclipse MAT 里搜字符串,效果一样,只是 actuator 这条路不用登录机器。
show-values 只决定 /actuator/env 回不回明文。密码字符串本来就在堆里,heapdump 一旦能下载,env 打不打码都挡不住。
线上怎么配
exposure 的 include 只写真正要用的几个端点。include=* 虽然拿不到明文密码,但 beans、configprops、mappings、threaddump 会把 Bean、配置项、请求映射和线程栈全亮出来。想更保守,可以设 management.endpoints.access.default=none,再用 management.endpoint.<id>.access 逐个打开需要的端点。
heapdump 的 access 和 env 的 show-values 保持默认的 none 和 never。线上要抓堆,用 jmap 或容器平台自带的方式,别长期开着这个端点。管理端点还可以用 management.server.port 放到单独端口,再用网络策略只放行运维网段。
一起交流
分享你的思考,让讨论更进一步。