首页 > 其他分享 >安全同学讲Maven间接依赖场景的仲裁机制

安全同学讲Maven间接依赖场景的仲裁机制

时间:2022-09-28 14:57:54浏览次数:88  
标签:场景 1.2 依赖 Maven pom 仲裁 POM

简介: 去年的Log4j-core的安全问题,再次把供应链安全推向了高潮。在供应链安全的场景,蚂蚁集团在静态代码扫描平台-STC和资产威胁透视平台-哈勃这2款产品在联合合作下,优势互补,很好的解决了直接依赖和间接依赖的场景。但是由于STC是基于事前,受限于扫描效率存在遗漏的风险面,而哈勃又是基于事后,存在修复时间上的风险。基于此,笔者尝试寻找一种方式可以同时解决2款产品的短板。

image.png

作者 | 唐天龙(唐礼)
来源 | 阿里开发者公众号

一 背景

为什么想写此文

去年的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文件定义的依赖管理引入进来。

image.png

二 单个Pom树的依赖竞争

Pom文件本质

一个Pom文件的本质就是一棵树。

在人的视角来观察一个Pom文件的时候,我们会认为他是一个线状的一个依赖列表,我们会认为下图的Pom文件抽象出来的结果是C依赖了A,B,D。但我们的视角是不完备的,Maven的视角来看,Maven会把这一个Pom文件直接抽象成一个依赖树。Maven的视角能看到除了ABD之外的节点。而人只能看到ABD三个节点。

既然是在一棵树上,那么相同的节点就必然会存在竞争关系。这个竞争关系就是我们提到了仲裁机制。

image.png

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会竞争成功。

image.png

常见场景

看到这里,想必大家已经了解了Maven的仲裁原则。但是在实际的工作中,光有原则还需要在代码中可以灵活的运用才能有属于自己的理解,这里笔者准备了5个场景,每个场景对应的答案都在后面,大家阅读时,可以自己尝试用Maven的原则来去推理,看看有没有哪里不符合预期的情况。

场景一 难度(☆)

场景描述

主POM里有< fastjson.version> 这个属性为1.2.24。

父亲是spring-boot-starter-parent-3.13.0。父亲里的< fastjson.version>是1.2.77。

并且在主pom中,消费了这个属性。

那么针对主POM这颗树,他最终会是使用哪一个fastjson呢?

场景示例

image.png
image.png

结构图

image.png

场景二 难度(☆☆)

在同一个主POM或者子POM中的dependencies中同时使用了Fastjson,第一个声明了1.2.24的版本,第二个声明了1.2.25版本。那么针对主POM或者子pom这棵树,最终会选择fastjson 1.2.24还是1.2.25呢?

场景示例

image.png

结构图

image.png

场景三 难度(☆☆☆)

下图中左图为主POM文件内的dependencyManagement里的fastjson为1.2.77,这个时候子POM中显示声明自己的版本1.2.78。那么针对子POM这颗树,子POM会选择听从父命还是遵从内心呢?

场景示例

image.png

结构图

image.png

场景四 难度(☆☆☆☆)

主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个版本中的哪一个呢?

场景示例

image.png

结构图

image.png

场景五 难度(☆☆☆☆☆)

主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个版本中的哪一个呢?

场景示例

image.png

结构图

image.png

答案

场景一

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没有人被依赖,所以最后构建。

image.png

SpringBoot Fatjar打包的策略

SpringBoot 打包会打成一个Fatjar,所有的依赖都会放在BOOT-INF/lib/目录下。SpringBoot的打包是越靠后的构建pom越优先,因为一般会把springboot的打包插件放在最不被依赖的module里(比如上图里的Pom3)。(SpringBoot的打包插件一般放在bootstrap pom里,这个名字可以我们自己起,一般都是依赖关系最考上的module。在多模块管理的springboot应用内,bootstrap往往是最不被依赖的那个module。)

image.png

子pom3最后参与构建,而且SpringBoot打包插件一般打的就是这个module。所以最终进入到SpringBoot打包产物的有A-2,B-2,E-2,F-2和D-1。因为A-2和B-2相比于其他几个相同节点更靠近树的主干。E-2和F-2也是同理。这个规律体感上是靠后优先了,因为靠后的树天然更加靠近主干。

image.png

四 仲裁机制在Maven源码中的实现

以Maven的3.6.3版本的源码进行分析,我们尝试分析Maven中对依赖处理的几处原则,方能从源码的层面上正向的证明仲裁机制的准确性。另外从源码上也可以看出一些Maven上的机制为什么是这样,而不是单单的他的机制是什么样。因为笔者相信,任何机制都无法保证与时俱进下的先进性,所以笔者认为上文中提到的所有的仲裁机制有一天可能会发生变化,这些结论并非最重要,而是如何调研这些结论更为重要!

Maven是如何实现出继承并且相同属性子覆盖父的

Maven中有2条非常重要的主线。一个是依赖,另一个就是继承。Maven在源码中实现继承大体如下。在下图中使用readParent进行对父亲的模型获取之后,便让自己陷入这个循环中。唯一可以出去这个循环的方式就是追不到父亲为止。并且把每次取到模型数据放到linega这个对象当中。下图中最下面的assembleInheritance我们看他消费了linega这个对象,目的就是完成真实的继承和覆盖。

image.png

在assembleInheritance中我们会发现一个很有意思的现象,lingage是倒着进行遍历,并且是从倒数第二个元素开始,这正是上文中我们提到了的Maven的一个设计哲学。Maven认为这个世界上所有的pom文件都存在一个父亲,类似Java的Object。这里便是对这个哲学处理的一个浅逻辑。

另外Maven自上而下的去遍历,更加方便自己去实现相同的元素子覆盖父的能力,这也是笔者认为在编码上的一个小心思。

image.png

Reactor反应堆在源码中的实现

上文中我们还提到了一个非常重要的概念,就是反应堆。反应堆直接决定了各个子pom是如何决定构建顺序的。在Maven的源码中,他是在getProjectsForMavenReactor函数中进行实现的。并且我们从下图中也可以看到,Maven的反应堆是不能解决循环依赖的,他直接捕获了这种异常!

image.png

真正实现反应堆算法的是在ProjectSorter的构造函数中通过Dag进行实现的。Dag(有向无环图)和广度优先搜索是解决依赖场景是一个很好的方式。

在有向无环图中通过每次挑选出入度为0的节点,再删除该节点和此节点的相邻边,不断重复上述步骤。就可以高效率的计算出DAG上的所有节点的依赖顺序,Maven也正是用到了这个思路。

从这个源码的视角也可以解释为什么Maven必须要保证每一个子pom之前不能出现循环依赖。

image.png

同一个Pom文件内dependency 后声明的优先的实现

在处理Dependencies时,Maven并没有对此进行特殊处理,是直接使用的Map的方式进行覆盖的。关于这里为什么这么设计,笔者并不清楚。笔者曾一度猜测这么设计是为了让开发同学更好的编写,因为靠后优先往往符合大部分人的编码习惯。但是在这里我们看到了作者的一行注释,意思大概是说,这样设计是为了向后兼容Maven2.x,因为Maven2.x 是不会去校验一个文件是否只存在一个同GA的唯一依赖。所以后面的maven的版本应该也是延续了这种风格。

image.png

当循环进行处理到1.2.25的时候,依然进行对normalized这个map进行put操作导致了 key值相同的情况下的覆盖。

image.png

五 安全视角应如何避免间接依赖

分析

作为安全同学,笔者更希望的是针对这种多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阶段用到的依赖。

推荐阅读

1.如何写出一篇好的技术方案?

2.阿里10年沉淀|那些技术实战中的架构设计方法

3.如何做好“防御性编码”?


 

阿里云产品测评—开源PolarDB-PG

 

体验阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 的能力和高可靠、高可用、弹性扩展等企业级数据库特性。发布评测,写下你的感受与评价即可获得多重福利。

点击这里,查看详情。

原文链接:https://click.aliyun.com/m/1000360205/ 本文为阿里云原创内容,未经允许不得转载。

标签:场景,1.2,依赖,Maven,pom,仲裁,POM
From: https://www.cnblogs.com/yunqishequ/p/16738014.html

相关文章

  • 什么是GPU云服务器,有哪些优势,适用于什么场景?
    GPU云服务器(Cloud GPU Service)是基于GPU的快速、稳定、弹性的计算服务,主要应用于深度学习训练/推理、图形图像处理以及科学计算等场景。GPU云服务器提供和标准CVM......
  • 腾讯云硬盘有哪些类型可选?都分别适用于哪些场景?
    云硬盘类型简介高性能云硬盘高性能云硬盘是腾讯云推出的混合型存储类型,通过Cache机制提供接近固态存储的高性能存储能力,同时采用三副本的分布式机制保障数据可靠性。高性......
  • wagon-maven-plugin实现自动打包部署到服务器
    https://dandelioncloud.cn/article/details/14893795494766714901.在maven中添加依赖<dependency><groupId>org.codehaus.mojo</groupId......
  • 读多写少的场景下,竟然还有比读写锁更牛X的锁?
    1)上一篇文章我们聊了读写锁,他的适用场景是读多写少的场景下,那有没有其它性能比读写锁还要牛逼的锁呢?StampedLock,java1.8诞生的。2)StampedLock比读写锁牛在什么地方?读写锁......
  • 1.SSM框架 maven模块+oracle+注解开发
    一、SSM框架maven模块+oracle+注解开发1.初始化数据库--创建表空间语句createtablespacessm_69datafile'c:\ssm_69.dbf'size100mautoextendonnext10m;--创建用......
  • P4 MAVEN的目录结构
    #4MAVEN的目录结构和手动创建MAVEN项目##4.1MAVEN的目录结构   5MAVEN项目的编译和启动5.1准备工作第一次会下载很多包,要改变一下下载路径会下载很多......
  • P2(从2开始的) MAVEN的简介
    注:笔记内容摘抄自视频,侵权删!3项目构建工具摘要:新型公司使用grandle4Maven四大特性4.1依赖管理系统听不太懂,大概就是下载jar包只需要配置,不需要手动下载4.2多模......
  • 用过Redis吗,它使用在哪些地方(使用场景)
    1.用过Redis吗,它使用在哪些地方(使用场景)?Redis是一种内存型数据库,用作数据库,缓存和消息代理。数据结构简单,读写速度快(毫秒级),原子操作。适用于:数据高并发读写、海量数......
  • 【转载】Python的深浅拷贝以及应用场景
    深浅拷贝的原理深浅拷贝用法来自copy模块。导入模块:importcopy浅拷贝:copy.copy深拷贝:copy.deepcopy字面理解:浅拷贝指仅仅拷贝数据集合的第一层数据,深拷贝指拷贝数据......
  • ARMS实践|日志在可观测场景下的应用
    简介: 在实际生产中,通过灵活组合文内几种使用方式,运维团队可以很好地排除日常观测、故障定位过程中的干扰因素,更快的定界甚至定位问题根因。作者:陈陈 日志在......