☰
Java 技术选型三条铁律:Fastjson CVE、重复依赖、外部进程踩坑实录(附自查清单)
2026/10/13 7:05:14 网站建设 项目流程

老炮踩坑录 · V06 · 老炮视野系列
基于「企业融合评估平台」真实源码,复盘那些选的时候都说没问题、上线一年后才爆雷的技术选型决定
关键词:Fastjson 反序列化 · 重复依赖 · 外部进程 · 命令注入 · 版本安全

👋 欢迎阅读

🏠个人主页:知守观
📘我的专栏:老炮踩坑录
💻当前内容:技术选型

文章目录

    • 前言
    • 铁律一:带 CVE 的版本不能用——版本号是安全声明
    • 铁律二:同一种能力只留一个入口——重复选型就是给未来埋冲突
    • 铁律三:外部进程别扛核心链路——黑盒出问题你只能干瞪眼
    • 自查清单
    • 老炮点评

前言

上一篇十条军规发完,有个朋友私信我:你说的这些都是写代码的规矩,那选型呢?框架、中间件、工具,引进来的时候大伙都觉得没问题,问题往往一年后才冒出来。

这个话题我有发言权。

离职以后我把这个项目的pom.xml拉出来数了一遍依赖,越数越后背发凉。光 JSON 序列化就引了三套,分页框架引了两套,PDF 生成同时用着 iText 和一个外部命令行工具。

今天把我在这个项目里踩到的选型坑整理成三条铁律,每条都带有真实代码说明。

铁律一:带 CVE 的版本不能用——版本号是安全声明

打开pom.xml,第一行就写着 Spring Boot2.1.0.RELEASE。这个版本 2018 年底发布,2020 年就停止维护了。我写本篇的时候,Spring Boot 已经出到 4.1.1了。

2.1.0 本身倒没直接出过能一招利用的漏洞,但它拉进来的依赖有。

最典型的是Fastjson 1.2.37。

<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.37</version></dependency>

Fastjson 1.2.37 发布于 2017 年,autoType 反序列化漏洞在当时被爆了一轮又一轮。官方修到 1.2.83 才算把这条缝彻底堵上,中间隔了快五年。

这个项目里 Fastjson 用得有多狠?我搜了一下:

grep -rn "fastjson\|JSONObject\|JSONArray" src/main/java | wc -l → 925 处,分布在 83 个文件里

大量调用长这样:

// LoginServiceImpl.java:178JSONObjectjsonObject=JSON.parseObject(resultMap.get("result").toString());

parseObject只传了一个字符串参数,没指定目标类型。这就是 autoType 的入口——Fastjson 会根据 JSON 里的@type字段自动实例化任意类。

我不确定这个接口对外暴露时有没有被实际打过——离职后自然没权限看线上日志。但代码层面这个洞是实打实在的,扫漏洞的工具一跑一个准。

同一份pom.xml里还有几个老朋友:

依赖版本发布年份问题
fastjson1.2.372017autoType RCE,官方修到 1.2.83
poi / poi-ooxml3.122014多个 XXE、拒绝服务漏洞
commons-fileupload1.32013CVE-2016-1000031 反序列化 RCE
guava20.02016多个已修复漏洞
spring-boot2.1.02018已 EOL,不再发安全补丁

这些不是"可能有问题",是 CVE 库里查得到编号的公开漏洞。攻击者不需要零日,拿现成的 EXP 就能打。

我后来的规矩很简单:引依赖前先查这个版本有没有公开 CVE。查的渠道就两个——NVD 官网搜组件名加版本号,或者看依赖的官方 changelog 里 Security 章节。五分钟的事,省掉的是上线后被打穿的代价。

如果项目里已经有了,升级。Fastjson 升到 1.2.83 以上,或者直接换 Jackson(Spring Boot 默认就带,不用额外引)。POI 升到 4.x 或 5.x。commons-fileupload 1.3 这种,要么升要么用 Spring 的MultipartFile别直接碰底层。

版本号写在pom.xml里不是装饰,它是你对安全态势的一个声明。

这个项目的 Fastjson 问题,我在《FastJSON 1.2.37 的 autoType》里专门写过,这里就不展开了。

铁律二:同一种能力只留一个入口——重复选型就是给未来埋冲突

这个项目的依赖里有个很有意思的现象:同一件事,引了不止一个库。

先说分页。pom.xml里同时有 MyBatis-Plus 和 PageHelper:

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.0</version></dependency><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.2.5</version></dependency>

两套都在干活。我搜了一下:

  • 用PageHelper.startPage/PageInfo的:15 个文件
  • 用 MyBatis-Plus 的IPage/selectPage的:20 个 Mapper

同一个列表查询,有人这么写:

// ApplyInfoServiceImpl.java —— PageHelper 写法PageHelper.startPage(pageNum,pageSize);List<ApplyInfo>list=applyInfoMapper.selectList(queryWrapper);PageInfo<ApplyInfo>pageInfo=newPageInfo<>(list);

有人那么写:

// ApplyInfoMapper.java —— MyBatis-Plus 写法IPage<ApplyInfo>page=newPage<>(pageNum,pageSize);IPage<ApplyInfo>result=applyInfoMapper.selectPage(page,queryWrapper);

两套分页的参数、返回类型、count 查询行为都不一样。新人接需求,先看老代码,看到两种写法,不知道该跟哪个。跟着跟着,一个接口里就混着用了——PageHelper.startPage设了分页参数,下一行调的 MyBatis-Plus 的selectPage,分页参数互相覆盖,算出来的页数怎么都不对。

再说 JSON。这个项目里同时存在三套 JSON 库:

  1. Jackson——Spring Boot 默认带的,HTTP 消息转换用它
  2. Fastjson 1.2.37——业务代码里 925 处调用的主力
  3. json-lib 2.2.3(net.sf.json-lib)——还有人在用JSONObject.fromObject

三套库处理 null、日期、字段名的策略都不一样。同一个对象,用 Fastjson 序列化出来date是时间戳,用 Jackson 配了date-format就是字符串。前端拿到数据,一会儿是数字一会儿是字符串,报了 bug 排查半天,查下来是两个接口用了不同的序列化库。

PDF 生成也一样。pom.xml里引了 iText 5,实际代码里跑的是 wkhtmltopdf 外部进程(下一条细说)。iText 的依赖从头到尾没被 import 过,纯属占着位置。

还有commons-lang2.6 和commons-lang3同时在pom.xml里——commons-lang早就停更了,API 和 lang3 还不兼容,引着它就是给ClassNotFoundException留种子。

重复选型的代价不是多占几十兆磁盘,是认知负担和隐性冲突。

我现在的做法:引新依赖之前,先搜项目里有没有同类库。已经有 Jackson 了就别再加 Fastjson,已经有 MyBatis-Plus 了就别再加 PageHelper。真要换,就统一换,把旧的删干净,别让两套并存。

如果是接手老项目,第一件事就是把依赖清单拉出来,把同类的合并。删掉一个不用的依赖,比加十个新依赖都值钱——少一个库,就少一份漏洞面,少一个出 bug 的变量。

重复选型的问题,和《代码腐化的五个信号》里的 “重复方法”是同一个病根。

铁律三:外部进程别扛核心链路——黑盒出问题你只能干瞪眼

这个项目的 PDF 导出,用的是wkhtmltopdf。

wkhtmltopdf 本身没问题,把 HTML 渲染成 PDF 的老牌工具。问题在于它是个外部命令行程序,Java 代码通过Runtime.exec去调它。

来看HtmlToPdf.java的核心逻辑:

publicstaticbooleanconvert(StringsrcPath,StringdestPath){StringBuildercmd=newStringBuilder();cmd.append(toPdfTool);cmd.append(" --header-line --disable-javascript ");cmd.append(srcPath);// 用户相关的文件路径直接拼进命令cmd.append(" ");cmd.append(destPath);// 输出路径也直接拼Processproc=Runtime.getRuntime().exec(cmd.toString());// ...proc.waitFor();// 没有超时,卡死就一直等}

这段代码有四个坑,每一个都能在线上要你的命。

坑一:命令拼接。srcPath和destPath直接用字符串拼进命令行。如果这两个值里有空格、分号、反引号,Shell 会把它们当成分隔符或命令。Linux 下destPath要是带个; rm -rf /,后果自己想。正确的写法是用ProcessBuilder传字符串数组,每个参数是独立元素,不经过 Shell 解析:

ProcessBuilderpb=newProcessBuilder(toPdfTool,"--header-line","--disable-javascript",srcPath,destPath);Processproc=pb.start();

坑二:waitFor()没有超时。wkhtmltopdf 渲染一个复杂 HTML 可能要几秒,如果它内部死锁或者某个资源加载卡住,waitFor()会一直阻塞,占着一个 Tomcat 线程。请求一多时,线程池被占满,整个服务就假死了。应该用waitFor(timeout, unit),超时后destroyForcibly():

if(!proc.waitFor(30,TimeUnit.SECONDS)){proc.destroyForcibly();thrownewTimeoutException("PDF 生成超时");}

坑三:环境耦合。工具路径写死在配置里,Windows 一套、Linux 一套:

wkhtmltopdf:pdftool:windows:F:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exelinux:/usr/local/bin/wkhtmltopdf

换台服务器,路径变了,没人更新配置,PDF 导出直接报"系统找不到指定的文件"。运维凌晨被叫醒,查半小时结果发现是路径不对。

坑四:出问题没法在 Java 层调试。wkhtmltopdf 跑在独立进程里,它内部渲染失败、字体缺失、网络超时,Java 这边只能拿到一个退出码和 stderr 文本。日志里看到Exit code: 1,不知道到底是 HTML 有问题还是字体没装还是磁盘满了。排查的时候得登录服务器手动跑命令复现,脱离了 Java 的调试体系。

我不是说外部进程不能用。批处理、定时任务、边缘功能,调一下外部工具没问题。但核心链路上的能力,别押在外部进程上。这个项目的 PDF 导出是企业申报的核心环节——用户填完资料导出评估报告,这一步挂了,整个业务流程就断了。

如果让我重做这个选型,我会优先用 JVM 内的方案:OpenPDF(iText 的开源分支,LGPL 协议)或者 Apache PDFBox。渲染效果可能不如 wkhtmltopdf 还原 HTML,但部署可控、调试方便、没有命令注入风险。实在需要 HTML 转 PDF,也可以考虑飞书的文档导出或者浏览器无头模式,但那是另一回事了。

外部进程是黑盒。黑盒可以用,别让它扛核心链路。

这个 PDF 工具的安全问题,我在《HTML 转 PDF 三种方案对比》里详细拆过,感兴趣的可以翻一下看看。

自查清单

把pom.xml打开,对照这张表过一遍:

检查项怎么查危险信号
依赖有没有公开 CVENVD 搜组件名+版本号,或看官方 changelog版本发布超过两年没升级 = 大概率有未修复漏洞
同一能力是否引了多套搜pom.xml里同类依赖(JSON、分页、PDF、Excel)两套以上并存 = 未来会出隐性冲突
有没有引了没用的依赖搜代码里有没有 import 对应包零引用 = 白占漏洞面,该删
核心链路有没有外部进程搜Runtime.exec/ProcessBuilder外部进程处理核心业务 = 出问题难排查
外部进程调用有没有超时和参数化看waitFor有没有传 timeout,命令是否用数组传exec(String)+waitFor()无参 = 命令注入+卡死风险

老炮点评

三条铁律再压缩,其实就一句话:技术选型不是选最酷的,是选最不容易出事的。

选 Fastjson 因为它快、API 顺手,结果留了个 RCE 的洞。选 PageHelper 因为写法简单,结果和 MyBatis-Plus 打起架来。选 wkhtmltopdf 因为 HTML 还原度高,结果线上出问题只能登录服务器敲命令。

这些选择在做决定的那一刻都有道理。技术选型的难就难在——你要为那个道理付多久的代价。

一个库引进来,不是引进来就完了。它要跟着你三年五年,要升级、要补漏洞、要教会每个新人怎么用。选型时多花十分钟查 CVE、多花十分钟搜项目里有没有同类库、多花十分钟想清楚核心链路能不能扛外部依赖,上线后省的是十个通宵。

我自己现在引依赖有个笨办法:写在一张便签上,问自己三个问题——这个版本有没有洞?项目里有没有同类的?它挂了我的核心链路会不会断?三个问题都答得让自己放心,再往pom.xml里写。


延伸阅读
上一篇 《做了18年开发,我总结的十条军规》 讲的是写代码层面的规矩,这篇讲选型层面的。两篇对着看,一个谈"写法",一个谈 “选型” 。


下期预告:《从单体到微服务:这个项目该不该拆?》
前面讲的都是“怎么改”,下期换个视角:这个项目到底该不该拆成微服务?先说结论——不该;再说什么时候该。

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

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询