安全同学讲Maven间接依赖场景的仲裁机制
简介: 去年的Log4j-core的安全问题,再次把供应链安全推向了高潮。在供应链安全的场景,蚂蚁集团在静态代码扫描平台-STC和资产威胁透视平台-哈勃这2款产品在联合合作下,优势互补,很好的解决了直接依赖和间接依赖的场景。但是由于STC是基于事前,受限于扫描效率存在遗漏的风险面,而哈勃又是基于事后,存在修复时间上的风险。基于此,笔者尝试寻找一种方式可以同时解决2款产品的短板。
作者 | 唐天龙(唐礼)
来源 | 阿里开发者公众号
一 背景
为什么想写此文
去年的Log4j-core的安全问题,再次把供应链安全推向了高潮。在供应链安全的场景,蚂蚁集团在静态代码扫描平台-STC和资产威胁透视平台-哈勃这2款产品在联合合作下,优势互补,很好的解决了直接依赖和间接依赖的场景。
但是由于STC是基于事前,受限于扫描效率存在遗漏的风险面,而哈勃又是基于事后,存在修复时间上的风险。基于此,笔者尝试寻找一种方式可以同时解决2款产品的短板。笔者尝试研究了一下Maven是如何处理一个项目中的直接依赖和间接依赖的,并且在遇到相同依赖时,Maven是如何进行抉择的,这里的如何抉择其实就是Maven的仲裁机制。带着这些问题,笔者尝试调研了Maven的源码和做了一些本地的测试实验。总结了这篇文章。
坐标是什么?
在空间坐标系中,我们可以通过xyz表示一个点,同样在Maven的世界里,我们可以通过一组GAV在依赖的世界里明确表示一个依赖,比如:
< groupId> : com.alibaba 一般是公司的名称
< artifactId> : fastjson 项目名称
< version> : 1.2.24 版本号
影响依赖的标签都有哪些
1.< dependencies>
直接引入具体的依赖信息。注意是不在< dependencyManagement>标签内的情况。如果是在< dependencyManagement>内的情况,请参考2号标签。
2.< dependencyManagement>
只声明但不发生实际引入,作为依赖管理。依赖管理是指真正发生依赖的时候,再去参考依赖管理的数据。
- 这样使用dependency的时候,可以缺省version。
- 另外< dependencyManagement> 还可以管控所有的间接依赖,即使间接依赖声明了version,也要被覆盖掉。
3.< parent>
声明自己的父亲,Maven的继承哲学跟Java很类似,因为Maven本身也是用Java实现的,满足单继承。
- 一旦子pom继承了父pom,那么会把父pom里的 < dependencies> ,< dependencyManagement>等等属性都继承过来的。当然如果在继承的过程中,出现一样的元素,也是子去覆盖父亲,和Java类似。
- 继承时,会分类继承。dependencies继承dependencies,dependencyManagement里的依赖管理只能继承dependencyManagement范围内的依赖管理。
- 每一个pom文件都会有一个父亲,即使不声明Parent,也会默认有一个父亲。和Java的Object设计哲学类似。后面在源码分析中我们还会提到。
4.< properties>
代表当前自己的项目的一个属性的集合。
properties仅仅代表属性的声明,一个属性声明了,和他是否被引用并无关系。我完全可以声明一系列不被人使用的属性。
依赖的作用域都有哪些
一个依赖在引入的时候,是可以声明这个依赖的作用范围的。比如这个依赖只对本地起作用,比如只对测试起作用等等。作用域一共有compile,provided,system,test,import,runtime 这几个值。
简单总结一下:
- compile和runtime会参与最后的打包环节,其余的都不会。compile可以不写。
- test只会对 src/test目录下的测试代码起作用。
- provided是指线上已经提供了这个Jar包,打包的时候不需要在考虑他了,一般像serlvet的包很多都是provided。
- system和provided没什么太大的区别。
- import只会出现在dependencyManagement标签内的依赖中,是为了解决Maven的单继承。引入了这个作用域的话,maven会把此依赖的所有的dependencyManagement内的元素加载到当前pom中的,但不会引入当前节点。如下图,并不会引入fastjson作为依赖管理的元素,只是会把fastjson文件定义的依赖管理引入进来。
二 单个Pom树的依赖竞争
Pom文件本质
一个Pom文件的本质就是一棵树。
在人的视角来观察一个Pom文件的时候,我们会认为他是一个线状的一个依赖列表,我们会认为下图的Pom文件抽象出来的结果是C依赖了A,B,D。但我们的视角是不完备的,Maven的视角来看,Maven会把这一个Pom文件直接抽象成一个依赖树。Maven的视角能看到除了ABD之外的节点。而人只能看到ABD三个节点。
既然是在一棵树上,那么相同的节点就必然会存在竞争关系。这个竞争关系就是我们提到了仲裁机制。
Maven仲裁机制原则
1.依赖竞争时,越靠近主干的越优先。
2.单颗树在依赖在竞争时(dependencies)(注意:不是dependencyManagement里的dependencies):
当deep=1,即直接依赖。同级是靠后优先。
当deep>1,即间接依赖。同级是靠前优先。
3.单颗树在依赖管理在竞争时(注意:是dependencyManagement里的dependencies)是靠前优先的。
4.maven里最重要的2个关系,分别是继承关系和依赖关系。我们所有的规律都应该只从这2个关系入手。
下图中分别是2个子pom文件(方块代表依赖的节点,A-1 表示A这个节点使用的是1版本,字母代表节点,数字代表版本)。
左边这个子pom生成的树依赖了 D-1,D-2和D-5。满足依赖竞争原则1,即越靠近树的左侧越优先的原则,所以D-5会竞争成功。
但是B-1和B-2同时都位于树的同一深度,并且深度为1,由于B-2更加靠后,所以B-2会竞争成功。
右边的子pom生成的树依赖了 D-1和D-2,并且位于同一深度,但由于D-1和D-2是属于间接依赖的范围,deep大于1,所以是靠前优先,那么也就是D-1会竞争成功。
常见场景
看到这里,想必大家已经了解了Maven的仲裁原则。但是在实际的工作中,光有原则还需要在代码中可以灵活的运用才能有属于自己的理解,这里笔者准备了5个场景,每个场景对应的答案都在后面,大家阅读时,可以自己尝试用Maven的原则来去推理,看看有没有哪里不符合预期的情况。
场景一 难度(☆)
场景描述
主POM里有< fastjson.version> 这个属性为1.2.24。
父亲是spring-boot-starter-parent-3.13.0。父亲里的< fastjson.version>是1.2.77。
并且在主pom中,消费了这个属性。
那么针对主POM这颗树,他最终会是使用哪一个fastjson呢?
场景示例
结构图
场景二 难度(☆☆)
在同一个主POM或者子POM中的dependencies中同时使用了Fastjson,第一个声明了1.2.24的版本,第二个声明了1.2.25版本。那么针对主POM或者子pom这棵树,最终会选择fastjson 1.2.24还是1.2.25呢?
场景示例
结构图
场景三 难度(☆☆☆)
下图中左图为主POM文件内的dependencyManagement里的fastjson为1.2.77,这个时候子POM中显示声明自己的版本1.2.78。那么针对子POM这颗树,子POM会选择听从父命还是遵从内心呢?
场景示例
结构图
场景四 难度(☆☆☆☆)
主POM的dependencies Fastjson:1.2.24 主POM的dependencymanagent Fastjson:1.2.77
主POM的父亲(springboot)的dependencies Fastjson 1.2.78
子POM里的dependencies Fastjson 1.2.25
这种情况下针对子pom来说,他会选择4个版本中的哪一个呢?
场景示例
结构图
场景五 难度(☆☆☆☆☆)
主POM的dependencies Fastjson:1.2.24 主POM的dependencymanagent Fastjson:1.2.77
主POM的父亲(springboot)的dependencies Fastjson 1.2.78
子POM里的dependencies 不写version
场景五跟场景四整体没有差别,只是将子pom的dependencies的版本进行缺省。
这种情况下针对子pom来说,针对子pom,他会选择3个版本中的哪一个呢?
场景示例
结构图
答案
场景一
1.2.24会最终生效。
因为子会继承父亲的属性,但是由于自己有这个属性,那么则覆盖!
继承一定会伴随着覆盖的,这个设计在编程语言中还是比较普遍的。
场景二
1.2.25会最终生效。
参考 单颗树在依赖在竞争时:当deep=1,即直接依赖。同级是靠后优先。
满足Maven的核心竞争依赖策略!
场景三
1.2.78最终会生效。
一个项目里的dependencyManagement只能对不声明version的dependency和间接依赖有效!
场景四
1.2.25会最终生效。这个比较复杂。
〇: 首先根据父子的继承关系,1.2.24会覆盖掉1.2.78。所以78版本淘汰
一: 由于一个项目里的dependencyManagement只能对不声明version的dependency和间接依赖有效,所以
1.2.77无法对1.2.25起作用。
二: 由于父子的继承关系,1.2.25会覆盖掉1.2.24.
所以最终1.2.25胜出!
场景五
1.2.77会最终生效。
〇: 首先根据父子的继承关系,1.2.24会覆盖掉1.2.78。所以78版本淘汰
一: 由于一个项目里的dependencyManagement是可以对不声明的version起作用,所以子pom的版本为1.2.77
二: 由于父子的继承关系,1.2.77会覆盖掉1.2.24.
所以最终1.2.77胜出!
三 多个Pom树合并打包
多棵树构建顺序原则
现在的项目一般都是多模块管理,会存在非常多的pom文件。多棵树的情况下每棵树的出场顺序都是事先已经被计算好的。
这个功能在Maven的源码中是一个叫Reactor(反应堆)实现的。它主要做了一件事情就是决定一个项目中,多个子pom谁先进行build的顺序,这个出厂顺序很重要,在合并打包时,往往决定了最终谁会在多个pom之间胜出的问题。
Reactor的原则
多棵树(多个子pom)构建的顺序是按照被依赖方的要在前,依赖方在后的原则。
项目要保证这里是不能出现循环依赖的。
Reactor的原则图解
如下图子pom1 在被子pom2和子pom3同时依赖,所以子pom1最先被构建,子pom3没有人被依赖,所以最后构建。
SpringBoot Fatjar打包的策略
SpringBoot 打包会打成一个Fatjar,所有的依赖都会放在BOOT-INF/lib/目录下。SpringBoot的打包是越靠后的构建pom越优先,因为一般会把springboot的打包插件放在最不被依赖的module里(比如上图里的Pom3)。(SpringBoot的打包插件一般放在bootstrap pom里,这个名字可以我们自己起,一般都是依赖关系最考上的module。在多模块管理的springboot应用内,bootstrap往往是最不被依赖的那个module。)
子pom3最后参与构建,而且SpringBoot打包插件一般打的就是这个module。所以最终进入到SpringBoot打包产物的有A-2,B-2,E-2,F-2和D-1。因为A-2和B-2相比于其他几个相同节点更靠近树的主干。E-2和F-2也是同理。这个规律体感上是靠后优先了,因为靠后的树天然更加靠近主干。
四 仲裁机制在Maven源码中的实现
以Maven的3.6.3版本的源码进行分析,我们尝试分析Maven中对依赖处理的几处原则,方能从源码的层面上正向的证明仲裁机制的准确性。另外从源码上也可以看出一些Maven上的机制为什么是这样,而不是单单的他的机制是什么样。因为笔者相信,任何机制都无法保证与时俱进下的先进性,所以笔者认为上文中提到的所有的仲裁机制有一天可能会发生变化,这些结论并非最重要,而是如何调研这些结论更为重要!
Maven是如何实现出继承并且相同属性子覆盖父的
Maven中有2条非常重要的主线。一个是依赖,另一个就是继承。Maven在源码中实现继承大体如下。在下图中使用readParent进行对父亲的模型获取之后,便让自己陷入这个循环中。唯一可以出去这个循环的方式就是追不到父亲为止。并且把每次取到模型数据放到linega这个对象当中。下图中最下面的assembleInheritance我们看他消费了linega这个对象,目的就是完成真实的继承和覆盖。
在assembleInheritance中我们会发现一个很有意思的现象,lingage是倒着进行遍历,并且是从倒数第二个元素开始,这正是上文中我们提到了的Maven的一个设计哲学。Maven认为这个世界上所有的pom文件都存在一个父亲,类似Java的Object。这里便是对这个哲学处理的一个浅逻辑。
另外Maven自上而下的去遍历,更加方便自己去实现相同的元素子覆盖父的能力,这也是笔者认为在编码上的一个小心思。
Reactor反应堆在源码中的实现
上文中我们还提到了一个非常重要的概念,就是反应堆。反应堆直接决定了各个子pom是如何决定构建顺序的。在Maven的源码中,他是在getProjectsForMavenReactor函数中进行实现的。并且我们从下图中也可以看到,Maven的反应堆是不能解决循环依赖的,他直接捕获了这种异常!
真正实现反应堆算法的是在ProjectSorter的构造函数中通过Dag进行实现的。Dag(有向无环图)和广度优先搜索是解决依赖场景是一个很好的方式。
在有向无环图中通过每次挑选出入度为0的节点,再删除该节点和此节点的相邻边,不断重复上述步骤。就可以高效率的计算出DAG上的所有节点的依赖顺序,Maven也正是用到了这个思路。
从这个源码的视角也可以解释为什么Maven必须要保证每一个子pom之前不能出现循环依赖。
同一个Pom文件内dependency 后声明的优先的实现
在处理Dependencies时,Maven并没有对此进行特殊处理,是直接使用的Map的方式进行覆盖的。关于这里为什么这么设计,笔者并不清楚。笔者曾一度猜测这么设计是为了让开发同学更好的编写,因为靠后优先往往符合大部分人的编码习惯。但是在这里我们看到了作者的一行注释,意思大概是说,这样设计是为了向后兼容Maven2.x,因为Maven2.x 是不会去校验一个文件是否只存在一个同GA的唯一依赖。所以后面的maven的版本应该也是延续了这种风格。
当循环进行处理到1.2.25的时候,依然进行对normalized这个map进行put操作导致了 key值相同的情况下的覆盖。
五 安全视角应如何避免间接依赖
分析
作为安全同学,笔者更希望的是针对这种多module的Maven项目可以梳理出一个经验,怎样去避免间接依赖的问题。
经过上面的分析,我们可以得出3条结论:
1.子pom声明版本在安全视角是非常危险的,子pom不应该显示声明版本。
由于子pom会继承主pom的元素,并且在继承的时候会出现覆盖的场景。那么针对CE或者SpringBoot打包时,有可能出现子pom的build的顺序位置天然非常有优势,容易造成子pom的版本进入最终的打包产物。
2.主POM的dependencyManagent可以管控到 间接依赖 和 不显示声明version的直接依赖。
3.主POM的dependencies不能出现危险版本。否则子pom天然的继承了这个危险版本参与打包。
结论
以上几条同时满足,便可以解决间接依赖的问题。
即:
针对SpringBoot而言,子pom不应该显示声明版本,主Pom的dependencyManagent应该管控安全版本的依赖,并且主pom不能出现危险版本。(主Pom dependencies强行写上安全版本更佳,这样可以避免掉依赖的父亲里存在残留的不安全的依赖)
六 最后
Maven的源码地址
https://archive.apache.org/dist/maven/maven-3/
我是怎么分析的
本人在本地针对SpringBoot,做多轮测试。在根目录下执行mvn clean package即可!
mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true 会帮助分析到具体的节点。
另外就是尝试在源码中找到这里的实现,这样更能加深理解!
常用的分析命令
0.mvn clean package -DSkipTest 直接进行打包,进行结果分析
1.mvn dependency:tree 会把整个的maven的树形结构输出
2.mvn help:effective-pom -Dverbose 这个命令输出的信息更加完整,输出的是effectivepom
3.mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true
4.mvn -D maven.repo.local =你的目录 compile阶段用到的依赖。
推荐阅读
阿里云产品测评—开源PolarDB-PG
体验阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 的能力和高可靠、高可用、弹性扩展等企业级数据库特性。发布评测,写下你的感受与评价即可获得多重福利。
点击这里,查看详情。
安全同学讲Maven间接依赖场景的仲裁机制的更多相关文章
- Maven间接依赖冲突解决办法
如果项目中maven依赖太多,由于还有jar之间的间接依赖,所以可能会存在依赖冲突.依赖冲突大部分都是由于版本冲突引起的,查看maven的依赖关系,可以找到引起冲突的间接依赖 如上图,通过Depend ...
- maven 间接依赖的jar自动引入
很多时候,我们引用的第三方jar需要一些其他的第三方jar,这个时候默认情况下,间接需要依赖的第三方jar是不会自动被引入的,如果希望这些额外的三方jar被自动引入,则在Maven仓库中除了提交jar ...
- Maven快速入门(五)Maven的依赖管理
前面我们讲了maven项目中的最重要的文件:pom.xml 配置文件相关内容.介绍了pom 是如何定义项目,如何添加依赖的jar 包的等. 我们知道,在Maven的生命周期中,存在编译.测试.运行等过 ...
- maven实现依赖的“全局排除”
大多数java应用源码构建和依赖管理是使用maven来实现的,maven也是java构建和依赖管理的事实上的标准.我们的应用系统也都是基于maven构建的,maven虽然在依赖管理方面确实很牛叉,但是 ...
- 我用段子讲.NET之依赖注入其二
<我用段子讲.NET之依赖注入其二> "随着我们将业务代码抽象化成接口和实现两部分,这也使得对象生命周期的统一管理成为可能.这就引发了第二个问题,.NET Core中的依赖注入框 ...
- maven的中传递依赖,maven的依赖管理(转)
在maven的pom文件中 <dependencies> <dependency> <groupId>junit</groupId> <artif ...
- maven的依赖特性
若排版紊乱可查看我的个人博客原文地址 maven的依赖特性很多很杂,这里大概总结一下,maven的依赖特性主要是依赖范围和传递依赖,前者会影响后者,这篇文章会介绍传递依赖的传递原则,出现冲突传递依赖默 ...
- Maven学习(十七)-----Maven外部依赖
Maven外部依赖 正如大家所了解的那样,Maven确实使用 Maven 库的概念作依赖管理.但是,如果依赖是在远程存储库和中央存储库不提供那会怎么样? Maven 提供为使用外部依赖的概念,就是应用 ...
- maven项目依赖包问题
问题 maven传递依赖 解决方案 前段时间,开发中遇到一个关于maven依赖包的问题:由于业务需要,支付网关对账代码中的slf4j-api包需要更新,原包为1.5.8版本,需要更新到1.6.4版 ...
- maven 学习---Maven外部依赖
现在,你也知道Maven做依赖管理使用Maven仓库的概念.但是,如果依赖是不提供任何远程存储库和中央存储库发生了什么? Maven提供为使用外部依赖的概念,应用在这样的场景. 举一个例子,让我们做以 ...
随机推荐
- C++ 赋值运算符和拷贝构造函数
拷贝构造函数 class Foo{ public: Foo(); Foo(const Foo&); //自己定义的拷贝构造函数 }; 如果不自己定义,编译器会自己合成一个默认拷贝构造函数: c ...
- 【leetcode 春季比赛3题 二叉搜索树染色】广度搜索
暴力: import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import ja ...
- Ubuntu如何进救援模式
linux的救援模式-1 详解在 Ubuntu 中引导到救援模式或紧急模式 这篇教程将介绍如何在 Ubuntu 22.04.20.04 和 18.04 LTS 版本中引导到 救援Rescue 模式或 ...
- 云VR:虚拟现实专业化的下一步
传统的VR通常需要功能强大的计算机和其他高性能设备来提供良好的用户体验.但是,如果有一种方法可以从任何设备和任何地方处理VR内容呢?这就是云VR对VR用户的承诺.随着5G和其他网络的到来,VR技术的未 ...
- java方法的内存及练习
方法的内存 一.方法调用的基本内存原理: Java内存分配 栈: 方法运行时使用的内存方法进栈运行,运行完毕就出栈 堆: newl出来的,都在堆内存中开辟了一个小空间 方法区: 存储可以运行的clas ...
- 介绍几款WPF应用的UI库
在WPF中对于前端页面的书写,我们有现成的UI类库,不需要我们自己再去写 我这里介绍几款 1.MahApps 官网 https://mahapps.com/ 使用,在App.xaml中添加 <A ...
- KingbaseES使用sys_backup.sh脚本init初始化配置文件常见错误处理
KingbaseES使用sys_backup.sh脚本init初始化配置文件常见错误处理: 一.sys_backup.sh脚本按照如下顺序寻找初始化配置文件: [kingbase@postgres ~ ...
- wordpress自建博客站,为文章添加显示浏览次数功能
wordpress自建博客站,为文章添加显示浏览次数功能 笔者使用的主题是 GeneratePress 版本:3.1.3 1.后台文章管理列表添加浏览次数列 效果如图: 实现: 编辑funct ...
- Python爬取腾讯疫情实时数据并存储到mysql数据库
思路: 在腾讯疫情数据网站F12解析网站结构,使用Python爬取当日疫情数据和历史疫情数据,分别存储到details和history两个mysql表. ①此方法用于爬取每日详细疫情数据 1 impo ...
- MySQL创建和操纵表
表创建基础 CREATE TABLE customers ( cust_id int NOT NULL AUTO_INCREMENT , cust_name char(50) NOT NULL , c ...