ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot未授权访问:Actuator等高危端点与修复方案

SpringBoot未授权访问:Actuator等高危端点与修复方案 1. 从一次内网扫段说起SpringBoot 的未授权访问为什么这么常见很多人第一次意识到 SpringBoot 未授权访问这个问题都不是在做安全测试的时候而是在做资产梳理或者内部巡检的时候。我自己印象比较深的一次是帮一个团队梳理测试环境资产端口扫描跑完满屏都是 8080、8081、8082 这类端口随手挑一个访问根路径返回一个 Whitelabel Error Page看着人畜无害。但把路径换成/actuator直接吐出一长串 JSON 端点列表再点进/actuator/env数据库连接串、中间件地址、第三方密钥的 key 名称一览无余。那一刻其实挺震撼的一个功能写得规规矩矩的 SpringBoot 服务可能因为一行配置没写就把自己的底裤露在了内网里。未授权访问这个词听起来很安全圈但用大白话讲就是一句话本该登录之后才能看、才能改的接口或页面在没登录的情况下直接就能访问而且往往还能读到敏感数据、改掉关键配置。它跟 SQL 注入、反序列化那种需要构造复杂 payload 的漏洞不一样未授权访问很多时候连工具都不用浏览器地址栏敲一下就行。也正因为门槛低它成了 SpringBoot 生态里被报得最多、修得最慢、复发率最高的一类问题。这里面有三个结构性原因理解了这三点后面所有的案例就都能串起来了。第一SpringBoot 的设计哲学是约定优于配置默认帮你把东西装好、开好。这在开发阶段是极大的生产力但对安全来说是反向的——默认值往往是能跑通优先而不是最安全优先。Actuator 在 1.x 时代默认暴露全部端点2.x 虽然收窄到health和info但只要有人为了排障手动加一句include: *并且忘了在发布前收回去暴露面立刻就回来了。第二现代微服务架构把鉴权这件事集中到了网关服务本身反而变裸了。很多团队的做法是所有外部流量必须经过网关网关做统一认证后面的业务服务就假设进来的请求一定是已经认证过的于是服务里根本不写权限校验。这个假设在正常链路下成立但一旦服务端口可以从内网直连或者测试环境把 NodePort 暴露出去整条鉴权链就绕过去了。这不是某个人的疏忽而是架构层面留下的口子。第三SpringBoot 的组件生态太丰富了每一个 starter 都可能自带一个 Web 界面。Druid 有监控页、Swagger 有文档页、H2 有控制台、Nacos 有管理台、Prometheus 有指标端点。这些界面在引入依赖的那一刻就已经存在了只是大多数人没意识到我引入了一个依赖等于我开了一个页面。这篇文章我想做的事情很具体把 SpringBoot 项目里最常见的几类未授权访问场景拆开讲清楚包括它们的暴露位置、判断方法、背后成因以及我在实际项目里验证有效的收敛方案。文中所有验证命令请只在自有资产或已获得书面授权的环境中使用这是底线越过去就不是技术分享了。2. Actuator 端点最经典的暴露面也是被误判最多的一个2.1 端点清单与各自的真实危害等级Actuator 是 SpringBoot 官方提供的运维监控模块本意是给运维同学看健康状态、线程栈、环境变量的。它的端点命名很直白功能也很直白直白到一旦暴露就几乎没有门槛。先把各个端点的危害等级排一遍这个表我建议直接贴到团队的巡检清单里端点路径泄露内容危害等级典型后果/actuator/env全部环境变量、配置项、系统属性高数据库账号、密钥名、内网拓扑泄露/actuator/heapdump完整堆内存快照文件极高内存中的明文密码、Token、会话/actuator/jolokiaJMX MBean 操作入口极高可触发日志组件加载逻辑/actuator/gateway/routes网关路由表中高暴露内部服务地址与路径规则/actuator/mappings全部请求映射中帮手找出未公开的内部接口/actuator/beans全部 Bean 及依赖关系中推断框架版本与可用组件/actuator/configprops配置属性绑定详情中高与 env 类似含敏感配置/actuator/httptrace最近 HTTP 请求记录中高含请求头、Cookie/actuator/threaddump线程栈低中辅助判断业务逻辑/actuator/loggers动态调整日志级别中可被用来放大日志、写文件/actuator/health健康状态低一般可放开注意别开 details这张表里我想特别强调两个因为它们经常被低估。一个是heapdump。很多人以为它只是下载一个文件没什么大不了的。但 heapdump 是运行时的完整内存镜像Spring 容器里的数据源对象、连接池里的连接串、缓存中的用户会话、请求上下文里的 Authorization 头全都在里面。拿到 heapdump 之后用内存分析工具打开搜password、secret、token这几个关键字往往几分钟就能捞出可用凭据。从能下载 heapdump到拿到数据库权限中间几乎没有技术壁垒。另一个是loggers端点配合env。单独看loggers它只是让你改日志级别单独看env它只是读配置。但 2.x 早期版本存在通过/actuator/env修改配置再配合/actuator/refresh生效的链路这条链路在某些组件版本上可以走到加载外部资源那一步。虽然官方后续版本做了限制但能写配置这个能力本身就不该出现在未认证的端点上。2.2 为什么默认收窄了还是会被打穿SpringBoot 2.x 之后的默认配置其实已经很克制了management.endpoints.web.exposure.include默认只包含health和info。所以如果你看到一个项目 Actuator 全开基本可以断定是有人主动改过配置。常见的几种改法每一种背后都有一个合理的理由为了本地排障方便在application.yml里写了include: *然后这份配置跟着代码一起上了生产。多环境配置文件profile管理混乱application-dev.yml、application-test.yml、application-prod.yml里只有 dev 写了收窄prod 继承了 base 里的全开。为了做 Prometheus 监控必须暴露/actuator/prometheus顺手把include写成了*因为写具体列表嫌麻烦。用了配置中心运维在 Nacos 或 Apollo 上临时改过include想排障排完忘了改回来。这种情况最难查因为代码仓库里搜不到。我在实际排查里养成了一个习惯不要只看代码仓库里的配置文件一定要在运行中的实例上实际请求一次/actuator。因为配置来源可能有三层——本地文件、环境变量、远程配置中心最终生效的是合并后的结果而只有请求一次才能看到真实答案。# 仅在自有或已授权环境执行观察返回的端点列表 curl -s -i http://127.0.0.1:8080/actuator | head -40判断要点在于状态码和响应体返回200并且是一段带_links的 JSON说明端点根路径可访问接下来要逐个看_links里列出的端点。返回404说明端点没暴露或者base-path被改过。这时候可以试几个常见变体/manage、/internal、/monitor、/actuator/。返回401或403说明有安全框架接管了这是好现象。返回302跳登录页也说明有拦截。注意/actuator/health本身可以放开但如果配置了show-details: always它会连带返回数据库、Redis、磁盘的健康详情里面可能包含连接地址和异常堆栈。巡检时这个也要看一眼。2.3 独立管理端口不是万能的业内有条流传很广的建议把 Actuator 挪到独立端口比如management.server.port: 18080然后这个端口只绑内网地址。这个建议本身没问题但它的效果完全取决于绑定地址和网络策略写成下面这样才有意义management: server: port: 18080 address: 127.0.0.1 endpoints: web: base-path: /_internal/monitor exposure: include: health,info endpoint: health: show-details: neveraddress: 127.0.0.1的意思是只监听本机回环这样即使容器网络里其他服务能连到这台机器也连不上这个端口只有本机的运维代理比如 node exporter、日志采集器能访问。如果只写了端口没写 address默认还是会监听0.0.0.0同网段一扫就能发现。我见过一个反例服务本身部署在 Kubernetes 里Pod 的 Service 是 ClusterIP按理说外网访问不到。但集群里跑着一个同样有未授权问题的组件攻击者从那个组件进去之后在集群内部横向扫 18080一样能拿到 heapdump。所以**内网不是一个安全边界它只是一个访问难度更高的区域**。真正靠谱的做法是给 Actuator 本身加上认证。2.4 给 Actuator 套一层认证的正确姿势如果项目已经引入了 Spring Security保护 Actuator 非常简单关键是别把它的过滤器链和业务的混在一起否则很容易因为顺序或者匹配规则把 Actuator 放出去Configuration public class ActuatorSecurityConfig { Bean Order(1) public SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception { http.securityMatcher(EndpointRequest.toAnyEndpoint()) .authorizeHttpRequests(auth - auth.anyRequest().hasRole(OPS)) .httpBasic(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable); return http.build(); } Bean Order(2) public SecurityFilterChain appChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth - auth .requestMatchers(/api/public/**).permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } Bean public JwtAuthFilter jwtFilter() { return new JwtAuthFilter(); } }这段代码里有三个容易写错的地方我逐个说。Order(1)必须加在 Actuator 的过滤链上。EndpointRequest.toAnyEndpoint()是一个匹配器它的匹配范围包含base-path的变体比手写requestMatchers(/actuator/**)更可靠——后者在你改了base-path之后就会失效这也是很多项目改了路径以为安全了其实拦截规则压根没生效的原因。securityMatcher而不是requestMatchers。securityMatcher决定的是这条过滤链管不管这个请求requestMatchers决定的是这条链内部怎么授权。用错的话会出现两种极端要么 Actuator 的链把所有请求都管了业务接口全要 Basic 认证要么业务链把 Actuator 也管了但业务链里 Actuator 路径被放行了。csrf要关掉。Actuator 的很多端点是 POST 请求开着 CSRF 会导致运维工具调不通然后在排障压力下有人干脆把整条链删了。3. 被依赖顺带装进来的那些管理页面Actuator 是官方组件大家多少有防备心。真正容易出事的是那些第三方依赖自带的管理界面——它们在引入的那一刻就存在但几乎没人把它当成一个需要防护的页面。3.1 Swagger / Knife4j接口文档变成了接口清单只要项目里引了 springfox 或者 springdoc默认就会生成一套接口文档页面常见路径有这么几个/swagger-ui.html、/swagger-ui/index.htmlspringdoc 新版/v2/api-docs、/v3/api-docsOpenAPI JSON 描述/doc.html、/webjars/**Knife4j 增强版/swagger-resources这套页面的问题不在于泄露了接口文档而在于它是一份完整的、实时的、带参数说明的接口清单。攻击者拿到之后可以直接看到哪些接口没有鉴权、哪些接口的参数是路径拼接、哪些接口带文件上传从不知道打哪儿变成照着列表挨个试。经常有人反驳我我们的接口文档页面有登录才能看。那就要确认一下/v2/api-docs这个 JSON 端点是否也被保护了。很多项目只给 UI 页面加了拦截忘了 JSON 描述端点是独立路径结果页面打不开JSON 直接下载。处理方式我推荐分环境开关而不是在每个环境都靠拦截器拦# application-prod.yml springdoc: api-docs: enabled: false swagger-ui: enabled: false如果是 springfox对应的是springfox: documentation: enabled: false用配置开关的好处是生产环境这几个路径直接返回 404从行为上就等同于不存在不需要依赖拦截器的匹配规则是否正确。我踩过的一个坑就是某个项目在拦截器里写了excludePathPatterns(/swagger-ui/**)想放行静态资源结果/**这种通配在多段路径匹配上的行为和预期不一致把不该放的也放了。用配置开关就没这个问题。3.2 Druid 监控页数据源信息 可被重置的统计用 Druid 连接池的项目只要加了druid-spring-boot-starter并启用 StatViewServlet就会有一个/druid/index.html监控台。它能看的东西包括SQL 执行统计、慢查询、URI 监控、Session 监控更重要的是数据源的连接地址和用户名密码虽然打码但地址和用户名本身就是重要信息。这个页面还有一个常被忽略的点如果reset-enable没关访问/druid/reset-all.json可以清空统计数据。这个操作不致命但它说明了一个问题——未授权访问不只是读还可能是写。正确的配置是这样spring: datasource: druid: stat-view-servlet: enabled: true login-username: ops_monitor login-password: ${DRUID_MONITOR_PWD} reset-enable: false allow: 127.0.0.1 deny: 0.0.0.0/0 web-stat-filter: enabled: true exclusions: *.js,*.css,/druid/*,/actuator/*allow和deny的配置要注意顺序Druid 的判定逻辑里deny优先。写allow: 127.0.0.1加上deny: 0.0.0.0/0的组合效果是只允许本机访问。如果所在环境有反向代理记得把代理的出口地址也加进 allow不然运维自己也进不去。3.3 Nacos、MongoDB、Redis服务端组件的独立暴露面这三个严格来说不属于 SpringBoot 框架本身但它们经常和 SpringBoot 项目一起部署而且在热词里出现频率极高所以必须一起说。Nacos 在早期版本默认关闭鉴权管理台可以直接访问/nacos能看到所有命名空间和服务列表甚至能读配置。配置文件里往往写着数据库密码、第三方密钥。现在新版默认要求配置鉴权但升级旧集群时很多人为了先跑起来把鉴权又关掉了。判断方法很简单访问/nacos/v1/console/namespaces如果直接返回命名空间列表而不是 403就是没开鉴权。MongoDB 和 Redis 的情况类似核心是两个参数是否开启了认证以及绑定地址是不是0.0.0.0。SpringBoot 项目里如果连接串写成mongodb://10.0.0.21:27017/appdb没有用户名密码那说明这个实例本身没开认证。很多时候开发者会说我们不对外开放 27017但只要内网有任何一台机器被拿下这个实例就等同于公开的。SpringBoot 侧的正确连接方式应该始终带上凭据并指定认证库spring: data: mongodb: uri: mongodb://app_user:${MONGO_PWD}10.0.0.21:27017/app_db?authSourceapp_db data: redis: host: 10.0.0.22 port: 6379 password: ${REDIS_PWD} timeout: 2000ms提示Redis 从 6.x 开始有 ACL可以按用户授权比起单一的requirepass更细粒度。生产环境建议关掉FLUSHALL、CONFIG、KEYS这类高危命令用rename-command重命名或者用 ACL 规则禁用。3.4 H2 Console 与开发期残留这个场景通常出现在用 H2 做单元测试或者本地演示的项目上。spring.h2.console.enabled: true一旦跟配置一起带到了测试环境/h2-console就是一个可以直接执行 SQL 的 Web 界面。它的危害不用多说——如果用的是远程 H2 或者文件模式等于把数据库的读写权限挂在了 HTTP 上。我在实际项目里的做法是H2 只在 test 目录下的配置里启用主application.yml里根本不出现这个配置项。靠 profile 隔离而不是靠人记住去关才不容易漏。4. 鉴权链路上的坑为什么代码里明明写了校验还是被绕过去前面说的是功能没关这一节说的是更隐蔽的情况鉴权代码写了但写得不对或者写了却没生效。这类问题排查起来最费时间因为代码 review 的时候看着都对。4.1 JWT 校验里那些看起来能跑的写法JWT 是现在最主流的无状态认证方案它的使用陷阱也最密集。我把见过的几类问题按危险程度排一下。第一类是用错了 API。某些 JWT 库同时提供了parse和parseClaimsJws两个方法前者不校验签名后者校验。如果代码里写的是前者那么任何人把 payload 改一改把用户名改成管理员签名随便填服务端都会照单全收。这个问题的可怕之处在于功能测试完全正常——正常登录发的 token 也能解析成功只有在有人篡改 payload 的时候才会暴露。public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .requireIssuer(order-service) // 校验签发方 .requireAudience(web-client) // 校验受众 .build() .parseClaimsJws(token) // 一定是 parseClaimsJws .getBody(); }除了方法名还有两点要补上requireIssuer和requireAudience这类约束能防止别的系统签发的 token 被本系统误认签名密钥必须来自环境变量或密钥管理系统绝对不能硬编码在代码里也不能用secret、123456这种值——HMAC 密钥太短是可以被离线暴力破解的一般建议不低于 32 字节随机值。第二类是把解析异常吞掉。有些代码会在 catch 块里返回 null然后上层判断if (claims null)就放行——本意是解析失败当匿名用户处理实际效果是只要构造一个解析失败的 token 就能变匿名用户。匿名用户如果正好能访问某些内部接口就等于绕过了认证。第三类是缺少过期与吊销机制。JWT 本身是无状态的签出去就没法撤回。如果签发的有效期是 7 天用户改了密码、账号被禁用旧 token 依然有效 7 天。比较实用的折中方案是有效期设短15 分钟到 2 小时配合 refresh token同时在服务端维护一份已吊销 jti的短周期缓存登出和改密时写入校验时查一次。这个缓存只存到 token 自然过期时间即可容量可控。4.2 Filter 顺序写了但没跑是最难查的一类Java Web 的过滤器链有个很反直觉的特性你写的 Filter 不一定按你以为的顺序执行甚至可能根本不执行。我遇到过最典型的一次是同事用WebFilter(urlPatterns /*)加了认证过滤器本地测试通过上了服务器就失效了。排查过程是这样的先确认 Filter 类被扫描到——加了ServletComponentScan才会注册再确认urlPatterns匹配的是/api/*而实际请求带了 context-path导致路径对不上最后发现问题出在执行顺序上一个更早执行的日志过滤器把请求包装成了另一种类型认证过滤器取不到请求头。这类问题的排查我整理成了一套固定动作在 Filter 的doFilter第一行打日志确认它到底有没有被执行。没执行就说明注册或匹配出了问题别往后查。打印request.getRequestURI()和request.getContextPath()确认拼出来的路径和urlPatterns是否真的匹配。检查注册方式。注解方式WebFilter和编程方式FilterRegistrationBean混用时顺序由order值决定注解方式默认是最低优先级。要控制顺序统一用FilterRegistrationBean设置setOrder()。如果项目里有 Spring Security注意它的FilterChainProxy默认 order 是-100会排在绝大多数自定义 Filter 前面。想让自定义 Filter 在安全链之后跑得说清楚它的位置而不是简单设order。Bean public FilterRegistrationBeanAuditFilter auditFilter() { FilterRegistrationBeanAuditFilter bean new FilterRegistrationBean(); bean.setFilter(new AuditFilter()); bean.addUrlPatterns(/api/*); bean.setOrder(Ordered.LOWEST_PRECEDENCE); return bean; }4.3 拦截器放行规则/**是最大的隐患Spring MVC 的HandlerInterceptor用起来比 Filter 轻很多项目用它做登录校验。问题出在放行列表上registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns( /actuator/**, /swagger-ui/**, /v2/api-docs, /static/**, /error );这段代码非常常见每一条放行看起来都有理由Actuator 是运维用的、Swagger 是开发用的、static 是静态资源、error 是错误页。但把它们组合起来等于业务接口要登录运维接口不要。而运维接口泄露的信息往往比业务接口更敏感。更麻烦的是通配符的匹配语义。/static/**在某些配置下能匹配到/static/../api/xxx这种归一化后越界的路径取决于容器和框架的规范化时机。安全测试里有一大类就是专门针对这类路径归一化差异的。我的建议是反过来写不要写排除清单写允许清单。需要匿名的接口逐个在注解上标记拦截器只检查带注解的接口或者把匿名路径集中定义成一个常量类在代码评审时强制 review 这个类的每次变更。排除清单会随着需求不断变长而且没人会在新增一条排除时去想这会不会放开不该放的。4.4 网关鉴权 服务裸奔的组合拳最后说架构层面的问题这个不解决前面所有细节都是白搭。我见过不止一个项目是这样的外层有一个网关做了统一的 JWT 校验所有业务服务都不写鉴权理由是网关已经校验过了服务信任网关。这个设计的弱点在于信任是怎么传递的——如果服务只是假设请求已经过网关那么任何人只要能直连服务端口就拥有了全部权限。正确的做法有两层第一层是网络层收敛。业务服务的端口不对外发布只允许网关的地址访问。在 Kubernetes 里可以用 NetworkPolicy在虚拟机里用安全组本质上都是只认来源 IP。第二层是应用层补齐。即使有网络策略服务端也应该做一次轻量的校验通常有三种方式校验网关下发的内部签名头比如带时间戳的 HMAC并把签名密钥只配置在网关和内部服务上或者内部也走一次 JWT 校验用Feign拦截器自动透传 token再或者用服务网格的 mTLS把身份校验下沉到基础设施层。这三条里第一条最容易落地效果也最直接。内网服务之间互相信任这个假设在今天的攻击面下已经不成立了因为横向移动的成本比你想象的低得多。5. 一套可复现的自查流程从端口到证据前面讲的是原理和场景这一节给一套可以直接照着跑的自查流程。适用于上线前检查、季度巡检也适用于收到漏洞报告之后快速定位。再次强调只在你拥有或有明确授权的资产上执行。5.1 第一步确定服务实际暴露的端口很多人以为我只启动了 8080实际上一台机器上可能跑着好几个进程。先看监听端口# Linux 环境查看监听端口与对应进程 ss -lntp | grep -E java|node这一步的重点不是找到 8080而是发现那些你以为没开但实际在监听的端口——比如 docker 映射出来的 18080或者某个内嵌的 H2 控制台端口。找到之后逐个确认是否只绑定了127.0.0.1。5.2 第二步按清单逐个探测端点把常见路径整理成一个清单用脚本批量请求只看状态码和响应长度快速筛选出有响应的路径#!/usr/bin/env bash # 仅在自有或已授权资产上使用 TARGEThttp://127.0.0.1:8080 PATHS( /actuator /actuator/env /actuator/heapdump /actuator/mappings /actuator/configprops /actuator/loggers /actuator/httptrace /actuator/health /actuator/prometheus /swagger-ui.html /swagger-ui/index.html /v2/api-docs /v3/api-docs /druid/index.html /druid/reset-all.json /h2-console /nacos /nacos/v1/console/namespaces /manage /internal ) for p in ${PATHS[]}; do code$(curl -s -o /dev/null -w %{http_code} -m 5 $TARGET$p) size$(curl -s -m 5 $TARGET$p | wc -c) printf %-40s %s %s\n $p $code $size done这里有个判断技巧值得记一下光看状态码会漏判。有些系统对所有未知路径都返回 200比如做了统一错误处理返回自定义 404 页面这时候状态码全是 200看不出区别。所以脚本里同时打印响应体长度长度明显偏大或者格式是 JSON 的那些路径才值得深入看。还有一个技巧是看响应头。X-Application-Context、X-Powered-By、Server这类头能帮你确认技术栈和版本对判断后续该试哪些路径很有帮助。5.3 第三步验证是否真的能读到敏感内容筛选出有响应的路径之后需要逐个确认敏感程度。这里的判断标准要客观不能凭看着像就下结论验证项判断依据严重度参考/actuator/env响应中包含数据库地址、用户名、密钥 key 名称高/actuator/heapdump返回application/octet-stream且文件大于 1MB极高/actuator/mappings列出内部接口路径含未公开的管理接口中/actuator/configprops返回绑定属性值含敏感配置中高/v2/api-docs返回完整 OpenAPI JSON含全部接口定义中/druid/index.html页面可打开能看到数据源与 SQL 统计中高/nacos/v1/console/namespaces直接返回命名空间列表高以env为例具体看几个位置propertySources数组里往往能看到systemEnvironment里面放着通过环境变量注入的密码spring.datasource.url能确认数据库是否可达spring.redis.host能确认缓存位置。这些信息单独看都不致命组合起来足够勾勒出内网结构。heapdump的处理要谨慎。下载下来之后不要直接在生产环境分析文件可能几百 MB 到几个 G拷到隔离环境用内存分析工具打开搜索凭据关键字。在正式报告里只需要证明能下载且文件是有效的堆转储就够了不需要把里面的密码贴进报告。这一点是职业素养也是对自己和对方的保护。5.4 第四步记录可复现的证据漏洞报告的写法直接影响修复效率。我习惯按这个结构记录环境信息主机地址报告中对内可以写对外要脱敏、服务端口、服务名。复现步骤从哪个地址开始执行了什么请求用了几步到达敏感数据。步骤要精确到能让人照着重现。证据请求的原始报文curl -i的输出、响应的关键片段。敏感值做脱敏比如spring.datasource.password****。影响判断能读到什么、能改什么、是否可进一步利用。不要夸大可能和确认要分开写。修复建议给到具体配置项级别比如将management.endpoints.web.exposure.include收敛为health并给 Actuator 配置独立端口与认证。我在帮团队做巡检的时候发现一个现象同样的漏洞报告写得具体修复时间可能是一天报告只写存在未授权访问可能拖两周还没人动。因为后者需要修复者自己去确认到底哪里有问题而确认问题的成本往往比修复问题还高。6. 收敛方案从配置、代码到网络的三层防护修复这类问题的思路不是把某个端点关掉而是建立一个分层的收敛机制。我把实践中比较有效的做法按层次整理如下。6.1 配置层默认最小化用 profile 强隔离核心原则是基础配置application.yml永远是最保守的任何放开都必须显式写在特定 profile 里。这样即使有人误把 dev 配置带上生产失效的方向也是功能不可用而不是安全失守。# application.yml —— 基础配置最小暴露 management: endpoints: web: exposure: include: health endpoint: health: show-details: never env: show-values: never springdoc: api-docs: enabled: false swagger-ui: enabled: false# application-dev.yml —— 只在开发环境放开 management: endpoints: web: exposure: include: health,info,env,mappings springdoc: api-docs: enabled: true swagger-ui: enabled: true这里有个细节值得说show-values: never这个配置项很多人不知道。它控制的是/actuator/env返回时配置值是否显示。设成never之后即使端点暴露了值也会被打码成******只留 key 名。这不能替代关闭端点但能作为一层兜底。类似的还有management.endpoint.configprops.show-values。6.2 代码层认证写成可测试的组件鉴权代码有个特点写在 Filter 里、Interceptor 里、AOP 里都能跑但可测试性差别很大。我倾向于把这些逻辑抽成一个独立的 ServiceFilter 只负责从请求里提取凭据、调用 Service、决定是否放行。这样做的好处是可以写单元测试覆盖过期 token篡改签名缺失签发方这些分支而不是每次都靠手工构造请求验证。Service public class TokenService { private final SecretKey key; private final String expectedIssuer; public TokenService(Value(${security.jwt.secret}) String secret, Value(${security.jwt.issuer}) String issuer) { byte[] raw Base64.getDecoder().decode(secret); if (raw.length 32) { throw new IllegalStateException(JWT 密钥长度不足 32 字节拒绝启动); } this.key Keys.hmacShaKeyFor(raw); this.expectedIssuer issuer; } public OptionalPrincipal verify(String token) { try { Claims claims Jwts.parserBuilder() .setSigningKey(key) .requireIssuer(expectedIssuer) .build() .parseClaimsJws(token) .getBody(); return Optional.of(new Principal(claims.getSubject(), claims.get(roles, List.class))); } catch (JwtException | IllegalArgumentException e) { return Optional.empty(); } } }throw new IllegalStateException那一段是我强烈建议加的在启动时校验密钥强度不合格就让服务起不来。这比在运行时发现密钥太弱要有效得多因为运行时没人会主动去看。同样的思路可以用在很多配置上——日志级别、数据源密码是否为空、Actuator 是否意外全开都可以做成启动自检。6.3 网络层让直连这条路走不通配置和代码都会有疏漏网络层的收敛是最后一道防线而且它有个好处它不需要改任何业务代码也不会因为一次发布被回滚掉。具体做法根据部署形态不同在传统虚拟机上核心是安全组规则。业务服务的端口只允许网关和应用负载均衡的地址段访问运维跳板机的地址单独放行其他来源一律拒绝。这里要注意的是不要图省事写成0.0.0.0/0加内部端口没人知道这种模式。在容器编排环境里用 NetworkPolicy 做 Pod 级别的访问控制。默认拒绝所有入站然后按需放行。这个策略的收益很大因为容器环境里的 IP 是动态的靠 IP 白名单很难维护而 NetworkPolicy 可以基于标签选择器。apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: business-service-policy spec: podSelector: matchLabels: app: business-service policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: api-gateway ports: - protocol: TCP port: 8080这段策略的意思是只有带app: api-gateway标签的 Pod 能访问business-service的 8080 端口其他一律拒绝包括同命名空间里的其他 Pod。这一条就能挡住绝大部分的横向探测。6.4 持续发现把检查变成常态化动作最后一点也是很多团队容易忽略的这个问题修一次不够得能持续发现复发。我见过太多修了又回来的案例原因通常是这几种新版本为了排障临时放开忘了收回新同事照着旧代码抄了一份配置配置中心的改动没有纳入版本管理。应对方式有三个一是把端点探测脚本接入 CI在发布前对新版本跑一遍把结果作为发布卡点。脚本本身不复杂就是第 5 节那段加上结果判定逻辑。二是在依赖层面做管控用软件成分分析工具扫描依赖清单发现引入了带管理界面的组件时自动提醒。Swagger、Druid、H2 这些都属于引入即暴露的类型提前知道比事后排查省事。三是把安全配置纳入代码评审的必查项。我们在团队里定了一条规则任何对management.*、springdoc.*、knife4j.*、spring.h2.console.*这几个前缀的配置修改MR 里必须 到安全接口人。一开始大家觉得麻烦跑了两三个月之后基本没有漏网的了因为这些配置项的修改频率其实很低。7. 一些踩过之后才明白的经验写了这么多最后分享几个我在实际项目里反复验证过、但文档里通常不会提的点。改了 base-path 就安全了是错觉。改路径只能挡住那些只会扫默认路径的自动化工具对有经验的人来说/actuator返回 404 之后试几个常见变体是本能反应。真正的安全来自认证和网络收敛路径改名最多算个缓兵之计而且它还会让你的安全监控规则变复杂。未授权的危害不只是读。很多人评估影响的时候只想到信息泄露但/actuator/loggers能改日志级别、/druid/reset-all.json能清统计、Nacos 能改配置。能写比能读严重得多因为在评估影响的时候能改变系统行为是一个完全不同的量级。修复时要一次收干净不要分批。我见过一个项目先关了 Swagger过了两周再关 Actuator中间那两周里做安全复测的人把两个问题当成同一个报结果修复验证一直不过。同类问题集中修、集中验证省下来的沟通成本远大于一次性改完的工作量。判断严重程度时把是否需要认证和能拿到什么分开看。一个需要登录才能看的 Swagger 页面是低风险一个不需要认证的/actuator/env是高风险的。同样的路径认证状态不同评级可能差两三个档。写报告和做风险评估的时候这两个维度要分开评估不要笼统地说存在信息泄露。最后一点也是我认为最值得强调的不要指望没人知道这个地址。安全性从来不应该建立在路径不公开或端口不常见上面。资产测绘、内网扫描、源码泄露、错误堆栈、前端 JS 里的接口地址都会把服务的真实位置暴露出来。把每个对外可访问的入口都当成随时会被人发现然后在这个前提下设计防护才是靠谱的思路。SpringBoot 的未授权访问问题本质上不是框架的缺陷而是便利的默认值和严格的访问控制之间的取舍没做好。框架给了你一个能快速跑起来的起点但从这个起点到生产环境中间那段路得自己走——该关的关掉、该加的加上、该隔离的隔离开这三件事做好了这类问题基本就能从你的清单上划掉。
返回列表