安全最佳實踐

意圖

此文件旨在擴展目前的威脅模型,並提供詳細的指南,以確保 Node.js 應用程式的安全性。

文件內容

  • 最佳實踐:一種簡化的總結方式來查看最佳實踐。我們可以使用此問題此指南作為起點。重要的是要注意,此文件特定於Node.js,如果您正在尋找一些廣泛的內容,請考慮OSSF 最佳實踐
  • 攻擊解釋:用簡單的英語以及一些代碼示例(如果可能)來說明和記錄我們在威脅模型中提到的攻擊。
  • 第三方庫:定義威脅(如拼寫錯誤攻擊、惡意包...)以及有關節點模塊依賴性的最佳實踐,等等...

威脅清單

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

這是一種攻擊,應用程序因處理傳入的 HTTP 請求的方式而變得無法正常使用。這些請求不需要由惡意行為者刻意製作:配置不當或有錯誤的客戶端也可能向伺服器發送一系列請求模式,導致拒絕服務。

HTTP 請求由 Node.js HTTP 伺服器接收,並通過註冊的請求處理程序傳遞給應用程序代碼。伺服器不解析請求主體的內容。因此,由於在將它們傳遞給請求處理程序後,主體的內容引起的任何 DoS 不是 Node.js 本身的漏洞,因為應用程序代碼有責任正確處理它們。

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

const net = require('node:net');

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

server.listen(5000, '0.0.0.0');

如果執行一個錯誤的請求,伺服器可能會崩潰。

一個例子是不由請求內容引起的 DoS 攻擊是Slowloris。在這種攻擊中,HTTP 請求被慢慢地分段送出,一個片段一次。直到完整的請求被傳遞,伺服器將持續為進行中的請求保留資源。如果同時發送足夠多的這些請求,並發現同時連線數很快就達到最大值,導致服務拒絕。這就是攻擊不取決於請求內容,而是取決於發送給伺服器的請求的時間和模式。

緩解措施

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

DNS 重新綁定(CWE-346)

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

由於在網頁瀏覽器中打開的網站可以發出 WebSocket 和 HTTP 請求,它們可以針對本地運行的調試檢查器。這通常被現代瀏覽器實施的 同源政策 防止,該政策禁止腳本從不同來源(意味著惡意網站無法讀取從本地 IP 地址請求的資料)。

然而,通過 DNS 重綁定,攻擊者可以暫時控制其請求的來源,使其看起來來自於本地 IP 地址。這是通過控制網站和用於解析其 IP 地址的 DNS 伺服器來完成的。詳細資訊請參見DNS 重綁定維基

緩解措施

  • 通過將 SIGUSR1 信號附加到 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,幾乎可以針對所有運行時。

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

緩解措施

  • 加密 API 公開了一個函數 timingSafeEqual 來使用常數時間算法比較實際和預期的敏感值。

  • 對於密碼比較,您可以使用在原生加密模組上也可用的 scrypt

  • 更一般地,避免在變量時間操作中使用秘密。這包括在秘密上進行分支和當攻擊者可能與相同基礎設施(例如相同的雲機器)共處時,將秘密用作記憶體索引。在 JavaScript 中編寫常數時間代碼很難(部分原因是 JIT)。對於加密應用,使用內建的加密 API 或 WebAssembly(用於未在本地實現的算法)。

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

目前,在 Node.js 中,任何套件都可以訪問強大的資源,例如網路訪問。此外,因為它們還可以訪問文件系統,所以它們可以將任何數據發送到任何地方。

運行到節點進程中的所有代碼都可以使用 eval()(或其等效物)加載並運行其他任意代碼。具有文件系統寫入訪問權限的所有代碼都可以通過寫入加載新的或現有文件來實現相同的功能。

Node.js 在實驗性¹政策機制中,可將載入的資源宣告為不受信任或受信任。但是,此政策並未預設啟用。請確保釘選依賴版本並使用常見工作流程或 npm 腳本自動檢查漏洞。在安裝套件之前,請確認此套件是否維護良好且包含您預期的所有內容。請注意,GitHub 的原始碼並不總是與公開發佈的版本相同,請在 node_modules 中驗證。

供應鏈攻擊

Node.js 應用程式的供應鏈攻擊發生在其相依性(直接或間接)之一受到破壞時。這可能是由於應用程式對相依性的規範過於寬鬆(允許不需要的更新)和/或規範中的常見錯誤(容易受到typosquatting)。

攻擊者控制上游套件後,可以發佈帶有惡意程式碼的新版本。如果一個 Node.js 應用程式依賴於該套件,而不對可使用的版本進行嚴格控制,則該套件可能會自動更新為最新的惡意版本,從而危害應用程式。

package.json 檔案中指定的相依性可以是確切的版本號或範圍。然而,當將相依性釘選到確切版本時,其相依性仍未被釘選。這仍然使應用程式容易受到不需要/不期望的更新。

可能的攻擊向量

  • typosquatting 攻擊
  • 鎖檔毒化
  • 受損的維護者
  • 惡意套件
  • 相依性混淆

緩解措施

  • 使用 --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=n,其中n是分配的最大位元組大小。
  • 不要在共享機器上運行生產應用程式。

猴補丁(CWE-349)

猴補丁是指在運行時修改屬性,旨在更改現有的行為。例如

// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // overriding the global [].push
};

緩解措施

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

// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // 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 a = { a: 1, b: 2 };
const data = JSON.parse('{"__proto__": { "polluted": true}}');

const c = Object.assign({}, a, data);
console.log(c.polluted); // true

// Potential DoS
const data2 = JSON.parse('{"__proto__": null}');
const d = Object.assign(a, data2);
d.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 Best Practices Badge Program 項目可以自願地自我認證,描述它們如何符合每個最佳實踐。這將生成一個可以添加到項目中的徽章。