“内网项目,密码明文放 yml 怎么了?”——一次让我反思的 Code Review

35 阅读8分钟

老炮踩坑录 · FIN-F02 · 翻车现场系列

基于「协作金融项目」真实源码,复盘我在 review 里放行的第二处

关键词:明文密码 · root 直连 · Git 历史 · 环境变量 · 最小权限

👋 欢迎阅读 f25-fm.png 🏠个人主页:知守观
📘我的专栏: 老炮踩坑录
💻当前内容:一次 review 的复盘(续)

引子

上一篇复盘操作员模块的默认密码,结尾预告了这篇。有兄弟留言问:配置文件里的密码也算 review 问题?配置又不过编译。

问得好,这篇文章就来回答这个。配置文件在我的 review 里享受过什么待遇,这个待遇的代价是什么,下面按实际文件讲。

交接那天,我把仓库地址发给接手的兄弟。五分钟后他消息过来:本地跑起来了,一行没改。

我回了个"顺利就好"。

复盘写到这儿才想明白,他能一行不改就跑起来,说明数据库密码跟着仓库走完了全程。从建仓那天,到传进下一个接手人的硬盘。全程没有任何人觉得这里有问题,包括我。

现场:一个 datasource 块,三处问题

application.yml 第 8 到 16 行,原文照抄:

spring:
  application:
    name: xiezuojinrong
  datasource:
    # MySQL 5,端口 3306;如密码不同请修改 password
    url: jdbc:mysql://localhost:3306/xiezuojinrong?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&allowMultiQueries=true&connectionCollation=utf8mb4_general_ci
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

三处问题,一个比一个麻烦。

第一处,password: root,明文。真正的重点在它上面那行注释:"如密码不同请修改 password"。这句话说明密码从来没被人设过,它是脚手架模板自带的默认值,团队里没有一个人为它做过决定。review 的时候我看过这行注释,当时想的是"回头有人会改"。回头这个词在工程里的实际含义,多数时候是永远不会。

第二处,username: root。root 是 MySQL 的超级账号,能建库、删库、授权,能读 mysql 库里全部账号的哈希。应用真正需要的,只是当前这个库的增删改查。拿超级账号跑业务,应用的每一行 SQL、每一个可能出现的注入点,碰到的都是整个实例。

第三处,整个项目只有这一份 application.yml。没有 application-dev.yml,没有 application-prod.yml,没有环境分离。url 里写的是 localhost,照这份配置,应用和数据库得部署在同一台机器上。生产部署时有没有人改过 url 和密码、用什么方式改的,我不知道,交接的时候没人说得清。一份连生产长什么样都没体现的配置文件,就这么一直待在仓库里。

顺带一提,url 里的 useSSL=false 和 allowPublicKeyRetrieval=true,是本地开发"能连上就行"的组合。跟着文件进了仓库,也跟着仓库进了每一台克隆它的机器。host 一旦从 localhost 换成远程 IP,账号口令就在网络上明文传输。

这份密码在 Git 里走多远

clone 即交付。 仓库给到谁,密码给到谁。Git 的每次提交都是永久的,密码从建仓第一天起就在历史里,今天把它改掉,git log -p 翻回去照样能看到。真要清理,得动 git filter-repo 或者 BFG 重写历史,所有协作者重新克隆,这笔代价比加一行环境变量高一个量级。

多语句执行开关。 url 里有一个参数:allowMultiQueries=true,允许一次提交跑多条 SQL。MyBatis-Plus 的常规写法用不上它,我猜当初是为了初始化脚本方便开的,为什么开,我没找到任何记录,这本身就是问题的一部分。这个开关平时无事,哪天项目里出现一个拼接 SQL 的点,注入的破坏上限就从单条查询变成任意多条语句。

SQL 全文进 stdout。 mybatis-plus 配置里,log-impl 指的是 StdOutImpl,把每条 SQL 连同实际参数值直接打到标准输出。用户表要是有密码字段,查询参数一起出去。日志文件的访问权限一般比数据库宽,这段输出给排查省下的时间,和它多送出去的数据放一起算,不划算。

我当时为什么放行:配置文件在 review 里的待遇

操作员模块那段 Java 代码,我至少逐行看过。这份 yml,我当时的处理方式是扫了一眼。

review 的时候,注意力全在 Java 文件上。diff 页面里 yml 的改动就几行缩进,没有编译报错,没有 lint 提示,没有测试失败。配置文件享受不到编译器的任何保护,它在 review 里走的是另一条通道:看一眼,没乱码,过。

《10条军规》里军规八讲过:改 yml 要按改代码处理。轮到自己 review 的时候,我还是扫了一眼。

还有一层原因。'内网项目'三个字,当年在组里能挡住所有安全追问。数据库在内网、系统只在内部用、外面进不来,三句话下来,明文密码、root 直连,全都"先这样"。我复盘过的另一个项目,鉴权拦截器整段被注释掉,理由同样是内网系统。

有个细节我没完全确认:这份 yml 是仓库里的最终状态。部署时有没有人用启动参数覆盖过密码、生产库是不是也是 root,我没有证据。这篇按"仓库即事实"来复盘,如果当年部署侧另有一套,严重程度要打折扣。但"仓库里存在明文的超级账号口令"这件事本身,不因为部署侧兜过底就消失。

改法:按成本从低到高,四步

第一步,换账号,不改代码。

给应用建专用账号,只授业务库的增删改查:

CREATE USER 'zjhz_app'@'%' IDENTIFIED BY '换一个像样的口令';
GRANT SELECT, INSERT, UPDATE, DELETE ON zjhz_jsjn.* TO 'zjhz_app'@'%';
FLUSH PRIVILEGES;

应用出了问题能碰到的范围,从整个实例缩小到一张库的读写。这步在数据库上执行完,yml 里改两行就生效。

第二步,密码离开仓库。

yml 里换成占位符:

spring:
  datasource:
    username: ${DB_USERNAME:zjhz_app}
    password: ${DB_PASSWORD}

${DB_PASSWORD} 不带默认值,环境变量没配,应用启动直接失败。这是故意的:少了配置就该起不来,起来之后连着一个错误口令更糟。本地开发用 application-local.yml 配真实值,这个文件进 .gitignore。

第三步,换密码,再清历史,顺序别反。

先在数据库侧把口令换掉,仓库历史里的旧值立刻作废;然后用 git filter-repo 重写历史:

echo 'password: root==>password: REMOVED' > replacements.txt
git filter-repo --replace-text replacements.txt

历史重写完,所有协作者重新克隆。要是嫌重写历史动静太大,只做换密码也行,密码换了,历史里的旧值就是一串废字符。清历史是锦上添花,换密码才是正事。

第四步,顺手两处。

log-impl 从 StdOutImpl 换成 Slf4jImpl,让 SQL 日志走日志框架、受级别管,生产上直接关:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

allowMultiQueries 没有明确用途就关掉,真有批量脚本要跑,让脚本单独连。

Jasypt 那类加密方案我提一嘴,不推荐上。加密值在仓库里看不见了,可解密密钥总得有人管,管来管去还是同一个问题挪了个位置。环境变量加部署平台的加密变量,是同一件事的不同实现,选顺手的就行。各家平台差异大,这里不展开。

自查清单:五个能直接搜的信号

自查清单.png

自查打卡:打开你们项目的 application.yml,看一眼 password 的值。如果它和三年前一模一样,评论区打个 1,我看看有多少仓库三年没换过口令。

老炮点评

上一篇的账还能一人一半:代码是人写的,我是点通过的人。这一处的账没得分,密码连写的人都没有,它是模板自带的,从头到尾没被任何人碰过。审查链上唯一看过它的活人是我,扫了一眼。

配置文件的问题,代价都在编译器管不到的地方兑现。代码写错,编译器当场说话;配置配错,MySQL 说话,说话的地点是生产环境。

那位接手的兄弟现在应该还留着那个仓库。密码在他硬盘里,也在当年每一个克隆过仓库的人硬盘里。仓库能删,硬盘删不掉。

最后问一句:你们项目的数据库口令,现在还写在配置文件里的扣 1,已经进环境变量或者配置中心的扣 2。我看看哪个多。


相关阅读:

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注 | 💬 留言

我是老炮,18 年 Java 老兵,仍在一线。关注「Java老炮踩坑录」,不错过每一篇真实案例,少踩坑。


下期预告:《密码重置接口的权限校验,校验的是谁的权限》

resetPwd 接口里有一行 if (user != null && ...)。拦截器没生效、user 为 null 的时候,这个条件直接短路,403 不抛,谁的密码都能改。下一篇拆这行条件。