Node-js 安全最佳實踐
大家好,我是 ConardLi。
最近 Node.js 團隊在官方文檔上公佈了一份最新的安全實踐,解讀了一些 Node.js 服務下一些常見的攻擊場景以及預防手段,我們一起來看看吧!
計時攻擊
計時攻擊可能會讓攻擊者獲取到一些潛在的敏感信息,例如,測量應用程序響應請求所需的時間。這種攻擊並不是特定於 Node.js 的,幾乎可以針對所有運行時。
我們的程序代碼中可能會存在一些時間段敏感的操作,比如我們需要校驗一個用戶的密碼是否正確。
我們可能會從數據庫檢索出來的用戶信息中比較密碼。對於相同的長度值,使用內置字符串比較可能需要更長的時間。這種比較在以可接受的數量運行時會增加請求的響應時間。通過比較請求響應時間,攻擊者可以在大量請求中猜測密碼的長度和值。
緩解措施
crypto API
-
crypto API提供了一個timingSafeEqual函數,當你需要進行比較的值比較敏感時,它可一採用恆定時間算法進行比較。 -
對於密碼比較,你可以使用
crypto模塊上提供的scrypt(https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback)。 -
避免在可變時間操作中使用密鑰,包括密鑰分支,並且當攻擊者可能位於同一基礎設施(例如同一臺雲機器)上時,使用密鑰作爲內存索引。用
JavaScript編寫恆定時間的代碼還是很困難的,對於加密應用程序,推薦使用內置的加密 API 或WebAssembly。
惡意第三方模塊
目前,在 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,會嘗試覆蓋當前目錄、父目錄和根目錄的所有文件,把所有內容替換成 ❤。
詳細可以瞭解我之前的文章:
這主要還是因爲 Node.js 生態對依賴項的規範過於鬆懈了,比如允許不需要的更新,我們可能悄無聲息的在某一次上線中爲我們的程序帶來了巨大的危機。
雖然我們可以在 package.json 中指定依賴項確切的版本號或範圍,但這隻能保證直接依賴的固定,我們仍然無法保障間接依賴的不確定性更新。
緩解措施
-
防止
npm使用——ignore-scripts執行任意腳本 -
可以使用
npm config set ignore-scripts true全局禁用它 -
將
lock文件將依賴版本固定到特定的不可變版本,而不是一個範圍(當然後續要手動定期更新) -
將
npm audit引入 CI 流程,自動檢查漏洞 -
諸如
Socket之類的工具可以用來分析帶有靜態分析的包,以發現諸如網絡或文件系統訪問之類的風險行爲 -
使用
npm ci代替npm install,這將強制執行lockfile,避免它與package.json文件之間的不一致會導致錯誤 -
仔細檢查
package.json文件中依賴項名稱中的錯誤 / 錯別字。
內存訪問衝突
基於內存或基於堆的攻擊取決於代碼中的內存管理錯誤和可利用的內存分配器的組合。與所有運行時一樣,如果項目運行在共享的機器上,Node.js 很容易受到這些攻擊。使用 secure heap 有助於防止由於指針溢出和不足而導致敏感信息泄漏。
但是,secure heap 在 Windows 上不可用,更多信息可以看這個文檔:https://nodejs.org/dist/latest-v18.x/docs/api/cli.html#--secure-heap
緩解措施
-
根據程序的時機情況使用
——secure-heap=n,其中n是分配的最大字節大小; -
不要在共享機器上運行比較重要的應用程序。
猴子修補
猴子補丁指的是爲了改變現有的行爲在運行時修改屬性,比如:
// 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
緩解措施
-
避免不安全的遞歸合併(https://gist.github.com/DaniAkash/b3d7159fddcff0a9ee035bd10e34b277#file-unsafe-merge-js)
-
爲外部 / 不受信任的請求實施
JSON模式驗證 -
使用
Object.create(null)創建沒有原型的對象 -
使用
Object.freeze(MyObject.prototype)凍結原型 -
使用
--disable-proto標誌禁用Object.prototype.__proto__屬性 -
檢查屬性是否直接存在於對象上,而不是從使用
Object.hasOwn(obj, keyFromObj) -
避免使用
Object.prototype中的方法。
路徑引入漏洞
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服務器時,不要使用insecureHTTPParser選項; -
前端服務器的配置要儘量規範化,避免歧義請求;
-
持續監控 Node.js 和前端服務器中是否存在新的
HTTP請求走私漏洞; -
使用
HTTP/2端到端並儘可能禁用HTTP降級。
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 錯誤,當發送的請求量過大時,我們的服務就會崩潰。
緩解措施
-
使用反向代理接收請求並將請求轉發到
Node.js應用程序。反向代理可以提供緩存、負載平衡、IP 黑名單等功能,從而降低DoS攻擊生效的可能性; -
正確配置服務器超時,以便可以放棄空閒或速度太慢的連接。例如 http.Server 中的
headersTimeout、requestTimeout timeout keepAliveTimeout; -
限制每臺主機打開的
Socket總數,可以參考http中的agent.maxSockets、agent.maxTotalSockets、agent.maxFreeSockets、server.maxRequestsPerSocket
DNS 重綁定
這是一種針對在使用 --inspect (https://nodejs.org/en/docs/guides/debugging-getting-started/) 啓用調試檢查器的情況下運行的 Node.js 應用程序的攻擊。
由於在 Web 瀏覽器中打開的網站可以發出 WebSocket 和 HTTP 請求,它們可以針對本地運行的調試檢查器。這通常會被現代瀏覽器實施的同源策略所阻止,這個策略會禁止腳本訪問來自不同來源的資源(意味着惡意網站無法讀取從本地 IP 地址請求的數據)。
但是,通過 DNS 重綁定,攻擊者可以暫時控制其請求的來源,使它們看起來像是來自本地 IP 地址。這是通過控制網站和用於解析其 IP 地址的 DNS 服務器來完成的。詳細可以瞭解:https://en.wikipedia.org/wiki/DNS_rebinding
緩解措施
-
通過附加一個
process.on(‘SIGUSR1’, …)偵聽器來禁用SIGUSR1信號上的檢查器 -
不要在生產環境中運行
inspector協議
NPM 敏感信息泄漏
在包發佈期間,包含在當前目錄中的所有文件和文件夾都會被推送到 npm 註冊表中,如果我們的開發目錄中包含了一些敏感信息,它們都會被泄露出去。
我們可以通過用 .npmignore 和 .gitignore 定義一個阻止列表或者在 package.json 中定義一個 allowlist 來控制這種行爲
緩解措施
-
使用
npm publish——dry-run列出所有要發佈的文件,確保在發佈包之前進行檢查; -
創建和維護諸如
.gitignore和.npmignore這樣的忽略文件也很重要。在這些文件中,你可以指定不應該發佈哪些文件 / 文件夾;
最後
參考:https://nodejs.org/en/docs/guides/security/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2CBGgtja04NnOerpKfk0Ug