安全同學講Maven間接依賴場景的仲裁機制

簡介: 去年的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/
本文爲阿里雲原創內容,未經允許不得轉載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章