天天看點

npm 前員工自曝生态内部存在嚴重 bug | 附避坑指南

作者:InfoQ

作者 | Darcy Clarke

譯者 | 核子可樂

策劃 | 丁曉昀

最近,npm 前工程經理 Darcy Clarke 在一份報告中指出,npm 注冊沒有根據相應 tarball 包的内容驗證清單資訊。Clarke 說,這會導緻雙重事實來源,攻擊者可以利用它來隐藏腳本或依賴項。

這一點影響很大。例如,npm 上有個包可能會顯示它沒有依賴項,而實際上它有。同樣,它顯示的包名或版本可能與 package.json 中的不同,而這可能會導緻緩存中毒。更糟糕的是,它可以隐藏它将在安裝期間運作腳本的事實。

在接受 InfoQ 采訪時,Sonatype 安全研究員 Ax Sharma 強調,這種不一緻不一定是惡意的,可能是源于合法的克隆或分叉,或者是由于開發人員在更新包時沒有清理過時的中繼資料。他還提出了一點小小的異議:

相信 package.json 并不一定比相信包的 npmjs 頁面更好——兩者都不是完全可靠的。

根據 Sharma 的說法,要解決這個問題需要借助安全工具進行更深入的分析,例如,對惡意檔案或受到攻擊的檔案進行基于散列的分析,即進階二進制指紋。

另一個有用的建議來自 J. M. Rossy 的推特,他建議預設關閉腳本。

如果你對這個清單之惑感興趣,請閱讀 Clarke 的原文,其中有許多其他的見解。

以下為原文翻譯。

簡單自我介紹,2019 年 7 月至 2022 年 12 月期間,我負責 npm CLI 團隊的工程管理。2020 年我參與了 GitHub 收購 npm 項目.。2022 年 12 月,我因各種原因離開了 GitHub。

如今,各類新興供應鍊攻擊可謂層出不窮,而本文要向大家分享的則是其中一例——我個人稱之為“manifest 混淆”(manifest confusion)。

故事背景

在 Node 生态系統發展到如今全球使用者達數千萬、建立超過 310 萬個軟體包、月下載下傳量高達 2080 億次的規模之前,當初該項目的貢獻者數量曾非常有限。當然,社群越小,大家就越感覺安心,畢竟沒有哪個黑客團隊會找這麼“瘦”的目标下手。但随着時間推移,npm 系統資料庫被逐漸開發出來,人們可以免費貢獻并檢查其中的開源代碼,語料庫的組織政策和實踐也迎來同步發展。

從誕生之初,npm 項目就非常信任系統資料庫的用戶端與伺服器端。現在回想起來,這種高度依賴用戶端來處理資料驗證的作法真的很有問題。但也正是憑借這項政策,是讓 JavaScript 工具生态得以快速成長并在資料形态中有所展現。

發生甚麼事了?

npm 公共系統資料庫不會使用包 tarball 中的内容來驗證 manifest 資訊,反而是依賴 npm 相容的用戶端進行解釋和強制驗證/一緻性。事實上,在研究這個問題時,我發現伺服器似乎從未承擔過驗證任務。

如今,registry.npmjs.com 允許使用者通過 PUT 請求将軟體包釋出至相應的包 URI,例如:

https://registry.npmjs.com/-/<package-name>。

該端點會接收一條請求 body,内容如下所示(請注意:在經曆近 15 年的發展之前,如今的 npm 及其他系統資料庫 API 仍然嚴重缺乏記錄資訊):

{
  _id: <pkg>,
  name: <pkg>,
  'dist-tags': { ... },
  versions: {
    '<version>': {
      _id: '<pkg>@<version>`,
      name: '<pkg>',
      version: '<version>',
      dist: {
        integrity: '<tarball-sha512-hash>',
        shasum: '<tarball-sha1-hash>',
        tarball: ''
      }
      ...
    }
  },
  _attachments: {
    0: {
      content_type: 'application/octet-stream',
      data: '<tarball-base64-string>',
      length: '<tarball-length>'
    }
  }
}           

目前的問題是,version 中繼資料(也就是「manifest」資料)是獨立于存放有軟體包 package.json 的 tarball 而獨立送出的。這兩部分資訊之間從未進行過互相驗證,而且我們往往搞不清依賴項、腳本、許可證等資料的“權威事實來源”究竟是誰。據我所知,tarball 才是唯一擁有簽名,且有着可離線存儲及驗證的完整性值的工件。從這些角度看,它應該才是正确的來源;但令人意外的是,package.json 當中的 name & version 字段實際上很可能與 manifest 中的字段不同,因為二者間不會進行互相驗證。

示例

  1. 在 npmjs.com 上生成身份驗證令牌(例如: https://www.npmjs.com/settings/<your-username>/tokens/new - 選擇 "Automation" 以友善測試)
  2. 啟動一個新項目 (例如: mkdir test && cd test/ && npm init -y)
  3. 安裝 helper 庫(例如:npm install ssri libnpmpack npm-registry-fetch)
  4. 建立一個子目錄,作為“真實”的軟體包及内容(例如 mkdir pkg && cd pkg/ && npm init -y)
  5. 修改該包的内容……
  6. 在項目根目錄中建立一個 publish.js 檔案,内容如下:
;(async () => {
  // libs
  const ssri = require('ssri')
  const pack = require('libnpmpack')
  const fetch = require('npm-registry-fetch')




  // pack tarball & generate ingetrity
  const tarball = await pack('./pkg/')
  const integrity = ssri.fromData(tarball, {
    algorithms: [...new Set(['sha1', 'sha512'])],
  })




  // craft manifest
  const name = '<pkg name>'
  const version = '<pkg version>'
  const manifest = {
    _id: name,
    name: name,
    'dist-tags': {
      latest: version,
    },
    versions: {
      [version]: {
        _id: `${name}@${version}`,
        name,
        version,
        dist: {
          integrity: integrity.sha512[0].toString(),
          shasum: integrity.sha1[0].hexDigest(),
          tarball: '',
        },
        scripts: {},
        dependencies: {},
      },
    },
    _attachments: {
      0: {
        content_type: 'application/octet-stream',
        data: tarball.toString('base64'),
        length: tarball.length,
      },
    },
  }




  // publish via PUT
  fetch(name, {
    '//registry.npmjs.org/:_authToken': '<auth token>',
    method: 'PUT',
    body: manifest,
  })
})()           
  1. 可随意修改其中 manifest 鍵(例如,我在這裡去掉了 scripts & dependencies );
  2. 運作程式(例如: node publish.js);
  3. 導航至 https://registry.npmjs.com/<pkg>/ & https://www.npmjs.com/package/<pkg>/v/<version>?activeTab=explore 以檢視差異。
npm 前員工自曝生态内部存在嚴重 bug | 附避坑指南

以上示例中的軟體包是用不同 manifest 釋出的,其各有對應的 package.json,請參考:

https://www.npmjs.com/darcyclarke-manifest-pkg

https://registry.npmjs.com/darcyclarke-manifest-pkg/

Bug, bug, 到處是 bug

如果大家想用更簡單的辦法重制這種不一緻性,現在也可以使用 npm CLI。一旦在項目中發現 binding.gyp 檔案,它就會在 npm 釋出期間改變 manifest 内容。這種行為似乎在我加入 npm 團隊之前(即 6.x 或更早版本)就已經存在于用戶端内,而且已經給衆多使用者惹出了不少麻煩。

  1. npm init -y
  2. touch binding.gyp
  3. npm publish
  4. 可以看到, "node-gyp rebuild" scripts.install 條目已被自動添加至 manifest 當中,但卻未被添加至 tarball 的 package.json 當中。例如:
  • https://registry.npmjs.com/darcyclarke-binding
  • https://unpkg.com/[email protected]/package.json

這種不一緻現象在 node-canvas 中經常出現:

  • https://www.npmjs.com/package/node-canvas/v/2.9.0?activeTab=explore
  • https://registry.npmjs.com/node-canvas/2.9.0
  • https://github.com/npm/cli/issues/5234

相關影響

這個 bug 可能會以多種方式影響消費者/最終使用者:

  1. 緩存中毒(即儲存的包可能與系統資料庫/URI 中的名稱+版本規格不比對;
  2. 安裝未知/未列出的依賴項(欺騙安全/審計工具);
  3. 安裝未知/未列出的腳本(欺騙安全/審計工具);
  4. 引發潛在降級攻擊(儲存到項目中的版本規格,為不符合要求/易受攻擊的包版本)。

已知受到影響的第三方組織/實體:

  • Snyk: https://security.snyk.io/package/npm/darcyclarke-manifest-pkg
  • CNPMJS/Chinese Mirror: https://npmmirror.com/package/darcyclarke-manifest-pkg
  • Cloudflare Mirror: https://registry.npmjs.cf/darcyclarke-manifest-pkg/2.1.15
  • Skypack: https://cdn.skypack.dev/-/[email protected]
  • UNPKG: https://unpkg.com/[email protected]/package.json
  • JSPM: https://ga.jspm.io/npm:[email protected]/package.json
  • Yarn: https://yarnpkg.com/package/darcyclarke-manifest-pkg
更新:前文提到,Socket Security 易受到 manifest 混淆問題的影響。自 2022 年 9 月 5 日起,Socket 方面已開始使用 tarball 内的 package.json 檔案作為事實來源,且要求顯示包的準确資訊(例如依賴項、許可證、腳本等)。截至本文釋出時,darcyclarke0-manifest-pkg 的軟體包頁面錯誤地引用了過時的資料,但 Socket 團隊很快解決了這個問題。這裡要稱贊一聲,Socket 可能是首個正确處理此問題的項目團隊。

此問題還會以下面介紹的幾種方式,影響到所有已知的主要 JavaScript 包管理器。jFrog 的 Artifacory 等第三方系統資料庫實作似乎也繼承了該 API 的設計/問題,是以使用這些私有系統資料庫執行個體的所有用戶端也會出現相同的問題/不一緻。

注意,各類包管理器和工具對應不同的應用場景。它們要麼使用/引用軟體包的系統資料庫 manifest,要麼使用/引用 tarball 的 package.json(主要是為了通過緩存機制提高安裝性能)。

這裡需要強調的是,生态系統目前仍普遍存在錯誤假設,即 manifest 的内容始終與 tarball 的 package.json 内容一緻(這主要是因為系統資料庫 API 說明文檔過少,且 docs.npmjs.com 多次提到系統資料庫會将 package.json 的内容存儲為中繼資料——但卻沒有強調其實是由用戶端負責確定一緻性)。

npm@6

執行 manifest/tarball 中不存在的腳本

重制步驟:

  1. 安裝一個格式經過篡改的依賴項: npx npm@6 install [email protected]
  2. See that lifecycle scripts are being executed even though none are present in the manifest & the registry has not registered the package as having install script (ie.可以看到,雖然在 manifest 中并不存在/系統資料庫尚未将包注冊為具有安裝腳本,但生命周期腳本仍在執行(即 hasInstallScript 為 undefined/false) 參考:

https://registry.npmjs.org/darcyclarke-manifest-pkg/2.1.13

代碼/包請參考:

https://github.com/npm/minify-registry-metadata/blob/main/lib/index.js

node_modules/darcyclarke-manifest-pkg 當中的 package.json 反映 tarball 條目。

npm 前員工自曝生态内部存在嚴重 bug | 附避坑指南

安裝 manifest/tarball 中不存在的依賴項

由于包 tarball 會被緩存在全局存儲當中,是以如果--prefer-offline 配置與--no-package-lock 共同使用,則下一次在系統中對該包運作 install 時,隐藏在 tarball 中的依賴項也會被安裝。

重制步驟:

  1. 安裝 npx npm@6 install [email protected]
  2. 再次運作安裝… npx npm@6 install --prefer-offline --no-package-lock
npm 前員工自曝生态内部存在嚴重 bug | 附避坑指南

npm@9

安裝 manifest/tarball 中不存在的依賴項

與 npm@6,類似,npm@9 在使用--offline 配置時也會直接安裝經過緩存的 tarball package.json 當中引用的依賴項。

注意:其中似乎存在争用條件,即--offline 可能會/可能不會被從緩存内提取,是以重制結果并不穩定。

重制步驟:

  1. 安裝格式經過篡改的依賴項以将其緩存;
  2. 在安裝時使用--offline 配置并/或關閉可用網絡(例如: npm install --offline --no-package-lock)
  3. 可以看到,manifest 中并未引用的依賴項也會被安裝。

yarn@1

執行 manifest/tarball 中不存在的安裝腳本

與 npm@6 & npm@9 類似,yarn@1 會 tarball 中引用、但 manifest 并未引用的腳本,反之亦然。

npm 前員工自曝生态内部存在嚴重 bug | 附避坑指南

使用 tarball 中的 version 字段——暴露潛在降級攻擊向量

現在大家已經了解,tarball 的内容定義可以與 manifest 有所不同;在這種情況下,yarn@1 順理成章地在更新/降級之後,再把錯誤版本儲存回目前項目的 package.json 當中(可能令使用者在後續安裝中遭受降級攻擊)。

npm 前員工自曝生态内部存在嚴重 bug | 附避坑指南

pnpm@7

執行 manifest/tarball 中不存在的安裝腳本

重制步驟:

與之前幾個案例類似,pnpm 會運作 tarball 中存在、但 manifest 并未引用的腳本,反之亦然。

npm 前員工自曝生态内部存在嚴重 bug | 附避坑指南

CWE 分類/細分

此漏洞可能涉及多種 CWE 分類。至少如果我們把此問題視為“特例”,則以上情況應該被歸納為“服務端安全的用戶端實施”(即 CWE-602——但我嚴重懷疑這種判斷并不适用。我在下文中會具體分析各種問題及其相應 CWE 分類,且分别提供參考代碼)。

  • CWE-602: 服務端安全的用戶端實施長期以來,我們一直嚴重依賴用戶端(即 npm CLI)完成本應在伺服器端完成的工作;代碼參考: https://github.com/npm/cli/blob/latest/workspaces/libnpmpublish/lib/publish.js#L63
  • CWE-94: 代碼生成控制不當(「代碼注入」)這會影響全體使用者(包括 npm 等包管理器); 如下所述,這會導緻其出現各種問題
  • CWE-295: 證書生成不當即使内容(包括 name, version, dependencies, license, scripts 等)與相關系統資料庫索引有所不同,tarball 仍可獲得簽名并被賦予完整性值
  • CWE-325: 缺少加密步驟manifest 資料未經簽名,是以無法離線緩存或驗證;缺少與 tarball 的 package.json 及包 manifest 相符的資料子集的哈希值/驗證;
  • CWE-656: 依賴建構于封閉的安全性由于缺乏關于系統資料庫 API 的說明文檔,是以很難發現該問題。

GitHub 為此做了哪些努力?

據我所知,GitHub 大概在 2022 年 11 月 4 日左右發現了這個問題;經過獨立研究之後,我認為這個問題的潛在影響/風險要比最初的判斷大得多,是以于 3 月 9 日送出了一份包含個人發現的 HackerOne 報告。3 月 21 日,GitHub 關閉了該工單,表示他們正在“内部”處理這個問題。據我了解,之後 GitHub 沒有取得任何 重大進展,也沒有公開釋出這個問題。相反,他們在過去半年間逐漸放棄了 npm 的産品地位,且拒絕更新或提供關于補救措施的相關說明。

可行的解決方案

GitHub 正陷入不可逆轉的困境。事實上,npmjs.com 就是在這樣的狀态下運作了十餘年,意味着目前的安全狀況已經被深深嵌入代碼當中,再難實作廣泛修複。如前所述,npm CLI 本身也依賴于這種設計,而且目前還可能存在其他非惡意用途。

  • 該做點什麼……
  • 應開展進一步調查,确定系統資料庫内受影響條目的具體範圍,這将有助于确定濫用情況。
  • 如果差異量不太大(但考慮到目前 manifest 變體的可觀規模,這種可能性恐怕很低),那麼也許可以根據 tarball 的 package.json 重新生成包含差異的 manifest。
  • 從現在起,根據研究/發現對 manifest 中高權限/已知密鑰強制執行/驗證。
  • 盡快将 npm 公共系統資料庫 API 及其相應的請求/響應對象記錄下來。

使用者能做點什麼?

與認識的任何使用 npm 系統資料庫 manifest 資料的已知工具作者/維護者聯系,確定他們知情并想辦法在适當時轉而使用包内容作為中繼資料(即除了 name & version 之外的所有内容)。另外,請從現在起嚴格執行/驗證系統資料庫代理的一緻性。

原文連結:

相關閱讀:

InfoQ 寫作社群-專業技術部落格社群

NPM 實用指令與快捷方式_JavaScript_SEAL安全_InfoQ寫作社群

Npm, Inc.釋出Npm Pro,面向獨立JavaScript開發人員_大前端_Bruno Couriol_InfoQ精選文章

本文轉載來源:

https://www.infoq.cn/article/NhSzg8cH4eHfWMppRDV9

繼續閱讀