Node-js 安全最佳實踐

大家好,我是 ConardLi

最近 Node.js 團隊在官方文檔上公佈了一份最新的安全實踐,解讀了一些 Node.js 服務下一些常見的攻擊場景以及預防手段,我們一起來看看吧!

計時攻擊

計時攻擊可能會讓攻擊者獲取到一些潛在的敏感信息,例如,測量應用程序響應請求所需的時間。這種攻擊並不是特定於 Node.js 的,幾乎可以針對所有運行時。

我們的程序代碼中可能會存在一些時間段敏感的操作,比如我們需要校驗一個用戶的密碼是否正確。

我們可能會從數據庫檢索出來的用戶信息中比較密碼。對於相同的長度值,使用內置字符串比較可能需要更長的時間。這種比較在以可接受的數量運行時會增加請求的響應時間。通過比較請求響應時間,攻擊者可以在大量請求中猜測密碼的長度和值。

緩解措施

crypto API

惡意第三方模塊

目前,在 Node.js 中,任何包都可以訪問網絡、文件系統,他們可以將任何數據發送到任何地方。

所有運行在 Node.js 進程中的代碼都能夠通過使用 eval() 加載和運行額外的任意代碼。所有具有文件系統寫訪問權限的代碼都可以通過寫入加載的新文件或現有文件來實現相同的目的。

Node.js 有一個實驗性的 策略機制(https://nodejs.org/api/permissions.html#policies) 來聲明加載的資源是否是不受信任的。

我們應該確保使用通用工作流或 npm script 固定依賴版本、自動檢查漏洞。在安裝依賴包之前,請確保這個還是在維護的幷包含你期望的所有內容。注意,Github 源代碼並不總是與發佈的包相同,最好在 node_modules 中驗證一下。

供應鏈攻擊

供應鏈攻擊一般指控制上游包的攻擊者可以發佈包含惡意代碼的新版本。如果我們的 Node.js 應用程序依賴於這個包,而沒有嚴格確定哪個版本可以安全使用,則該包可以自動更新到最新的惡意版本,從而危及應用程序。

供應鏈攻擊攻擊最近在 Node.js 的依賴生態中頻發發生,比如前段時間的 node-ipc,針對俄羅斯和白俄羅斯 IP,會嘗試覆蓋當前目錄、父目錄和根目錄的所有文件,把所有內容替換成 ❤。

詳細可以瞭解我之前的文章:

百萬周下載量的 npm 包以反戰爲名進行供應鏈投毒!

這主要還是因爲 Node.js 生態對依賴項的規範過於鬆懈了,比如允許不需要的更新,我們可能悄無聲息的在某一次上線中爲我們的程序帶來了巨大的危機。

雖然我們可以在 package.json 中指定依賴項確切的版本號或範圍,但這隻能保證直接依賴的固定,我們仍然無法保障間接依賴的不確定性更新。

緩解措施

內存訪問衝突

基於內存或基於堆的攻擊取決於代碼中的內存管理錯誤和可利用的內存分配器的組合。與所有運行時一樣,如果項目運行在共享的機器上,Node.js 很容易受到這些攻擊。使用 secure heap 有助於防止由於指針溢出和不足而導致敏感信息泄漏。

但是,secure heapWindows 上不可用,更多信息可以看這個文檔:https://nodejs.org/dist/latest-v18.x/docs/api/cli.html#--secure-heap

緩解措施

猴子修補

猴子補丁指的是爲了改變現有的行爲在運行時修改屬性,比如:

// 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) 可用於保證不會替換任何全局變量。

原型污染

原型污染是指通過濫用 _proto_、 constructor、prototype 和其他從內置原型繼承的其他屬性來修改或將屬性注入 JavaScript 語言項的攻擊,這是一種繼承自 JavaScript 語言的潛在漏洞。

比如下面的代碼,一個外部傳入的數據可能會影響到我們整個 Node.js 服務的 Object 對象的默認行爲:

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

緩解措施

路徑引入漏洞

Node.js 按照模塊解析算法(https://nodejs.org/api/modules.html#modules_all_together)加載模塊。因此,它默認假定請求(require)模塊的目錄是受信任的。

也就是說,這意味着以下應用程序行爲是預期的。假設有以下目錄結構:

app/
  server.js
  auth.js
  auth

如果 server.js 使用 require('./auth') ,它將遵循模塊解析算法並加載 auth 而不是 auth.js

緩解措施

具有完整性檢查的實驗性策略機制(https://nodejs.org/api/permissions.html#integrity-checks)可以避免上述威脅。對於上述目錄,可以使用以下 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 來避免策略突變。

HTTP 請求走私攻擊(HTTP Request Smuggling)

這是一種涉及兩個 HTTP 服務器(通常是代理服務和 Node.js 應用服務)的攻擊。客戶端發送 HTTP 請求,這個請求首先通過前端服務器(代理),然後重定向到後端服務器(應用程序)。當前端和後端對模糊的 HTTP 請求的解釋不同時,攻擊者就有可能發送前端看不到但後端會看到的惡意消息,有效地通過代理服務器進行了 “走私” 。

通俗地理解就是:攻擊者發送一個語句模糊的請求,就有可能被解析爲兩個不同的 HTTP 請求,第二請求可能會 “逃過” 正常的安全設備的檢測,使攻擊者可以繞過安全控制,未經授權訪問敏感數據並直接危害其他應用程序用戶。

由於這種攻擊產生的根本原因是 Node.js 與另一個 HTTP 服務器解釋 HTTP 請求的方式不同,我們可以認爲它是 Node.js、前端服務器兩者的漏洞 。如果 Node.js 解釋請求的方式是符合 HTTP 規範(https://datatracker.ietf.org/doc/html/rfc7230#section-3)的,那麼它就不被認爲是 Node.js 中的漏洞。

緩解措施

HTTP 服務拒絕訪問

很多時候,由於我們錯誤的代碼邏輯或者錯誤的配置可能會導致 HTTP 服務無法訪問,參考下面的代碼:

const net = require('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');

我們的 WebServer 沒有正確的處理 Socket 錯誤,當發送的請求量過大時,我們的服務就會崩潰。

緩解措施

DNS 重綁定

這是一種針對在使用 --inspect (https://nodejs.org/en/docs/guides/debugging-getting-started/) 啓用調試檢查器的情況下運行的 Node.js 應用程序的攻擊。

由於在 Web 瀏覽器中打開的網站可以發出 WebSocketHTTP 請求,它們可以針對本地運行的調試檢查器。這通常會被現代瀏覽器實施的同源策略所阻止,這個策略會禁止腳本訪問來自不同來源的資源(意味着惡意網站無法讀取從本地 IP 地址請求的數據)。

但是,通過 DNS 重綁定,攻擊者可以暫時控制其請求的來源,使它們看起來像是來自本地 IP 地址。這是通過控制網站和用於解析其 IP 地址的 DNS 服務器來完成的。詳細可以瞭解:https://en.wikipedia.org/wiki/DNS_rebinding

緩解措施

NPM 敏感信息泄漏

在包發佈期間,包含在當前目錄中的所有文件和文件夾都會被推送到 npm 註冊表中,如果我們的開發目錄中包含了一些敏感信息,它們都會被泄露出去。

我們可以通過用 .npmignore.gitignore 定義一個阻止列表或者在 package.json 中定義一個 allowlist 來控制這種行爲

緩解措施

最後

參考:https://nodejs.org/en/docs/guides/security/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2CBGgtja04NnOerpKfk0Ug