上个月给一个业务系统做安全巡检的时候,发现 Druid 监控页面居然可以直接打开,数据库表结构、SQL 语句、Session 信息就那么摆在页面上。当时第一反应是“这不该出现在生产环境”,第二反应是“如果被外面的人扫到,后果很麻烦”。后来我把这套东西完整梳理了一遍,从配置修复到框架层拦截,再到持续观察,形成了一套可以复用的修复合集。这篇就围绕 Druid 监控漏洞怎么排查、怎么修、怎么验证,把实际操作中的细节和踩过的坑一次性讲清楚。
1. 漏洞到底出在哪:先理解 Druid 监控的设计逻辑
1.1 监控页面的诞生与默认暴露
Druid 是阿里开源的一个数据库连接池组件,在国内 Java 项目里用得非常多。除了连接池管理之外,它内置了一套比较完善的监控体系,可以实时看到 SQL 执行次数、慢查询语句、活跃连接数、Session 明细、URI 请求分布等等。这套监控能力是通过StatViewServlet提供的,默认映射路径是/druid/*。
问题在于,这个 Servlet 的设计初衷是给开发人员和管理员做本地排查用的,很多项目组在集成时直接采用了默认配置。在 Spring Boot 场景下,如果只想开启监控,最省事的写法就是在application.yml里加一段配置,启动后浏览器访问/druid/index.html就能看到完整的监控台。这个页面默认情况下不校验登录态,也不限制访问来源,等于把内部数据库的运行细节直接暴露在网络上。
我在巡检中发现,不少项目甚至把 Druid 监控配置从开发环境直接复制到了生产环境,连密码都没改。更常见的一个现象是:开发同学只知道“配了这个就能看 SQL”,但并不知道这个页面默认是裸奔的。等到安全扫描器报出高危漏洞,才意识到问题的严重性。
1.2 攻击者的视角:一个公开的监控台意味着什么
从安全角度来看,一个未鉴权的 Druid 监控台等于提供了一个近距离观察数据库的窗口。通过监控页面可以看到:
- 完整的 SQL 列表,包括带具体参数值的执行语句,这在业务系统中往往包含用户手机号、身份证号、订单金额等敏感字段;
- 数据库连接信息,包括 JDBC URL、驱动信息、部分连接池配置,这些信息能帮助攻击者判断数据库类型和版本;
- Session 监控里的访问记录,可以了解后台接口的调用频率和参数结构;
- URI 监控里的接口分布,可以快速定位系统核心端点,为后续攻击做路径测绘。
在防御视角下,我强调的是“信息暴露”本身已经构成风险。并不是说看到页面就一定会被拖库,而是攻击者利用这些信息可以大幅降低突破难度。比如拿到了完整的 SQL 语句,就可以分析出表结构和字段含义,再结合常见的注入点进行更精准的尝试。
1.3 影响范围评估:哪些系统最容易受影响
这类漏洞最容易出现在三类系统里:
第一类是内部管理系统,比如后台管理平台、运营工单系统、报表系统,这些系统通常部署在内网但会映射到办公网甚至互联网,访问路径并不像想象中那么封闭;
第二类是快速迭代的业务系统,开发阶段为了方便排查直接开了监控,上线时没有按清单关闭或加固,监控台跟着一起发布到了生产环境;
第三类是使用老版本 Druid 的项目,部分历史版本在鉴权逻辑上存在明显缺陷,即使配置了登录参数也存在被绕过的可能性。
影响范围还取决于部署方式。如果服务直接暴露在公网,或者通过网关反向代理到公网,风险等级就会显著上升。如果只是在隔离内网里运行,风险相对可控,但仍然不应该把监控台当作默认能力开放,安全加固没有“内外有别”的说法,内网同样有横向移动的风险。
2. 修复方案的整体思路:分层防护而不是只加个密码
2.1 为什么单一方案不够
很多人遇到 Druid 监控漏洞时,第一反应是“给监控页面加个密码”。这个方向是对的,但仅仅设置loginUsername和loginPassword并不足以覆盖所有风险场景。原因有几个。
第一,Druid 的登录认证只是前端页面的拦截,它拦截的是浏览器用户,无法阻止通过 HTTP 工具直接调用后端接口获取数据。只要StatViewServlet本身还在路由上,即使没有登录,某些接口调用仍然可能返回数据,具体取决于版本和接口逻辑。
第二,登录密码如果写在配置文件里,随着项目代码打包进制品,或者被环境变量覆盖成弱口令,那么密码形同虚设。生产环境的密码一旦泄露,监控台依然可以被访问。
第三,登录认证只解决“谁能看”的问题,没有解决“谁能访问”的问题。真正的防护应该是多层的:网络层限制来源 IP,应用层校验登录身份,框架层统一拦截路由,数据层关闭不必要的接口能力。每一层解决一个维度的问题,叠加起来才能达到生产环境的安全要求。
2.2 各层方案的能力对比
在制定修复计划之前,我习惯先列一个能力矩阵,把候选方案按照防护效果、实施成本、对业务的影响程度做对比。
| 方案 | 核心作用 | 实施成本 | 绕过难度 | 推荐等级 |
|---|---|---|---|---|
| StatViewServlet 登录认证 | 拦截页面访问,要求登录 | 低 | 中 | 必做 |
| IP 白名单(allow/deny) | 限制访问来源,网络层收缩暴露面 | 低 | 中 | 必做 |
Spring Security/Shiro 拦截/druid/** | 统一鉴权,复用现有权限体系 | 中 | 高 | 强烈推荐 |
| 自定义 Filter 拦截 | 在 Servlet 前增加校验逻辑 | 中 | 高 | 按需 |
| 升级 Druid 版本 | 修复已知鉴权绕过与安全漏洞 | 低 | 中 | 必做 |
| 关闭 reset 功能 | 防止堆栈信息泄露与状态篡改 | 极低 | 低 | 建议 |
| 修改监控路径 | 避免默认路径被扫描器直接命中 | 极低 | 低 | 建议 |
从表格可以看出来,没有任何单一方案能同时覆盖“低成本”和“高防护”,所以实际落地时我都是组合使用。最基本的组合是:登录认证 + IP 白名单 + 版本升级,这三项成本最低、效果最明显。如果系统安全性要求更高,比如涉及金融、医疗、政务类业务,我会额外加上 Spring Security 拦截和路径改造。
2.3 一个合理的修复顺序
修复的时候不要一股脑全改,建议按照影响范围从小到大的顺序操作,每完成一步都做一次快速验证,避免最后出问题不知道是哪一步导致的。
我的习惯顺序是:
- 先升级依赖,把 Druid 换到当前稳定版本,这一步能消除大量已知漏洞;
- 再改配置文件,开启登录认证和 IP 白名单,这一步可以立即阻断页面访问;
- 接着加框架层拦截,把
/druid/**纳入统一鉴权管理; - 然后调整监控行为,关闭 reset,按需关闭不必要的监控项;
- 最后改造访问路径,让默认的
/druid/*不再可用。
每一步做完都访问一次/druid/index.html验证是否符合预期。这样做的好处是可以快速定位问题,比如升级之后出现登录页样式丢失,那就是版本兼容问题,跟后续配置无关,排障范围非常清晰。
3. 实操过程:一步步把监控入口锁起来
3.1 基础配置实战:给 StatViewServlet 加上登录态和来源限制
先讲最基础也是最重要的配置。在 Spring Boot 项目中,如果使用了spring-boot-starter-druid或者直接引入 Druid 依赖,最推荐的方式是在application.yml里配置监控 Servlet 的参数。
spring: datasource: druid: # 开启监控页面 stat-view-servlet: enabled: true # 访问路径,默认是 /druid/* # 建议改成难以猜测的路径,比如 /druid-2024/internal url-pattern: /druid/* # 登录用户名 login-username: monitor_admin # 登录密码,不要用弱口令 # 推荐通过环境变量注入,打包时不要明文出现在配置文件 login-password: ${DRUID_MONITOR_PASSWORD} # 允许访问的IP,多个用逗号分隔 # 只允许本地和特定网段访问 allow: 127.0.0.1,192.168.10.0/24 # 拒绝访问的IP,优先级高于allow deny: 10.0.0.88 # 关闭重置功能,防止在线重置统计信息 reset-enable: false这里有几个关键点要特别说明。
allow和deny的匹配逻辑是:如果同时配置了allow和deny,则优先匹配deny,命中deny的 IP 直接拒绝;没有命中deny的请求再看是否命中allow,只有命中allow的请求才允许继续访问,其他 IP 一律拒绝。所以不能把allow配置得过于宽松,比如配置成0.0.0.0等于没有限制。
login-password强烈建议不要直接写在配置文件里。虽然 Spring Boot 支持@ConfigurationProperties读取环境变量,但很多人图省事直接把明文密码写死在 yml 里,结果代码仓库一旦泄露,监控台就形同裸奔。我在公司内部推过一个规范:所有安全相关配置一律使用环境变量注入,本地开发通过.env文件管理,不同环境设置不同密码。
如果项目没有使用 Spring Boot 的自动配置,而是通过传统的ServletRegistrationBean手动注册,配置方式也大同小异:
@Configuration public class DruidMonitorConfig { @Bean public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() { ServletRegistrationBean<StatViewServlet> registration = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); // 添加初始化参数 registration.addInitParameter("loginUsername", "monitor_admin"); registration.addInitParameter("loginPassword", System.getenv("DRUID_MONITOR_PASSWORD")); registration.addInitParameter("allow", "127.0.0.1,192.168.10.0/24"); registration.addInitParameter("deny", "10.0.0.88"); registration.addInitParameter("resetEnable", "false"); return registration; } }手动注册时最容易犯的错是忘记设置loginUsername和loginPassword,只设置了allow。这种情况下,来自白名单 IP 的访问不需要登录就能直接看到监控台,注意allow和登录认证是两个独立的保护维度,必须同时配置。
3.2 加一层框架拦截:Spring Security 统一鉴权
如果项目里已经有 Spring Security,除了在 Druid 层配置登录认证之外,应该让/druid/**也走一遍框架的统一鉴权。这样即使 Druid 自身的登录逻辑被绕过,框架层的拦截仍然能挡在前面。
一个比较常见的做法是给/druid/**配置基于角色的访问控制:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 监控页面只允许指定角色访问 .requestMatchers("/druid/**").hasRole("MONITOR") .anyRequest().authenticated() ) .formLogin(login -> login .loginPage("/login") .permitAll() ) .logout(LogoutConfigurer::permitAll) .csrf(csrf -> csrf.disable()); return http.build(); }这段配置的含义是:只有拥有MONITOR角色的用户才能访问/druid/**,其他请求也必须经过认证。把监控页面纳入框架鉴权后,就实现了“双保险”——先过框架身份认证,再过 Druid 自身的登录凭证校验。
需要注意一个顺序问题。Spring Security 的过滤器链执行顺序在 Servlet 之前,所以框架层的拦截会先于StatViewServlet生效。这意味着攻击者首先要绕过 Spring Security,之后还要面对 Druid 自身的登录认证。如果只是期待其中一个生效,安全强度是不够的。
有些人会担心加 Spring Security 影响性能,实际监控页面的访问频率很低,增加的鉴权开销基本可以忽略。如果说项目之前没有引入 Spring Security,为了一个监控页面引入整套框架显得有点重,可以换一个自定义 Filter 的方案。
3.3 轻量方案:自定义 Filter 拦截监控路由
有些老项目用的是 Spring MVC + 自定义权限体系,没有 Spring Security,这时候可以直接写一个Filter,专门处理/druid/*路径的访问校验。
@Component @Order(1) public class DruidAccessFilter implements Filter { private static final Set<String> ALLOWED_IPS = Set.of("127.0.0.1", "192.168.10.0/24"); private static final String TOKEN = "monitor_session_token"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String uri = httpRequest.getRequestURI(); if (!uri.startsWith("/druid/")) { chain.doFilter(request, response); return; } // 1. 校验来源 IP String remoteAddr = getRemoteAddr(httpRequest); if (!isAllowedIp(remoteAddr)) { httpResponse.setStatus(403); return; } // 2. 校验登录凭证 HttpSession session = httpRequest.getSession(false); if (session == null || !TOKEN.equals(session.getAttribute(TOKEN))) { httpResponse.sendRedirect("/druid/login.html"); return; } chain.doFilter(request, response); } private boolean isAllowedIp(String ip) { // 这里做一个简易的 IP 段匹配,生产环境建议引入成熟的 IP 工具类 return "127.0.0.1".equals(ip) || ip.startsWith("192.168.10."); } private String getRemoteAddr(HttpServletRequest request) { // 注意:如果经过了反向代理,需要从 X-Forwarded-For 头取值 // 但这里存在伪造风险,建议在最前置的网关层把真实 IP 写入自定义头 String forwarded = request.getHeader("X-Forwarded-For"); if (forwarded != null && !forwarded.isEmpty()) { return forwarded.split(",")[0].trim(); } return request.getRemoteAddr(); } }自定义 Filter 的优点是灵活,不受框架限制,逻辑完全可控。但它有两个隐患需要提前意识到。
一个是 Filter 的注册顺序。如果项目的其他 Filter 没有设置@Order或者顺序比自己靠前,可能会出现请求还没经过自己校验就被转发到 Druid Servlet 的情况,所以务必把@Order(1)加上,确保这是最早执行的过滤器。
另一个是反向代理场景下的 IP 获取。如果服务前面还挂了一层 Nginx 或其他网关,request.getRemoteAddr()拿到的是网关的 IP,而不是真实客户端的 IP。如果不加处理,所有请求都会命中白名单,拦截形同虚设。我的建议是优先使用X-Forwarded-For头,同时在网关层清理这个头的传入值,防止用户伪造。
3.4 升级依赖:不要忽视版本本身的问题
Druid 监控漏洞之所以反复出现,很多时候是因为项目还在使用几年前的版本,而这些版本已经被发现存在鉴权绕过或资源控制方面的问题。修复的第一步,就是把你手里的依赖升到当前官方推荐的稳定版本。
以 Maven 项目为例,检查一下pom.xml中的依赖坐标:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>如果发现版本号停留在 1.1.x,就说明依赖已经落后了很多。升级的时候不要只改版本号什么都不管,Druid 的配置项在不同版本之间有调整,升级后先跑一遍应用自测,重点看监控页面是否正常展示、登录是否生效、连接池各项指标是否正常。
如果项目使用的是 Spring Boot 的 Starter,还需要确认druid-spring-boot-starter和 Druid 的核心版本之间的兼容性。我遇到过升级核心版本后 Starter 没有同步更新,导致某些配置项失效,监控页面的登录认证形同虚设。所以建议保持两个依赖版本的一致性,或者直接使用官方提供的 BOM 管理。
还有一点:升级后要重新检查一遍配置。有些老项目的配置写法在新版本中会被忽略,比如resetEnable配置项如果大小写写错,新版可能直接不识别,这条防线就没了。升级期间顺手把配置规范化,比等出问题再排查要省事得多。
3.5 最小暴露原则:能关的监控能力就别开着
修复漏洞的终极目标不是“能登录才访问”,而是“尽可能减少攻击面”。从这个角度出发,除了必开的监控项之外,其他能力能关就关。
先说reset-enable。这个配置项控制监控页面上是否显示“重置”按钮。如果开启,登录者可以一键清空所有统计信息,虽然看起来不是安全隐患,但在排障时会造成数据丢失,也容易被误操作。生产环境建议设为false。
再看StatViewServlet本身。如果你的业务对监控数据没有强需求,最安全的方式是直接把stat-view-servlet.enabled设为false,甚至不注册这个 Servlet,彻底关闭监控页面。很多生产系统运行几年根本没人看过监控台,这种系统保留一个长期暴露的入口毫无意义。
spring: datasource: druid: stat-view-servlet: enabled: false另外,Druid 还有 WebStatFilter,用于监控 Web 请求。这个 Filter 默认拦截所有请求,并会把请求路径记录到监控数据里,对于需要保护完整 URL 的业务来说,也会造成路径信息泄露。如果不需要 Web 层监控数据,可以一并关掉:
spring: datasource: druid: web-stat-filter: enabled: false要明白一个原则:监控能力属于“锦上添花”,业务核心功能完全不需要依赖它运行。安全加固时优先考虑的是取下不必要暴露的服务入口,而不是给每个入口都加一层保护。入口越少,需要维护的防线越少,出问题的概率自然越低。
3.6 路径改造:让默认路由失效
即使加了登录认证和白名单,攻击者仍然可以通过扫描/druid/index.html确认系统使用了 Druid,并尝试已知的绕过手法。一个简单的防御措施是把监控路径改掉,让扫描器扑空。
Spring Boot 的配置可以这样改:
spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid-monitor-2024/*改完之后,原来的/druid/*路径不再响应,新页面只能通过/druid-monitor-2024/index.html访问。这个路径不是机密信息,但能有效降低被自动化工具直接命中的概率。
路径改造要跟框架层拦截同步调整。如果你用 Spring Security 拦截/druid/**,改路径后要同步改成/druid-monitor-2024/**,否则框架层的保护就不覆盖新路径了。自定义 Filter 同理,URI 前缀也要改。
一个常见的误区是“路径改成随机字符串就绝对安全了”,这种想法很危险,它只是提高了一点扫描成本,不等于安全。路径改造只能作为辅助手段,绝不能替代登录认证和权限校验。
4. 常见问题与排查技巧实录
4.1 配置了 loginUsername 还是能匿名访问?
这个问题在实操中出现频率非常高,通常是三个原因造成的。
一是配置没有生效。StatViewServlet的参数是在初始化阶段读取的,如果你的注册方式不是通过 Spring Boot 自动配置,而是自己写的ServletRegistrationBean,要确认参数确实传进去了。可以在启动日志里搜一下包含 druid 的日志,或者直接访问监控台观察是否出现了登录入口。
二是版本问题。部分老版本 Druid 在特定环境下确实存在登录校验失效的情况,即使配置了loginUsername和loginPassword,页面依然直接展示监控数据。这种问题靠改配置是修不好的,必须升级版本。
三是路径大小写导致框架拦截失效。HTTP 路径在 Spring Security 默认是区分大小写的,如果请求的是/Druid/index.html,而拦截规则配置的是/druid/**,这个请求可能绕过框架层的校验。但 Druid 的 Servlet 映射在部分容器里又不区分大小写,于是出现了“能绕过拦截但又能访问业务”的情况。修复方案是在框架拦截规则里同时配置大小写两种路径,或者使用AntPathRequestMatcher时设置忽略大小写。
4.2 登录后 JS/CSS 加载失败,样式全丢了
改完监控路径后最容易遇到这个问题。页面能打开,但样式全乱,查看浏览器控制台发现/druid-monitor-2024/js/common.js返回 404。
原因一般是 Spring Security 拦截规则把静态资源也拦截了,又没有对/druid/**/js/**、**/css/**这类路径放行。Druid 监控页面自身引用的静态资源路径是相对/druid/前缀的,你把前缀改了之后,如果框架只放行了/druid-monitor-2024/*而没放行/druid-monitor-2024/js/*,资源就加载不出来。
解决方法是调整拦截规则,把监控资源路径显式放行。但注意不要直接把整个/druid/**无差别放行,而是先鉴权再放行,资源本身不敏感,真正需要保护的是数据接口。一个折中的策略是:对/druid/**/*.js、*.css直接 permitAll,对/druid/**/*.html和数据接口做鉴权。
还有一种情况是自定义 Filter 里重定向到了/druid/login.html,但实际路径已经改成了别的,导致重定向目标 404。改路径时务必把所有硬编码的/druid/前缀同步替换掉。
4.3 攻击探测和扫描器告警
开启监控修复后,仍然会在日志里看到大量针对/druid/index.html的探测请求。这属于正常现象,说明修复前已经有扫描器或者被别人侦察到了,修复后它们还在不断尝试,不用过度紧张,但也不要忽略告警。
我的处理习惯是:确认监控台已经无法未授权访问,同时把访问/druid/路径的探测行为记入单独的审计日志,方便后续追踪。如果入口在 Nginx 层,可以直接在 Nginx 配置里对/druid路径做语义拦截,比如非内网 IP 一律 403,这样压力到不了应用层。
还有一点要提醒:攻击者可能直接请求 Druid 监控后端的 JSON 数据接口,比如/druid/basic.json、/druid/sql.json等。这些接口返回的是监控数据的 JSON 格式,即使没有前端页面也可能被直接调用。在验证修复效果时,不能只看/druid/index.html,还要逐个检查这些数据接口是否也被保护住了。
4.4 验证清单:怎么判断自己修好了
修复完成不等于工作结束,还要做一轮完整的自我验证。下面是我每次加固完成后都会跑一遍的检查清单。
| 检查项 | 预期结果 | 验证方法 |
|---|---|---|
未登录访问/druid/index.html | 跳转登录页或返回 403 | 用无痕浏览器打开 |
未登录访问/druid/basic.json | 返回 403 或登录错误 | curl 直接请求 |
| 白名单外 IP 访问 | 被拒绝 | 用手机5G网络访问 |
| 登录后访问监控台 | 正常显示数据 | 输入账号密码登录 |
| 重置按钮 | 不存在或不可点击 | 登录后查看页面 |
| 非监控路径访问 | 业务功能不受影响 | 回归测试核心接口 |
请求/druid/login.html后的登录跳转 | 登录成功后正常回跳 | 使用正确密码登录 |
这里特别提醒basic.json和sql.json这类数据接口的检查。很多时候前端页面已经被拦住了,但后端数据接口还是裸的,这种隐蔽的漏洞更容易被利用。我建议在验证时直接用 curl 模拟未登录请求,不要只通过浏览器看效果。
4.5 生产环境持续观察的建议
修复完不等于一劳永逸,Druid 作为开源组件会持续更新,安全风险也是一个动态变化的过程。我个人在项目中的做法是每季度检查一次依赖版本,关注官方仓库的安全公告,如果出新版本就评估升级。
另外建议在生产环境保留 Docker 或其他容器化工具的镜像版本标签,把 Druid 依赖版本固化下来,方便同一套镜像在多个环境保持一致。不同环境的监控密码也要通过配置中心分发,保证不出现“测试环境口令+生产环境入口”这种组合问题。
5. 再聊几句个人体会
Druid 监控漏洞算是国内 Java 生态里一个比较典型的“便利与安全失衡”的案例。开发阶段想快速看问题,加几行配置就能看到 SQL 和连接池数据,这确实方便。但到了生产环境,这种便利如果没同步收紧,就会变成最容易被攻击者盯上的入口之一。修这么一个漏洞的技术难度不大,真正难的是养成“上线前先检查暴露面”的习惯。
我现在要求团队在每次版本发布前都执行一条命令,扫描一下依赖清单,再检查配置里的敏感项。虽然会花几分钟时间,但比起被安全部门通报或者被扫描器扫出漏洞再紧急处理,这几分钟绝对值得。希望这篇里写的配置、方案和踩坑记录能帮你少走一些弯路。如果你正在处理类似问题,可以先对照检查清单做一次全面体检,再按上面的步骤逐步加固,遇到具体问题随时回来翻翻这一篇。