Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

PEP 708 – 擴充套件庫 API 以減輕依賴混淆攻擊

作者:
Donald Stufft <donald at stufft.io>
PEP 委託人:
Paul Moore <p.f.moore at gmail.com>
討論於:
Discourse 討論串
狀態:
臨時性 (Provisional)
類型:
標準軌跡 (Standards Track)
主題:
套件封裝 (Packaging)
建立日期:
20-Feb-2023
公告歷史:
01-Feb-2023, 23-Feb-2023
決議:
Discourse 訊息

目錄

臨時接納 (Provisional Acceptance)

此 PEP 已被臨時接納,在成為正式 (Final) 版本前需滿足以下要求:

  1. 在 PyPI (Warehouse) 中實作此 PEP,包括任何允許專案擁有者設定追蹤資料所需的 UI 元素。
  2. 在 PyPI 以外的至少一個套件庫中實作此 PEP,因為若沒有至少兩個索引,便無法真正測試合併索引的過程。
  3. 在 pip 中實作此 PEP,支援預期的語義並能證明達到預期的安全性效益。此實作初期需設為「預設關閉」,這意味著使用者必須主動選擇測試它。理想情況下,我們應收集成功試用此新功能的使用者(專案擁有者與專案使用者)明確的正面回饋,而不僅僅是假設「沒有消息就是好消息」。

摘要

依賴混淆攻擊(Dependency confusion attacks),即安裝了惡意套件而非使用者預期的套件,是一種日益普遍的供應鏈威脅。大多數針對 Python 依賴項的此類攻擊,包括近期的 PyTorch 事件,都發生在擁有多個套件庫的情況下,即原本預期來自一個套件庫(例如自訂索引)的依賴項,卻被從另一個套件庫(例如 PyPI)安裝了。

為了協助解決此問題,本 PEP 提議擴充 Simple Repository API,允許套件庫營運者指明其套件庫中的專案「追蹤」了其他套件庫中的專案,並允許專案跨多個套件庫擴展其命名空間。

這些功能將允許安裝程式判斷從特定組合的套件庫取得的專案是否符合預期且應被允許,若否,則應停止安裝並報錯,以保護使用者。

動機

有一類長期存在的攻擊稱為「依賴混淆」攻擊,簡而言之,使用者預期取得套件 A,但實際上卻取得了 B。在 Python 中,這種情況幾乎總是因為配置了多個套件庫(可能包含預設的 PyPI),使用者預期套件 A 來自套件庫 X,但有人在套件庫 Y 中以相同名稱發佈了套件 B

依賴混淆攻擊長期以來一直存在,但隨著成功執行這些攻擊的公開案例出現,近期受到了媒體關注。

一個具體的例子是近期的 PyTorch 專案事件,其內部套件名稱為 torchtriton,僅原定從位於 https://download.pytorch.org/ 的套件庫中安裝。然而,該套件庫被設計為與 PyPI 共同使用,且 torchtriton 名稱在 PyPI 上未被佔用,這使得攻擊者能夠使用該名稱並發佈惡意版本。

目前有多種方法可以減輕這些攻擊,但它們都要求終端使用者主動採取行動來保護自己,而不是預設受到保護。這意味著對於絕大多數使用者而言,即使他們最終意識到了這些類型的攻擊,他們很可能仍然處於脆弱狀態。

最終,這些攻擊的根本原因在於沒有一個全域唯一的命名空間涵蓋所有 Python 套件名稱。相反地,每個套件庫都是其各自獨立的命名空間。當給定一個「抽象」名稱(如 spam)進行安裝時,安裝程式必須將其隱式轉換為「具體」名稱(如 pypi.org:spamexample.com:spam)。目前 Python 安裝工具的標準行為是隱式地將這些多個命名空間扁平化為一個包含所有命名空間檔案的空間。

這種合併命名空間的預設行為意味著,當不同套件庫中名稱相同的套件由不同方撰寫時(例如 torchtriton 案例),依賴混淆攻擊便成為可能。

這特別棘手的地方在於沒有唯一的「正確」答案;將兩個套件庫合併為一個命名空間,以及將兩個套件庫視為獨立命名空間,都有其合理的應用場景。這意味著安裝程式需要某種機制來判斷何時應合併多個套件庫的命名空間,以及何時不應合併,而不是採取「一律合併」或「永不合併」的簡單規則。

此功能可以直接推向終端使用者,因為終端使用者才是決定該從哪個套件庫安裝什麼內容的人。然而,透過擴充套件庫規格以允許套件庫指示何時是安全的,我們可以使個別專案和套件庫在「預設情況下運作」,即使專案自然地跨越了多個不同的命名空間,同時仍能保持安裝程式預設的安全能力。

單憑此 PEP 並不能解決依賴混淆攻擊,但它提供了足夠的資訊,使安裝程式能夠在不對其他有效且安全的用例造成過多連帶損害的情況下防止這類攻擊。

原理

本 PEP 旨在實現跨套件庫合併名稱的兩種廣泛用例。

第一種用例是當某個套件庫不自行定義名稱,而是擴展在其他套件庫中定義的名稱時。這種情況常見於專案從一個套件庫鏡像到另一個套件庫(請參見 Bandersnatch),或是套件庫為特定平台提供補充檔案(請參見 Piwheels)。

在這種情況下,被擴展的套件庫或專案可能完全不知道自己正被擴展,或不知道被誰擴展,因此這不能依賴於「擴展」套件庫本身以外的任何資訊。

第二種用例是當專案希望發佈到一個「主要」套件庫,但同時有額外的套件庫提供用於其他平台、GPU、CPU 等的二進位檔時。目前 wheel 標籤無法充分表達這些二進位相容性類型,因此希望依賴這些功能的專案被迫建立多個套件庫,並要求使用者手動設定以取得正確的平台、GPU、CPU 等二進位檔。

此用例與第一種相似,但關鍵區別在於誰提供資訊以及信任程度為何,這使其成為一個獨立的用例。

當使用者設定特定的套件庫(或依賴預設值)時,對於該套件庫的指涉並無歧義。套件庫由 URL 識別,透過網域系統,URL 是全域唯一的識別碼。這種無歧義性意味著安裝程式可以假設套件庫營運者是值得信賴的,並且可以信任他們提供的後設資料,而無需進行驗證。

反之,如果安裝程式在多個套件庫中發現同一個名稱,則應信任哪一個存在歧義。這種歧義意味著安裝程式不能假設任一套件庫的專案擁有者都是值得信賴的,並且需要驗證他們是否確實是同一個專案,以及這是否不是一場依賴混淆攻擊。

若沒有某種方式讓安裝程式驗證多個套件庫之間的後設資料,專案將被迫成為套件庫營運者以安全地支援此用例。這本身並非錯誤的選擇;然而,危險在於如果我們不提供一種方法讓套件庫允許專案擁有者安全地表達這種關係,他們將會受到激勵,改而使用套件庫營運者的後設資料,這將重新引入最初的不安全性。

規範

本規格定義了簡單套件庫 API 1.2 版的變更,增加了兩個新的後設資料項目:套件庫「追蹤 (Tracks)」與「替代位置 (Alternate Locations)」。

套件庫「追蹤 (Tracks)」後設資料

為了使一個套件庫能夠託管旨在「擴展」其他套件庫中所託管專案的專案,本 PEP 允許擴展套件庫透過新增其擴展之專案及套件庫的 URL,來宣告特定專案「追蹤」另一個套件庫或多個套件庫中的專案。

這在 JSON 中以 meta.tracks 鍵值表示,在 HTML 中則作為專案特定 URL($root/$project/)上名為 pypi:tracks 的 meta 元素存在。

使用此後設資料時,有幾個關鍵屬性必須被遵守:

  • 必須由套件庫營運者自行控制,而不是由使用該套件庫的任何個別發佈者控制。
    • 「套件庫營運者」也可包括任何管理特定套件庫整體命名空間的人員,這在託管套件庫服務等情況下可能會發生,即由一個實體操作軟體,但由另一個實體擁有/管理該套件庫的整個命名空間。
  • 所有 URL 必須代表與擴展套件庫中專案相同的「專案」。
    • 這並不意味著它們需要提供相同的檔案。它們包含在不同平台上建置的二進位檔、套用了本機修補程式的副本等皆是有效的。這被刻意保留模糊,因為最終「相同」專案究竟構成什麼,取決於使用者對該套件庫及其營運者的期望。
  • 必須指向「擁有」命名空間的套件庫,而不是另一個也在追蹤該命名空間的套件庫。
  • 必須指向名稱完全相同(正規化後)的專案。
  • 必須指向該專案的實際 URL,而不是被擴展套件庫的基礎 URL。

要求套件庫中的每個名稱都追蹤相同的套件庫,也不要求它們都追蹤任何套件庫。明確允許使用混合型套件庫,其中部分名稱追蹤套件庫,而部分名稱則不追蹤。

JSON

{
  "meta": {
    "api-version": "1.2",
    "tracks": ["https://pypi.org/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"]
  },
  "name": "holygrail",
  "files": [
    {
      "filename": "holygrail-1.0.tar.gz",
      "url": "https://example.com/files/holygrail-1.0.tar.gz",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "yanked": "Had a vulnerability"
    },
    {
      "filename": "holygrail-1.0-py3-none-any.whl",
      "url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "dist-info-metadata": true
    }
  ]
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta name="pypi:repository-version" content="1.2">
    <meta name="pypi:tracks" content="https://pypi.org/simple/holygrail/">
    <meta name="pypi:tracks" content="https://test.pypi.org/simple/holygrail/">
  </head>
  <body>
    <a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
    <a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
  </body>
</html>

「替代位置 (Alternate Locations)」後設資料

為了使專案能夠跨多個套件庫擴展其命名空間,本 PEP 允許專案擁有者宣告其專案的「替代位置」列表。這在 JSON 中以 alternate-locations 鍵值表示,在 HTML 中則作為名為 pypi-alternate-locations 的 meta 元素(可多次使用)存在。

使用此後設資料時,有幾個關鍵屬性必須被遵守:

  • 為了使此後設資料受到信任,在發現該專案的所有位置之間,必須針對替代位置達成共識。
  • 使用替代位置時,用戶端必須隱式假設回應獲取的 URL 已包含在該列表中。這意味著如果您從 https://pypi.org/simple/foo/ 獲取,且該處具有值為 ["https://example.com/simple/foo/"]alternate-locations 後設資料,則您必須將其視為具有值 ["https://example.com/simple/foo/", "https://pypi.org/simple/foo/"]
  • 陣列內元素的順序沒有任何特定含義。

當安裝程式遇到使用替代位置後設資料的專案時,它應該考慮到所有被命名的套件庫都是在多個套件庫間擴展同一個命名空間。

附註

此替代位置後設資料為專案層級的後設資料,而非成品層級(artifact level),這意味著它不會被包含在核心後設資料規格中,而是每個套件庫(若選擇支援的話)必須為其提供配置選項。

JSON

{
  "meta": {
    "api-version": "1.2"
  },
  "name": "holygrail",
  "alternate-locations": ["https://pypi.org/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"],
  "files": [
    {
      "filename": "holygrail-1.0.tar.gz",
      "url": "https://example.com/files/holygrail-1.0.tar.gz",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "yanked": "Had a vulnerability"
    },
    {
      "filename": "holygrail-1.0-py3-none-any.whl",
      "url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "dist-info-metadata": true
    }
  ]
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta name="pypi:repository-version" content="1.2">
    <meta name="pypi:alternate-locations" content="https://pypi.org/simple/holygrail/">
    <meta name="pypi:alternate-locations" content="https://test.pypi.org/simple/holygrail/">
  </head>
  <body>
    <a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
    <a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
  </body>
</html>

建議

本節為非規範性內容;它為安裝程式提供了如何解釋此後設資料的建議,本 PEP 認為這在預設保護使用者與最小化對現有工作流程的破壞之間提供了最佳的平衡。這些建議不具約束力,安裝程式可自由選擇忽略,或在特定情境下有意義時選擇性地應用它們。

檔案搜尋演算法

附註

此演算法是基於 pip 目前發現檔案的方式編寫的;其他安裝程式可能會根據其自身的發現程序進行調整。

目前「標準」檔案發現演算法大致如下:

  1. 產生所有設定套件庫中所有檔案的列表。
  2. 過濾掉任何與 lockfile 或 requirements 檔案中已知雜湊值不符的檔案。
  3. 過濾掉任何不符合當前平台、Python 版本等的檔案。
  4. 將該檔案列表傳遞至解析器,解析器會嘗試從中解析出「最佳」匹配項,而不考慮其來自哪個套件庫。

建議安裝程式修改其檔案發現演算法以納入此新後設資料,並改為執行:

  1. 產生所有設定套件庫中所有檔案的列表。
  2. 過濾掉任何與 lockfile 或 requirements 檔案中已知雜湊值不符的檔案。
  3. 若終端使用者已明確告知安裝程式從特定套件庫抓取專案,則過濾掉所有其他套件庫並跳至第 5 步。
  4. 檢查發現的檔案是否跨越了多個套件庫;若是,則判斷「追蹤」或「替代位置」後設資料是否允許安全地合併發現檔案的所有套件庫。若該後設資料允許,則產生錯誤;否則,繼續執行。
    • 注意:這僅適用於遠端套件庫;存在於本機檔案系統上的套件庫應該始終被隱式允許與任何遠端套件庫合併。
  5. 過濾掉任何不符合當前平台、Python 版本等的檔案。
  6. 將該檔案列表傳遞至解析器,解析器會嘗試從中解析出「最佳」匹配項,而不考慮其來自哪個套件庫。

這有些細微,但該建議的關鍵在於:

  • 使用包含「有效」成品特定雜湊值的 lockfile 或 requirements 檔案的使用者,預設會受到這些雜湊值的保護,因為其餘這些建議會在產生雜湊值期間應用。因此,我們在前端過濾掉未知雜湊值。
  • 若使用者已明確告知安裝程式希望從特定集合的套件庫中抓取專案,則無需質疑,我們假設他們已確保合併這些命名空間是安全的。
  • 若相關專案僅來自單一套件庫,則不會發生依賴混淆的可能性,因此除了允許執行外,無需採取任何行動。
  • 我們在基於平台、Python 版本等進行過濾之前檢查本 PEP 中的後設資料,因為我們不希望發生僅在特定平台、Python 版本等上才顯示的錯誤。
  • 若沒有任何資訊告訴我們合併命名空間是安全的,我們拒絕隱式地假設它是安全的,並改為產生錯誤。
  • 否則,我們合併命名空間並繼續執行。

此演算法確保安裝程式永遠不會假設兩個不同的命名空間可以被扁平化為一個,這在實際應用中消除了任何形式的依賴混淆攻擊的可能性,同時仍然賦予了整個堆疊強大的安全方式,允許人們在實際為一個可安全合併的邏輯命名空間時,明確宣告這一點。

上述演算法大多是一個概念模型。實際上,演算法最終可能會略有不同,以便更注重隱私保護、執行速度更快,或僅僅是為了更好地適應特定的安裝程式。

終端使用者的明確設定

本 PEP 避免強制規定或建議安裝程式允許終端使用者設定要從哪些特定套件庫安裝特定套件的具體機制。然而,它確實建議安裝程式應提供某種機制供終端使用者進行該設定,因為若無此機制,使用者可能會在 torchtriton 這類案例中陷入拒絕服務(DoS)的情況,除非他們在外部解決命名空間衝突(在一個套件庫中下架該名稱、架設處理合併的個人套件庫等),否則將完全無法運作。

此設定還允許終端使用者在預設行為變得安全之前,預先保護自己,這可能是一段漫長的過渡期。

如何溝通此變更

附註

此範例為 pip 特定,並假設了關於 pip 將如何實作此 PEP 的細節;它僅作為我們如何溝通此變更的範例,而非限制 pip 或任何其他安裝程式的實作方式。這最終可能成為實際溝通的基礎,屆時將需要針對準確性與清晰度進行編輯。

本節應視為一則完整的「公告」,用於溝通此變更,可用於部落格文章、電子郵件或 Discourse 貼文。

有一類長期存在的攻擊稱為「依賴混淆」攻擊,簡而言之,個別使用者預期取得套件 A,但實際上卻取得了 B。在 Python 中,這種情況幾乎總是因為終端使用者配置了多個套件庫,他們預期套件 A 來自套件庫 X,但有人在套件庫 Y 中以與套件 A 相同的名稱發佈了套件 B

目前有多種方法可以減輕這些攻擊,但它們都要求終端使用者明確採取行動來保護自己,而不是天生安全的。

為了確保 pip 使用者的安全並保護他們免受此類攻擊,我們將變更 pip 發現套件安裝的方式。

變更有哪些?

當 pip 發現同一個專案可從多個遠端套件庫取得時,預設情況下它將產生錯誤並拒絕執行,而不是猜測應該從哪個套件庫安裝。

原生發佈到多個套件庫的專案將獲得安全地將其套件庫「連結」在一起的能力,以便在同時使用這些套件庫時,pip 不會報錯。

pip 的終端使用者將獲得針對特定專案明確定義一個或多個有效套件庫的能力,使 pip 僅針對該專案考量這些套件庫,並完全避免產生錯誤。

詳情請參閱 TBD。

誰會受到影響?

對於從多個遠端(例如非本機檔案系統上的)套件庫安裝的使用者,若符合下列情況,可能會因 pip 報錯而導致無法成功安裝:

  • 他們安裝的專案名稱在多個遠端套件庫中都有提供。
  • 在多個遠端套件庫中可用的專案名稱,未使用定義的機制將這些套件庫連結在一起。
  • 呼叫 pip 的使用者未使用定義的機制明確控制哪些套件庫對特定專案有效。

未使用多個遠端套件庫的使用者將完全不受影響,這包括僅使用單一遠端套件庫加上本機檔案系統「wheel house」的使用者。

我需要做什麼?

作為 pip 使用者?

如果您僅使用單一遠端套件庫,則無需採取任何行動。

如果您使用多個遠端套件庫,您可以透過在 pip 呼叫中新增 --use-feature=TBD 來加入新行為,以觀察是否有任何依賴項正從多個遠端套件庫中提供。若有,您應審查它們以確定原因,並決定對您而言最佳的補救步驟。

一旦此行為成為預設值,您可以透過在 pip 呼叫中新增 --use-deprecated=TBD 來暫時退出。

如果您使用的專案未託管在公開套件庫上,但仍將公開套件庫作為後備(fallback),請考慮使用套件庫檔案設定 pip,以明確指出該依賴項應從何處取得,防止該名稱在公開套件庫中被註冊而導致 pip 為您報錯。

作為專案擁有者?

如果您僅將專案發佈到單一套件庫,則無需採取任何行動。

如果您將專案發佈到旨在同時使用的多個套件庫,請設定所有套件庫以提供替代套件庫後設資料,以防止您的終端使用者遇到錯誤。

如果您僅將專案發佈到單一套件庫,但它通常與其他套件庫一起使用,請考慮主動在那些套件庫中註冊您的名稱,以防止第三方導致您的使用者在執行 pip install 時開始失敗。如果您的專案名稱過於通用,或者套件庫有禁止防禦性名稱佔用的政策,此方法可能無法使用。

作為套件庫營運者?

您需要決定您希望終端使用者如何使用您的套件庫,以及您希望他們如何使用它。

對於託管私人專案的私人套件庫,建議您將使用者依賴的公開專案鏡像到您自己的套件庫中,注意不要讓公開專案與私人專案合併,並告訴您的使用者使用 --index-url 選項僅使用您的套件庫。

對於託管公開專案的公開套件庫,您應該實作替代套件庫機制,並允許這些專案的擁有者設定其專案可取得的套件庫列表(若他們選擇讓該專案在多個套件庫中可用)。

對於「追蹤」另一個套件庫,但提供補充成品(例如針對特定平台建置的 wheels)的公開套件庫,您應該為您的套件庫實作「追蹤」後設資料。然而,此資訊絕對不能由正在向您的套件庫發佈專案的終端使用者設定。詳情請參閱 TBD。

否決的想法

注意:其中一些內容對 pip 而言較為特定,但任何對 pip 無法運作的解決方案都不是特別有用的解決方案。

當檔案列表相同時,隱式允許鏡像 (mirrors)

若每個套件庫都回傳完全相同的檔案列表,則將這些套件庫視為相同的命名空間並隱式合併它們是安全的。這可能意味著無需使用者或套件庫營運者採取任何行動,鏡像就會自動被允許。

遺憾的是,這有兩個導致其不理想的缺點:

  • 它僅解決了互為完全副本的鏡像案例,但未能解決「追蹤」另一個套件庫的案例,而後者最終成為一個更通用的解決方案。
  • 即使在完全鏡像的情況下,多個鏡像彼此的套件庫屬於分散式系統,並不總是能完全保持一致,實際上是一個最終一致性系統。這意味著依賴此隱式啟發式運作的套件庫,會因來源套件庫與鏡像套件庫之間的漂移(drift)而產生零星失敗。

提供一種排序套件庫的機制

提供某種機制給套件庫排序,並在發現第一個為該專案提供檔案的套件庫時中斷檔案發現演算法,是另一個可行的解決方案,若順序指定正確,該方案是安全的。

然而,此方案因多個原因被拒絕:

  • 我們花費了 15 年以上的時間教育使用者:指定套件庫的順序是沒有意義的,它們實際上具有未定義的順序。現在要反轉並開始說順序很重要,將會很困難。
  • 使用者可以在單一位置輕鬆重新排列他們指定套件庫的順序,但當從多個位置(環境變數、設定檔、requirements 檔案、命令列參數)載入套件庫時,順序在 pip 中是硬編碼的。雖然這會是一個確定性且有記錄的順序,但沒有理由假設這是使用者希望定義套件庫的順序,這會迫使他們扭曲 pip 的配置方式,最終使隱式排序成為正確的順序。
  • 上述問題可以透過提供一種顯式宣告順序的方法來減輕,而不是隱式使用它們被定義的順序;然而,這意味著除非使用者進行一些明確配置,否則無法提供保護。
  • 排序假設一個套件庫總是優於另一個套件庫,且沒有辦法根據個別專案進行決策。
  • 依賴排序是細微的;如果我觀察一個套件庫的排序,我無法預先知道或確保哪些名稱會來自哪些套件庫。我只能在當下知道哪些套件庫提供了哪些名稱。
  • 依賴排序是脆弱的。沒有理由假設兩個不同的套件庫不會發生隨機的命名衝突——如果我正在使用來自低優先順序套件庫的函式庫,而一個高優先順序的套件庫碰巧開始有了衝突的名稱,會發生什麼事?
  • 在排序產生錯誤結果的情況下,它是靜默地發生的,不會給使用者任何回饋。這是設計使然,因為它實際上並不知道什麼是錯誤或正確的結果,它只是希望順序會產生正確的結果,如果確實如此,使用者則在沒有任何破壞的情況下受到保護。然而,當它產生錯誤結果時,使用者會遇到 pip 帶來的非常令人困惑的行為,即它靜默地安裝了錯誤的內容。

此想法有一種變體,有效地指出真正導致問題的是 PyPI 開放註冊的本質,所以如果我們將除了「預設」以外的所有套件庫視為同等優先順序,然後將預設套件庫視為較低優先順序,那麼我們就能解決問題。

確實,這改善了情況,但它與一般的排序想法有許多相同的問題(儘管並非全部)。

它還假設 PyPI,或任何被配置為「預設」的套件庫,是唯一具有開放名稱註冊的套件庫。然而,像 Piwheels 這樣的專案存在,使用者預期將其與 PyPI 一起使用,它同樣有效地具有開放名稱註冊,因為它追蹤在 PyPI 上註冊的任何名稱。

依賴套件庫代理 (proxies)

一種可能的解決方案是,與其讓安裝程式解決此問題,不如依賴可以智慧且安全地合併多個套件庫的套件庫代理(proxies)。對於有複雜需求的人來說,這可以提供更好的體驗,因為他們可以擁有專門針對該問題空間的配置與功能。

然而,這已被拒絕,因為:

  • 除非我們也移除安裝程式中擁有一個以上套件庫的功能,迫使用戶在需要多個套件庫時使用套件庫代理,否則它要求使用者主動選擇使用它們。
    • 移除配置多個套件庫的功能已被拒絕,因為這對終端使用者來說太具破壞性。
  • 使用者在不同情境下可能需要合併多個套件庫的不同結果,或者可能需要合併不同的、互斥的套件庫。這意味著他們需要為每組獨特的選項實際架設多個套件庫代理。
  • 這要求使用者維護基礎設施,或要求在安裝程式中增加功能,以便在每次呼叫時自動啟動一個套件庫。
  • 它實際上並未改變需要解決這些問題的要求,只是將實作責任從安裝程式轉移到了某個套件庫代理,但無論哪種情況,我們仍然需要某種東西來算出如何合併這些分散的命名空間。
  • 最終,大多數使用者不希望僅為了安全地與多個套件庫互動,就必須架設一個套件庫代理。

僅依賴雜湊值檢查

另一個可能的解決方案是依賴雜湊值檢查,因為在啟用雜湊值檢查的情況下,使用者無法取得他們預期之外的成品;命名空間是否被錯誤合併並不重要。

這確實是一個解決方案;遺憾的是,它同樣存在使其無法實行的問題:

  • 它要求使用者主動選擇使用,因此使用者在預設情況下仍然不受保護。
  • 它要求使用者花費大量勞力來管理他們的雜湊值,這是大多數使用者不太願意做的。
  • 當使用者不使用 requirements.txt 檔案作為其依賴來源時,取得此保護是困難且冗長的(這會影響建置時間依賴項,以及在命令列上提供的依賴項)。
  • 這只是稍微解決了問題,在某種程度上,它只是將問題的責任轉移到任何產生安裝程式將使用的雜湊值的系統上。如果該系統不是由人類手動驗證雜湊值(這不太可能),那麼我們只是將如何合併這些命名空間的問題轉移到了任何實作雜湊值維護的工具上。

要求所有專案必須存在於「預設」套件庫中

另一個想法是我們可以縮小 --extra-index-url 的範圍,使其唯一支援的用途是參考預設套件庫的補充套件庫,實際上是說預設套件庫定義了命名空間,而每個額外的套件庫只是透過額外的套件來擴展它。

其實作大致為:要求該專案必須在預設套件庫中註冊,額外的套件庫才能運作。

若您能以該方式成功縮小範圍,這種方法或許可行,但最終它被拒絕是因為:

  • 使用者不太可能理解或接受這種縮小的範圍,因此很可能會嘗試繼續以現今不受支援的方式使用它。
    • 事實上,隨著範圍縮小,不屬於此工作流程的使用者,除了架設套件庫代理之外別無選擇,這需要他們先前不需要的基礎設施與努力,使得情況更加複雜。
  • 它假設僅僅因為「額外」套件庫中的名稱與預設套件庫中的相同,它們就是同一個專案。如果我們是從零開始建立一個全新的生態系統,或許我們可以從一開始就做出這種假設並使其固化,但要讓生態系統適應這種變更將會極度困難。
    • 這是這種方法的一個根本問題;推動依賴混淆的根本問題在於我們正在將不同的命名空間扁平化為一個。這種方法本質上只是宣稱這沒問題,並試圖透過要求每個人註冊他們的名稱來減輕它。
  • 由於上述假設,在額外套件庫中的名稱與預設套件庫偶然碰撞的情況下,對於那些使用者來說它看起來會運作正常,但他們將靜默地處於依賴混淆的狀態。
    • 更糟的是,那個擁有該名稱的人會完全不知道自己對使用者所扮演的角色,並且可能會刪除他們的專案或將其交給其他人,這可能會使他們無意中允許惡意使用者接管該名稱。
  • 使用者很可能會嘗試透過在他們的預設套件庫中註冊名稱作為防禦性名稱佔用(defensive name squat)來恢復到正常運作狀態。他們這樣做的能力將取決於其預設套件庫的特定政策、是否已經有人擁有該名稱、它是否太通用等。在最好的情況下,這會導致產生不必要的佔位專案,除了保護某些名稱的內部使用外毫無用途。

遷移至全域唯一名稱

這個問題之所以存在,主要原因是我們沒有全域唯一的名稱,我們有存在於多個命名空間下的本機唯一名稱,而我們正試圖將其合併為一個單一的扁平命名空間。如果我們能想出一種方法來擁有全域唯一的名稱,我們就能繞過整個問題。

這個想法被拒絕是因為:

  • 在不依附於某種集中式資料庫的情況下,產生全域唯一且安全,同時對人類又有意義的名稱,幾乎是一項不可能的任務。據我所知,唯一成功做到這一點的系統,最後都是依附於網域系統,並透過帶有網域等的 URL 來參考套件。
  • 即使我們想出了一種取得全域唯一名稱的機制,我們將其改裝到我們數十年歷史的系統中的能力幾乎為零,除非將其完全摧毀並重新開始。我們可能頂多做到宣告所有非全域唯一名稱隱式地是在 PyPI 網域名稱下的名稱,並強制每個非 PyPI 套件的所有者重新命名他們的套件。
  • 這將顛覆我們當前系統中太多的核心假設與基本部分,很難知道從何處開始列舉它們。

僅建議安裝程式提供明確設定

有一個想法是被提出來的,基本上只是實作明確配置,而不對其他任何東西做任何變更。關於對映政策(mapping policy)的具體提案正是激發明確配置選項的靈感來源,並建立了一個看起來類似這樣的檔案:

{
  "repositories": {
    "PyTorch": ["https://download.pytorch.org/whl/nightly"],
    "PyPI": ["https://pypi.org/simple"]
  },
  "mapping": [
    {
      "paths": ["torch*"],
      "repositories": ["PyTorch"],
      "terminating": true
    },
    {
      "paths": ["*"],
      "repositories": ["PyPI"]
    }
  ]
}

擁有明確配置的建議將如何實作的決策推給了每個安裝程式,允許它們選擇最適合其使用者的做法。

最終,僅實作某種明確配置是被拒絕的,因為從本質上講,它是需要主動選擇的(opt in),因此它無法保護最無力使用現有工具解決問題的普通使用者;透過在明確配置的同時增加額外的保護,我們能夠在預設情況下保護所有使用者。

此外,僅依賴明確配置也意味著每位終端使用者都必須一遍又一遍地解決同樣的問題,即使是在 PyPI 鏡像、Piwheels、PyTorch 等案例中也是如此。在每一個案例中,他們都必須坐在那裡做出決定(或尋找某個範例來模仿),才能達到安全。在組合中增加額外的功能,使我們能夠在可能的地方集中這些保護,同時仍然讓高級終端使用者能夠完全控制自己的命運。

類似 npm 的作用域 (Scopes)

有人建議 類似 npm 實作的作用域 (scopes) 可能最終解決這個問題。最終,作用域並未改變關於此問題的任何內容。據我所知,npm 中的作用域並非全域唯一,它們與無作用域名稱一樣,都是綁定到特定的註冊中心(registry)。然而,作用域確實賦予了將相關專案分組的明確機制,以及 npm.org 上的使用者或組織宣告整個作用域的能力,這使得處理明確配置變得容易得多,因為您可以確信有一整個命名空間區塊完全屬於您,並且您可以輕鬆編寫規則,將整個作用域指定給特定的非公開註冊中心。

遺憾的是,它基本上變成了一個較容易版本的「僅使用明確配置」的想法,這在 npm 中運作得還可以,因為人們使用自己的註冊中心並不常見,但在 Python 中,我們鼓勵您這麼做。

定義並標準化「明確設定」

本 PEP 建議安裝程式具備明確配置特定專案來自哪個套件庫的機制,但它並未定義該機制是什麼。我們刻意保留未定義狀態,因為它與每個個別安裝程式的 UX 密切相關,我們希望允許每個個別安裝程式能夠以他們認為適合其特定用例的方式來揭露該配置。

此外,當定義該機制的想法出現時,沒有其他安裝程式似乎特別有興趣為它們定義該機制,這表明它們很高興將其視為其 UX 的一部分。

最後,如果我們確實選擇定義該機制,它值得擁有自己的 PEP,而不是將其作為本 PEP 中套件庫 API 變更的一部分,如果我們最終決定確實想要走標準化之路,這可以成為未來的 PEP。

致謝

感謝 Trishank Kuppusamy 發起導致本 PEP 的討論,即他的 提案

感謝 Paul Moore、Pradyun Gedam、Steve Dower 與 Trishank Kuppusamy 為本 PEP 中的想法提供早期回饋與討論。

感謝 Jelle Zijlstra、C.A.M. Gerlach、Hugo van Kemenade 與 Stefano Rivera 對本 PEP 進行文案編輯並改進其結構與品質。


來源:https://github.com/python/peps/blob/main/peps/pep-0708.rst

最後修改:2025-02-01 08:55:40 GMT