安全最佳實踐

意圖

本文件旨在擴充套件當前的威脅模型,並提供有關如何保護 Node.js 應用程式的廣泛指南。

文件內容

  • 最佳實踐:一種檢視最佳實踐的簡化凝練方式。我們可以使用此議題此指南作為起點。需要注意的是,本文件特定於 Node.js,如果您正在尋找更廣泛的內容,請考慮 OSSF 最佳實踐
  • 攻擊解釋:用通俗易懂的英語和一些程式碼示例(如果可能)來說明和記錄我們在威脅模型中提到的攻擊。
  • 第三方庫:定義威脅(拼寫錯誤攻擊、惡意軟體包等)以及有關 node 模組依賴項等的最佳實踐。

威脅列表

HTTP 伺服器的拒絕服務攻擊 (CWE-400)

這是一種攻擊,由於應用程式處理傳入 HTTP 請求的方式,導致其無法實現設計目的。這些請求不一定由惡意行為者蓄意構造:配置錯誤或有缺陷的客戶端也可能向伺服器傳送請求模式,從而導致拒絕服務。

HTTP 請求由 Node.js HTTP 伺服器接收,並透過註冊的請求處理程式移交給應用程式程式碼。伺服器不解析請求正文的內容。因此,任何由請求正文內容移交給請求處理程式後引起的 DoS 都不屬於 Node.js 本身的漏洞,因為正確處理它是應用程式程式碼的責任。

確保 WebServer 正確處理套接字錯誤,例如,當伺服器建立時沒有錯誤處理程式,它將容易受到 DoS 攻擊。

const  = ('node:net');

const  = .(function () {
  // socket.on('error', console.error) // this prevents the server to crash
  .('Echo server\r\n');
  .();
});

.(5000, '0.0.0.0');

如果執行了惡意請求,伺服器可能會崩潰。

一個非由請求內容引起的 DoS 攻擊示例是慢速攻擊 (Slowloris)。在這種攻擊中,HTTP 請求被緩慢且分片地傳送,一次一個片段。在完整請求送達之前,伺服器將為正在進行的請求保留資源。如果同時傳送足夠多的此類請求,併發連線數很快就會達到最大值,從而導致拒絕服務。這就是攻擊不依賴於請求內容,而依賴於傳送到伺服器的請求的時機和模式的原因。

緩解措施

  • 使用反向代理來接收請求並將其轉發到 Node.js 應用程式。反向代理可以提供快取、負載均衡、IP 黑名單等功能,從而降低 DoS 攻擊成功的可能性。
  • 正確配置伺服器超時,以便可以丟棄空閒或請求到達過慢的連線。請參閱 http.Server 中的不同超時設定,特別是 headersTimeoutrequestTimeouttimeoutkeepAliveTimeout
  • 限制每個主機和總的開放套接字數量。請參閱 http 文件,特別是 agent.maxSocketsagent.maxTotalSocketsagent.maxFreeSocketsserver.maxRequestsPerSocket

DNS 重繫結攻擊 (CWE-346)

這是一種可以針對使用 --inspect 開關啟用除錯檢查器的 Node.js 應用程式的攻擊。

由於在 Web 瀏覽器中開啟的網站可以發出 WebSocket 和 HTTP 請求,因此它們可以瞄準本地執行的除錯檢查器。這通常由現代瀏覽器實施的同源策略來防止,該策略禁止指令碼訪問來自不同源的資源(意味著惡意網站無法讀取從本地 IP 地址請求的資料)。

然而,透過 DNS 重繫結,攻擊者可以暫時控制其請求的源,使其看起來源自本地 IP 地址。這是透過同時控制一個網站和用於解析其 IP 地址的 DNS 伺服器來完成的。更多詳情請參閱DNS 重繫結維基百科

緩解措施

  • 透過附加一個 process.on(‘SIGUSR1’, …) 監聽器,在 SIGUSR1 訊號上停用檢查器。
  • 不要在生產環境中執行檢查器協議。

向未經授權的行為者洩露敏感資訊 (CWE-552)

在包釋出期間,當前目錄中包含的所有檔案和資料夾都會被推送到 npm 登錄檔。

有一些機制可以透過使用 .npmignore.gitignore 定義黑名單或在 package.json 中定義白名單來控制此行為。

緩解措施

  • 使用 npm publish --dry-run 列出所有要釋出的檔案。確保在釋出包之前審查內容。
  • 建立和維護忽略檔案(如 .gitignore.npmignore)也很重要。透過這些檔案,您可以指定哪些檔案/資料夾不應被髮布。package.json 中的 files 屬性允許反向操作——即白名單。
  • 如果發生洩露,請確保取消釋出該包

HTTP 請求走私 (CWE-444)

這是一種涉及兩個 HTTP 伺服器(通常是一個代理和一個 Node.js 應用程式)的攻擊。客戶端傳送一個 HTTP 請求,該請求首先透過前端伺服器(代理),然後重定向到後端伺服器(應用程式)。當前端和後端以不同方式解釋模糊的 HTTP 請求時,攻擊者就有可能傳送一個前端看不到但後端會看到惡意訊息,從而有效地“走私”過代理伺服器。

有關更詳細的描述和示例,請參閱 CWE-444

由於此攻擊取決於 Node.js 對 HTTP 請求的解釋與(任意)HTTP 伺服器不同,因此一次成功的攻擊可能是由於 Node.js、前端伺服器或兩者都存在漏洞。如果 Node.js 解釋請求的方式與 HTTP 規範(見 RFC7230)一致,則不將其視為 Node.js 的漏洞。

緩解措施

  • 建立 HTTP 伺服器時不要使用 insecureHTTPParser 選項。
  • 配置前端伺服器以規範化模糊請求。
  • 持續監控 Node.js 和所選前端伺服器中的新 HTTP 請求走私漏洞。
  • 儘可能端到端使用 HTTP/2 並停用 HTTP 降級。

透過時序攻擊洩露資訊 (CWE-208)

這是一種攻擊,允許攻擊者透過例如測量應用程式響應請求所需的時間來獲取潛在的敏感資訊。這種攻擊並非特定於 Node.js,幾乎可以針對所有執行時。

只要應用程式在時間敏感的操作(例如分支)中使用金鑰,這種攻擊就可能發生。考慮一個典型應用程式中的身份驗證處理。在這裡,基本的身份驗證方法包括電子郵件和密碼作為憑據。使用者資訊從使用者提供的輸入中檢索,理想情況下是從 DBMS 中檢索。檢索到使用者資訊後,將密碼與從資料庫檢索到的使用者資訊進行比較。使用內建的字串比較對於相同長度的值會花費更長的時間。這種比較在執行可接受的次數時,會不情願地增加請求的響應時間。透過比較請求響應時間,攻擊者可以在大量請求中猜測密碼的長度和值。

緩解措施

  • crypto API 公開了一個函式 timingSafeEqual,用於使用恆定時間演算法比較實際和預期的敏感值。

  • 對於密碼比較,您可以使用原生 crypto 模組中也提供的 scrypt

  • 更一般地說,避免在可變時間操作中使用金鑰。這包括對金鑰進行分支,以及當攻擊者可能位於相同基礎設施上(例如,同一臺雲主機)時,使用金鑰作為記憶體索引。用 JavaScript 編寫恆定時間的程式碼很困難(部分原因是 JIT)。對於加密應用,請使用內建的加密 API 或 WebAssembly(對於未在原生中實現的演算法)。

惡意第三方模組 (CWE-1357)

目前,在 Node.js 中,任何包都可以訪問強大的資源,例如網路訪問。此外,由於它們也可以訪問檔案系統,它們可以向任何地方傳送任何資料。

在 node 程序中執行的所有程式碼都能夠透過使用 eval()(或其等效項)來載入和執行額外的任意程式碼。所有具有檔案系統寫訪問許可權的程式碼都可以透過寫入將被載入的新檔案或現有檔案來實現相同的目的。

Node.js 有一個實驗性的¹ 策略機制,用於將載入的資源宣告為不受信任或受信任。但是,此策略預設未啟用。請確保鎖定依賴項版本,並使用常見的工作流或 npm 指令碼執行自動漏洞檢查。在安裝包之前,請確保該包是受維護的,幷包含您期望的所有內容。請注意,GitHub 原始碼並不總是與已釋出的相同,請在 node_modules 中進行驗證。

供應鏈攻擊

Node.js 應用程式的供應鏈攻擊發生在其某個依賴項(直接或間接)被攻破時。這可能是由於應用程式對依賴項的規範過於寬鬆(允許不必要的更新)和/或規範中的常見拼寫錯誤(易受域名搶注攻擊)所致。

控制上游包的攻擊者可以釋出一個包含惡意程式碼的新版本。如果 Node.js 應用程式依賴於該包,而沒有嚴格規定哪個版本是安全的,那麼該包可能會自動更新到最新的惡意版本,從而危及應用程式。

package.json 檔案中指定的依賴項可以有確切的版本號或一個範圍。然而,當將依賴項固定到確切版本時,其間接依賴項本身並未被固定。這仍然使應用程式容易受到不必要/意外更新的攻擊。

可能的攻擊向量

  • 域名搶注攻擊
  • 鎖檔案投毒
  • 被攻破的維護者
  • 惡意軟體包
  • 依賴混淆

緩解措施

  • 使用 --ignore-scripts 防止 npm 執行任意指令碼。
    • 此外,您可以使用 npm config set ignore-scripts true 在全域性範圍內停用它。
  • 將依賴項版本固定到特定的不可變版本,而不是一個範圍或來自可變源的版本。
  • 使用鎖檔案,它會固定每個依賴項(直接和間接)。
  • 使用 CI 自動化新漏洞的檢查,使用諸如 npm-audit 之類的工具。
    • 諸如 Socket 之類的工具可用於透過靜態分析來分析包,以發現諸如網路或檔案系統訪問之類的風險行為。
  • 使用 npm ci 而不是 npm install。這會強制執行鎖檔案,因此鎖檔案和 package.json 檔案之間的不一致會導致錯誤(而不是靜默地忽略鎖檔案而偏向 package.json)。
  • 仔細檢查 package.json 檔案中依賴項名稱的錯誤/拼寫錯誤。

記憶體訪問衝突 (CWE-284)

基於記憶體或基於堆的攻擊依賴於記憶體管理錯誤和可利用的記憶體分配器的組合。與所有執行時一樣,如果您的專案在共享機器上執行,Node.js 也容易受到這些攻擊。使用安全堆有助於防止由於指標上溢和下溢而導致的敏感資訊洩漏。

不幸的是,安全堆在 Windows 上不可用。更多資訊可以在 Node.js secure-heap 文件中找到。

緩解措施

  • 根據您的應用程式使用 --secure-heap=n,其中 n 是分配的最大位元組大小。
  • 不要在共享機器上執行您的生產應用。

猴子補丁 (CWE-349)

猴子補丁是指在執行時修改屬性以改變現有行為。示例

.. = function () {
  // overriding the global [].push
};

緩解措施

--frozen-intrinsics 標誌啟用了實驗性的¹ 凍結內在函式,這意味著所有內建的 JavaScript 物件和函式都被遞迴凍結。因此,以下程式碼片段不會覆蓋 Array.prototype.push 的預設行為

.. = function () {
  // overriding the global [].push
};

// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object ''

然而,需要注意的是,您仍然可以使用 globalThis 定義新的全域性變數並替換現有的全域性變數。

> globalThis.foo = 3; foo; // you can still define new globals
3
> globalThis.Array = 4; Array; // However, you can also replace existing globals
4

因此,可以使用 Object.freeze(globalThis) 來保證不會有全域性變數被替換。

原型汙染攻擊 (CWE-1321)

原型汙染是指透過濫用 __proto_、_constructorprototype 以及從內建原型繼承的其他屬性,來修改或注入屬性到 Javascript 語言項中的可能性。

const  = { : 1, : 2 };
const  = .('{"__proto__": { "polluted": true}}');

const  = .({}, , );
.(.polluted); // true

// Potential DoS
const  = .('{"__proto__": null}');
const  = .(, );
.hasOwnProperty('b'); // Uncaught TypeError: d.hasOwnProperty is not a function

這是從 JavaScript 語言繼承的潛在漏洞。

示例:

緩解措施

  • 避免不安全的遞迴合併,請參閱 CVE-2018-16487
  • 對外部/不受信任的請求實施 JSON Schema 驗證。
  • 使用 Object.create(null) 建立沒有原型的物件。
  • 凍結原型:Object.freeze(MyObject.prototype)
  • 使用 --disable-proto 標誌停用 Object.prototype.__proto__ 屬性。
  • 使用 Object.hasOwn(obj, keyFromObj) 檢查屬性是否直接存在於物件上,而不是從原型繼承。
  • 避免使用來自 Object.prototype 的方法。

不受控制的搜尋路徑元素 (CWE-427)

Node.js 遵循模組解析演算法來載入模組。因此,它假定請求(require)模組所在的目錄是受信任的。

這意味著以下應用程式行為是預期的。假設有以下目錄結構

  • app/
    • server.js
    • auth.js
    • auth

如果 server.js 使用 require('./auth'),它將遵循模組解析演算法並載入 auth 目錄而不是 auth.js 檔案。

緩解措施

使用帶有完整性檢查的實驗性¹ 策略機制可以避免上述威脅。對於上述目錄,可以使用以下 policy.json

{
  "resources": {
    "./app/auth.js": {
      "integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
    },
    "./app/server.js": {
      "dependencies": {
        "./auth": "./app/auth.js"
      },
      "integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
    }
  }
}

因此,當請求 auth 模組時,系統將驗證其完整性,如果不匹配預期的完整性,則會丟擲錯誤。

» node --experimental-policy=policy.json app/server.js
node:internal/policy/sri:65
      throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
      ^

SyntaxError [ERR_SRI_PARSE]: Subresource Integrity string "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" had an unexpected "%" at position 51
    at new NodeError (node:internal/errors:393:5)
    at Object.parse (node:internal/policy/sri:65:13)
    at processEntry (node:internal/policy/manifest:581:38)
    at Manifest.assertIntegrity (node:internal/policy/manifest:588:32)
    at Module._compile (node:internal/modules/cjs/loader:1119:21)
    at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
    at Module.load (node:internal/modules/cjs/loader:1037:32)
    at Module._load (node:internal/modules/cjs/loader:878:12)
    at Module.require (node:internal/modules/cjs/loader:1061:19)
    at require (node:internal/modules/cjs/helpers:99:18) {
  code: 'ERR_SRI_PARSE'
}

注意,始終建議使用 --policy-integrity 來避免策略突變。

生產環境中的實驗性功能

不建議在生產環境中使用實驗性功能。實驗性功能可能會在需要時發生重大更改,並且其功能不夠安全穩定。儘管如此,我們非常歡迎反饋。

OpenSSF 工具

OpenSSF 正在領導幾項非常有用的倡議,特別是如果您計劃釋出 npm 包。這些倡議包括

  • OpenSSF Scorecard Scorecard 使用一系列自動化的安全風險檢查來評估開源專案。您可以使用它來主動評估程式碼庫中的漏洞和依賴項,並就接受漏洞做出明智的決定。
  • OpenSSF 最佳實踐徽章計劃 專案可以透過描述它們如何遵守每個最佳實踐來自願進行自我認證。這將生成一個可以新增到專案中的徽章。