坦白讲,做完这次年度依赖健康检查,我对着扫描报告愣了好一会儿。项目组重构完的一套核心业务系统,构建产物总大小接近 300 MB,统计下来第三方 Jar 包编译出的字节码占比 71% 左右——剩下的才是我们自己的代码。我顺着软件物料清单一项项看过去,发现这份“依赖地图”里,有一大半组件是我只在文档里见过名字、从没打开过源码的。
我不打算贩卖焦虑,但必须承认:供应链攻击已经不是安全团队 PPT 里的概念,而是 2026 年 Java 项目实打实要面对的高频威胁。攻击者从“挨个打网站”转向“污染一个被大量项目引用的组件”,相当于在整片供水管道的上游下药,效果比一家家投毒高效太多。下面这些内容,是我这段时间做依赖治理、拆解攻击链路时沉淀下来的东西,希望能帮团队把“能跑就行”的 Java 项目,变成“经得起审”的 Java 项目。
1. 流转到你仓库里的那71%:Jar依赖的构成真相
1.1 一个普通核心业务系统,到底依赖了多少东西
我先带你看看一个常规 Java 微服务项目的依赖构成。用当前主流的框架体系去新建一个 Web 服务,脚手架默认会带上一批直接依赖:Web 容器、数据访问层、序列化组件、多线程工具、监控探针、配置中心客户端、链路跟踪 SDK、日志门面、数据库连接池……这批“直接依赖”往往就有三四十个。
真正让人头疼的是传递依赖。你写一行配置引入某个组件,它的构建描述文件里又带着一堆子依赖,那些子依赖各自再引用别的库,层层展开之后,依赖树的总节点数通常是直接依赖的 5 到 10 倍。我拿一个日常业务系统举例,它在 pom 里只写了二十几个显式依赖,用依赖分析插件一展开,实际参与编译和打包的组件数量超过 400 个。等到用dependency:tree把整棵树铺到终端里,翻了七八屏都没到底。
这就是 71% 这个数字的来源基础:现代 Java 框架把大量控制逻辑藏在依赖里,自带业务代码往往只是最薄的那一层。就算团队刻意精简,一个带缓存、带消息队列、带接口文档的项目,第三方组件的数量也轻松奔着三四百去。每个组件,都意味着一段由外部维护、发布节奏不受你控制的代码。
1.2 71% 是怎么统计出来的,以及它真正的含义
有人会问:71% 是不是标题党?实际上这个比例有相对明确的统计口径。一般流程是:把最终构建产物(可执行 Jar 或镜像里的 class 文件集)解压,按类文件的字节数或代码行数统计——自带 class 和第三方 class 分别算账。现代框架的 jar 往往体积不小,第三方字节码超过七成是很常见的结果,有些重度依赖框架全家桶的系统,这个比例能逼近 85%。
但我要强调,占比高不等于必然危险。两个比例同为 70% 的系统,一个把依赖固定到精确版本、只从内网制品库拉取、每次升级都过漏洞扫描和回归测试;另一个放任构建工具每天自动检查快照更新、版本号写成带通配符的区间、直接从公网仓库解析——两者的风险敞口差着数量级。所以,真正该关注的不是“71% 这个数高不高”,而是这 71% 的背后,你有没有一套能说清楚“从哪来、是谁、到底干了什么”的机制。
1.3 为什么说这 71% 的代码“不受你控制”
“不受控制”不是指代码质量差,而是指你在几个关键维度上对依赖缺乏掌握。首先是来源不受控:你无法仅凭“坐标长得对”来确认拉下来的 Jar 就是你想要的那份。公网仓库一旦发生坐标混淆、版本被篡改或上游维护者账号出现异常,构建时拉到的东西可能已经完全走样。其次是内容不受控:组件每发布一个新版本,里面除了正常更新还可能夹带隐私收集逻辑、后门或挖矿脚本,代码层面不易察觉,只有运行时行为会暴露端倪。
生命周期不受控同样要命。依赖的上游维护者可能停更、删库、改许可证,或者因为精力不足把仓库交接给不熟悉安全的人。你项目的安全边界就这样被动地系在别人身上。更现实的是修复节奏不受控:某个深埋在传递链第三层的组件被曝出高危漏洞,你想修,往往不是升级那一个小包就能完事,而是要连带升级一个更大的框架、调整一堆接口和兼容配置。这段“修复滞后窗口期”,恰恰就是攻击者最爱的机会。
2. 黑客盯上Jar包的三种攻击路径与一个典型链路
2.1 依赖混淆:一次“同名高版本”的钓鱼
依赖混淆是我拆过最多、也最容易复现的攻击路径。它的核心逻辑很简单:企业内部有一个坐标(groupId:artifactId)只存在于内网制品库,比如“某某内部日志增强包”,版本 1.0.0;攻击者跑到公共仓库,注册同样的 groupId 和 artifactId,发布一个更高的版本号,比如 99.0.0,并在里面塞入恶意逻辑。
构建工具解析依赖时,如果你配置了公共仓库优先于内网仓库,或者构建脚本里写的是“最新版本优先”,构建机器会先去公共仓库拉取 99.0.0,而这个版本带着后门。一旦被拉取并打包进产物,恶意代码就顺着发布流程进了生产环境。我处理过一起外部项目事故,根因就是某位开发把快照仓库直接指到了公共仓库,又开着自动更新选项,结果第二天构建出来的版本变成了公网恶意包。
防御依赖混淆不需要高深技巧:构建配置里把内网制品库设为唯一解析源或最高优先级,关掉快照组件的自动更新,对外部仓库保持“白名单”心态——不是默认信任,而是逐个确认。对发布流程里的每个坐标,最好都做哈希锁定。
2.2 供应链投毒:在正常更新的壳里藏东西
比依赖混淆更隐蔽的是供应链投毒。攻击者不需要伪造坐标,而是混进上游维护链路。常见的三种手法让我拆给你看:第一种是伪装成社区贡献者,向一个小项目提交“修 bug”的 PR,PR 里夹带一段不易察觉的恶意逻辑——比如用异常处理掩盖一个外部请求,表面上是完善边界条件,实际在偷传数据。第二种是注册一个与知名库只差一个字母的坐标,等着依赖拼写错误或自动补全时中招。第三种更阴险:攻击者先主动维护一个正常的开源库,养几个月口碑,然后在某个小版本里把恶意行为推迟到特定时间或特定环境才触发,绕过前期的漏洞扫描和人工抽查。
Java 生态对这类投毒有放大效应。因为 Jar 一旦被拉取并缓存到本地或内网制品库,即使上游后来删除了那个恶意版本,缓存里的副本仍可能继续被构建和发布。拦截这类攻击的关键,是把“新依赖引入”当成代码评审的一部分,而不是只依赖扫描器放行。
2.3 攻击构建管道:比攻击应用本身更高效
第三种路径最让我警惕,攻击者的目标不是你的业务系统,而是构建或发布环节。理由很直接:只要渗透进 CI,攻击者就不用关心你到底用了哪些依赖,它可以改动产物本身,像“在成品出厂前掉包”。
构建管道易被下手的点包括:CI 机器上被植入后门的构建插件、构建脚本里被篡改的依赖解析流程、制品仓库管理后台泄露的凭证,以及开发者本地信任链的问题——比如不验签地执行来自依赖库中的命令。这类攻击难发现,因为“整个过程看起来一切正常”,构建日志里都是绿色勾,产物在流水线上被悄然替换。为了抵御这种路径,必须把构建环境当作生产环境来保护:CI 节点最小权限、不暴露生产密钥、制品签名并校验、构建日志留存审计。
2.4 一条典型恶意 Jar 攻击链路的完整拼图
把上面三种路径串起来,一个典型攻击链路是这样的:攻击者先在公共仓库投放一个名字高仿的组件,伪装成热门工具库的扩展包;接着通过开源社区渠道伪装成“热心贡献者”,向一个二线组件提交包含后门的 PR,同时该组件所在坐标被某个内网项目引用为传递依赖;攻击者在某个时间点触发恶意代码——读取 JVM 启动环境中的脚本配置,把进程运行时的网络流量转发到指定地址,再尝试读取本地常见的凭证文件。
从拉取依赖到触发行为,整条链路可能横跨数周。如果团队没有 SBOM 清单、没有行为观测,只有等到数据泄露或出现异常流量时才会察觉。这也是我坚持认为“依赖治理必须前置到构建阶段”的原因。
3. JDK 21缘何成为“透明人”:版本升级背后的安全误区
3.1 版本号升级了,安全状态却不一定升级
“JDK 21 正沦为透明人”这个说法有点夸张,但它精准戳中一个普遍现象:很多系统虽然启动命令里写着 JDK 21,安全状态却和跑在老版本时几乎没区别。我接到评测请求的那些系统里,有相当一部分的启动脚本长期挂着一长串--add-opens参数。
认识一下背景:JDK 9 引入模块化系统之后,JDK 自身的内部 API 默认被强封装,外部代码想访问这些内部类会被拒绝。这对安全是好事,能大幅压缩攻击者利用内部实现的想象空间。但不少老项目依赖反射调用 JDK 内部 API,升级时图省事,直接往启动脚本里加了一大堆--add-opens和--add-exports,等于亲手把 JDK 9+ 才建好的隔离门又全部打开。这样一来,JDK 版本号是 21,实际防护等级和几年前的旧版本相差无几。
另一个容易被忽略的点是:即使你什么都不做,长期停留在一个不再支持安全修复的旧版本,同样等于裸奔。JDK 21 作为长期支持版本,安全维护周期很长,但前提是团队要真正跟进它的安全补丁——很多团队连小版本更新都迟迟不升,习惯“上了 LTS 就永远不碰”。
3.2 JDK 21 本身的安全能力不差,差在没人启用
从能力底座看,JDK 21 是很扎实的版本。它默认启用强封装,限制了外部代码直接触碰 JDK 内部实现;加密层面对现代 TLS 协议和算法组合支持得更好,也支持在国家合规场景下启用 FIPS 模式。但问题在于:大多数团队只是在版本号上“路过”,并没有把这些能力接到系统的默认启动路径里。
举一个具体例子。模块化系统的强封装限制之所以有效,是因为它把“哪些类能被外部反射访问”收窄到一个明确边界。当你在启动脚本里为了兼容某个老库而无脑开放某几个包,那这个边界就开始模糊了。更现实的情况是,团队只完成了“换 JDK 版本 → 编译通过 → 测试通过”三步,没人去检查启动参数里是否残留着拆墙开关。
安全管理器的情况也类似。它从 JDK 17 开始被标记为弃用,后续版本逐步退出。过渡期内的 JDK 21 虽然还保留相关接口,但大量老项目里的沙箱配置并没有迁移到新机制,导致安全行为既带着旧的兼容负担,又没有明确的新防线。这个时期的“中间状态”,很容易被误读为“版本安全所以运行时安全”。
3.3 JDK 再强,也管不住依赖里的洞
“透明人”还有第二层含义:JVM 层级的防护不能替代应用层代码的过滤与收束。过去那些著名的反序列化漏洞、表达式注入漏洞,本质都是框架把外部输入当成可执行指令去处理。JVM 能做的是限制权限边界,但没法替每个业务接口判断某个输入到底该不该被解析成代码。
我见过不少团队,升到 JDK 21 之后觉得“安全这块差不多了”,结果一跑依赖扫描,构建产物里高位漏洞组件还能列出长长一串。两者完全是两个维度:JDK 负责运行时隔离,依赖治理负责减少恶意或缺陷代码的进入。判断一个系统安全状态,不能只看java -version,至少得同步看这三样:启动脚本的 JVM 参数里有哪些拆墙开关、依赖树里高位组件数量、运行时的类加载和外部连接有没有被观测。
4. 2026年Jar包“提款机”化的三个底层推手
4.1 生成式 AI 正在把恶意 Jar 的生产成本打到地板
2026 年需要正视的第一个变化,是生成式 AI 大幅降低了恶意组件生产的门槛。以前要写一个像样的恶意 Jar,攻击者得懂字节码、懂依赖描述、懂代码混淆,整套流程和正规开发差不多。现在,生成一批“看起来人畜无害”的工具库、配置处理组件、日志扩展包,只需对话式的方式就能完成雏形。
这意味着公共仓库里名字相似度极高的新包会越来越多,很多根本不是人工维护,而是自动化流水线批量投放。恶意代码可能藏在某个不起眼的“字符串处理工具”里,等应用启动后回连远程地址收取命令。对开发者的直觉来说,这类恶意包从代码风格上看几乎无法和正常库区分——因为它们可能就是 AI 按热门库的风格仿写出来的。
当恶意组件的“量”上去了,依赖扫描的盲区就会暴露。扫描器擅长的是匹配已知特征库,面对每天海量新增的、特征不明显的恶意组件,匹配结果会存在滞后。这要求团队不能只依赖扫描器,得在引入依赖的源头做好评估。
4.2 漏洞利用自动化:盯住升级路径而非公开漏洞
过去安全圈流行“在漏洞公开后抢时间”,现在攻击者的节奏变了。他们会主动拉取开源项目的版本变更记录、提交日志和依赖差异,自动分析新增的工具类和接口,把“依赖升级”本身当成一次攻击面扫描。攻击者不需要盯着最热门的那几个大组件——它们往往修复快、曝光高;真正被盯上的是大量被间接引用的二线组件,它们维护人力少、更新慢、使用方却很广。
这种趋势在 Java 生态里尤其值得警惕。因为 Java 二进制仓库体系成熟,一个组件可以被无数项目作为传递依赖引用。攻击者只需要污染一个二线组件的中低版本,就能在同一时间影响横跨多个行业的系统。这种“牵一发而动全身”的杠杆效应,正是 Jar 包被称作“提款机”的原因。
4.3 攻击者真正想要的东西:凭据、数据和部署节点
把 Jar 包叫“提款机”,重点不在 Jar 本身,而在于它能触达的下游价值。恶意组件一旦进了你的进程,攻击者通常瞄准这几类东西:埋在内网环境变量里的密钥和凭据、云服务商提供的临时角色凭证、配置中心和数据库里的业务数据,以及被打通后的服务器部署权限。
把恶意逻辑放进框架扩展机制中是常用手段——自定义序列化器、数据库驱动、指标上报组件、消息队列拦截器。这类扩展机制天然与业务代码深度绑定,加载后不易引起注意。攻击者只要把恶意代码塞进“看起来像工具增强”的接口实现里,就成了一个“只要加载依赖就能拿到运行时控制权”的入口。单次成功投放带来的收益,可能抵得上成千上万次钓鱼。
5. 防线怎么落:从依赖清单到运行时观测的五个实操
5.1 先给依赖上“清单制”:软件物料清单不是口号
我强烈建议团队现在就把软件物料清单(SBOM)生成接进构建流程。实操上不复杂:在构建配置里加上对应的 SBOM 插件,每次构建自动输出一份 JSON 或 XML 格式的依赖清单,里面包含每个组件的坐标、版本、下载地址和哈希值。这份清单需要随构建产物一起归档到制品库。
有了这份清单,当某天某个组件被曝出安全事件,团队能花十分钟定位受影响范围,而不是打开十几个故仓库目录手动比对依赖坐标。我今年已经靠这个机制处理了数次“某组件被报漏洞”的应急通知,效率比以往翻了几倍。前提是坚持在每次构建中生成归档——只写在文档里、不出现在 CI 里的 SBOM,等于没有。
5.2 内网化和锁定化:把依赖拉取从“自由市场”变成“持牌经营”
依赖解析的默认状态需要从“随便拉”改成“只拉内网”。具体做法分几步走。
先搭一台内网制品代理仓库,把公网仓库设成上游,但开发者不能直接访问公网,所有解析都通过内网代理完成。接着在构建配置中把依赖仓库源统一指向这个代理,杜绝在 pom 或构建脚本里硬编码公网地址。代理仓库开启缓存与校验,组件下载后比对哈希值,不一致的直接丢弃。
快照依赖的管理是另一个重点。能不用就不用;实在要用的,把自动更新策略改成“固定每日时刻”或“手动触发”,禁止每次构建都去检查最新快照。这样一来,即使公共仓库发生坐标混淆或版本投毒,内网代理层也能成为第一道拦截与审计点。
5.3 扫描与门禁都上,但别被数字焦虑带偏
把依赖扫描工具嵌进流水线,并设置硬性条件:高危组件不修复不放行,中危组件与版本升级窗口合并处理。这是技术动作,不需要太多纠结。真正需要经验的是处理“假阳性”。
很多扫描结果拦截的原因是项目同时存在多个传递版本,或者组件在运行时并没有走到出问题的代码路径。面对这类结果,我的处理原则是三步:先用依赖树定位,确认是哪个直接依赖引入了这个高危坐标;再看运行时路径,判断当前系统是否真的会执行相关代码;最后决定是升级、替换组件,还是记录为风险例外由安全负责人审批。
用这个原则,团队既不会在高位漏洞面前无脑恐慌,也不会因为扫得懒而漏掉真正要紧的风险。
5.4 构建环境不是“开发工具”,要当生产环境保护
构建和发布系统的安全水位,很多团队是偏低的。CI 节点不要挂载生产密钥、数据库地址和存储凭据;构建产物应当在完成签名后才进入制品库,发布前再校验签名和哈希。构建容器不要挂载宿主机的特权接口,避免容器内被攻破后直接波及宿主机。
还有个细节值得检查:CI 里有没有自动升级依赖的机器人。它本身是资产,可以帮助团队及时拿到上游修复,但自动合入新版本前,一定要有扫描门禁和测试保护兜底。如果机器人配置不当,比如合并 PR 时不跑测试、不看扫描结果,那它就不是自动维护依赖的好助手,而是一个给攻击者送更新包的传送带。
5.5 运行时最小面:把攻击者的舒适区压缩掉
最后说运行时。安全加固不一定要靠重武器,把“程序暴露面”收敛起来往往更有效。
产物瘦身是第一步:运行时不需要的工具类、调试接口、示例代码,从最终镜像里剔掉,减少可疑代码的藏身空间。权限收束是第二步:给 JVM 进程用独立系统账户运行,限定它可读目录和网络出口,别让一个业务进程默认拥有整台机器的访问权。行为观测是第三步:对异常类加载、异常外连、批量读取敏感文件等行为配置告警。这三样单拎出来都不是银弹,但合在一起,能把大量“默认敞开”型攻击挡在外面。
6. 写在最后的依赖治理体会
我自己的做法可能不太“标准”,但挺实用:这几年我持续把“每次构建都能回答三个问题”当成团队习惯——我们到底拉了什么、为什么不拉别的、运行时哪条路径会碰到它。这三个问题一旦有明确答案,供应链风险的可控度立刻不一样。
如果你还没行动,建议从这周开始:给自己最核心的业务系统生成一份软件物料清单,把依赖仓库的解析顺序改掉,在 CI 流水线里把高危组件扫描设为硬性门禁。不用追求一步到位,先迈出能落地的那一步。2026 年的威胁并不会等人把流程想周全再动手,但先跑起来的人,至少能看见攻击者留在仓库外的那串脚印。