運行不受信任的 JavaScript 程式碼

WBOY
發布: 2024-07-22 07:10:29
原創
996 人瀏覽過

Running Untrusted JavaScript Code

重要這僅與執行 JavaScript 和 TypeScript 程式碼有關。話雖如此,寫作也可能是用其他語言運行其他程式碼的方向。

允許用戶在您的應用程式中執行他們的程式碼開啟了一個自訂和功能的世界,但它也使您的平檯面臨重大的安全威脅。

鑑於它是用戶程式碼,一切都在預料之中,從停止伺服器(可能是無限循環)到竊取敏感資訊。

本文將探討各種緩解執行使用者程式碼的策略,包括 Web Workers、靜態程式碼分析等等…

你應該關心

有很多場景需要運行使用者提供的程式碼,從 CodeSandbox 和 StackBiltz 這樣的協作開發環境到像 January 這樣的可自訂 API 平台。即使是代碼遊樂場也容易受到風險。

也就是說,安全運行使用者提供的程式碼的兩個基本優勢是:

  1. 獲得用戶的信任:即使用戶值得信賴,他們也可能會執行從其他故意壞人複製的程式碼。
  2. 保護您的環境:您最不需要的就是一段程式碼來停止您的伺服器。邊思考 (true) {}

定義“敏感資訊”

運行使用者代碼並沒有什麼害處,除非您擔心這可能會導致某些資料被盜。無論您關心什麼數據,都將被視為敏感資訊。例如,在大多數情況下,JWT 是敏感資訊(也許用作身份驗證機制時)

可能會出現什麼問題

考慮 JWT 儲存在隨每個請求發送的 cookie 中的潛在風險。使用者可能會無意中觸發將 JWT 發送到惡意伺服器的請求,並且...

  • 跨站腳本(XSS)。
  • 拒絕服務 (DoS) 攻擊。
  • 資料外洩。如果沒有適當的保護措施,這些威脅可能會損害應用程式的完整性和效能。

方法

邪惡的評估

最簡單,但風險最大。

雷雷

當您執行此程式碼時,它會記錄該訊息。本質上,eval 是一個能夠存取全域/視窗範圍的 JS 解譯器。

雷雷

此程式碼使用了在全域範圍內定義的 fetch。解釋器不知道這一點,但由於 eval 可以訪問窗口,所以它知道。這意味著在瀏覽器中運行 eval 與在伺服器環境或工作執行緒中運行不同。

雷雷

這個怎麼樣...

雷雷

此程式碼將停止瀏覽器標籤。您可能會問為什麼使用者會對自己這樣做。好吧,他們可能正在從互聯網上複製代碼。這就是為什麼最好使用/或限制執行時間來進行靜態分析。

您可能想查看有關 eval 的 MDN 文件

時間框執行可以透過在 Web Worker 中執行程式碼並使用 setTimeout 來限制執行時間來完成。

雷雷

函數構造器

這與 eval 類似,但它更安全一些,因為它無法存取封閉的範圍。

雷雷

此程式碼將記錄 2.

注意:第二個參數是函數體。

函數構造函數無法存取封閉範圍,因此以下程式碼將拋出錯誤。

雷雷

但它可以存取全域範圍,因此上面的 fetch 範例可以工作。

網路工作者

您可以在 WebWorker 上運行“函數構造函數和 eval”,由於沒有 DOM 訪問,這會更安全。

要實施更多限制,請考慮禁止使用全域對象,例如 fetch、XMLHttpRequest、sendBeacon 查看本文以了解如何做到這一點。

隔離虛擬機

Isolated-VM 是一個函式庫,可讓您在單獨的 VM 中執行程式碼(v8 的 Isolate 介面)

雷雷

此程式碼將記錄 hello world

網路組裝

這是一個令人興奮的選項,因為它提供了運行程式碼的沙盒環境。需要注意的是,您需要一個具有 Javascript 綁定的環境。然而,一個名為 Extism 的有趣項目促進了這一點。您可能想遵循他們的教程。

它的迷人之處在於,您將使用eval 來運行程式碼,但考慮到WebAssembly 的性質,DOM、網路、檔案系統和對主機環境的存取都是不可能的(儘管它們可能根據wasm 的不同而有所不同)運行時).

function evaluate() { const { code, input } = JSON.parse(Host.inputString()); const func = eval(code); const result = func(input).toString(); Host.outputString(result); } module.exports = { evaluate };
登入後複製

You'll have to compile the above code first using Extism, which will output a Wasm file that can be run in an environment that has Wasm-runtime (browser or node.js).

const message = { input: '1,2,3,4,5', code: ` const sum = (str) => str .split(',') .reduce((acc, curr) => acc + parseInt(curr), 0); module.exports = sum; `, }; // continue running the wasm file
登入後複製

Docker

We're now moving to the server-side, Docker is a great option to run code in an isolation from the host machine. (Beware of container escape)

You can use dockerode to run the code in a container.

import Docker from 'dockerode'; const docker = new Docker(); const code = `console.log("hello world")`; const container = await docker.createContainer({ Image: 'node:lts', Cmd: ['node', '-e', code], User: 'node', WorkingDir: '/app', AttachStdout: true, AttachStderr: true, OpenStdin: false, AttachStdin: false, Tty: true, NetworkDisabled: true, HostConfig: { AutoRemove: true, ReadonlyPaths: ['/'], ReadonlyRootfs: true, CapDrop: ['ALL'], Memory: 8 * 1024 * 1024, SecurityOpt: ['no-new-privileges'], }, });
登入後複製

Keep in mind that you need to make sure the server has docker installed and running. I'd recommend having a separate server dedicated only to this that acts as a pure-function server.

Moreover, you might benefit from taking a look at sysbox, a VM-like container runtime that provides a more secure environment. Sysbox is worth it, especially if the main app is running in a container, which means that you'll be running Docker in Docker.

This was the method of choice at January but soon enough, the language capabilities mandated more than passing the code through the container shell. Besides, for some reason, the server memory spikes frequently; we run the code inside self-removable containers on every 1s debounced keystroke. (You can do better!)

Other options

  • Web Containers
  • MicroVM (Firecraker)
  • Deno subhosting
  • Wasmer
  • ShadowRealms

Safest option

I'm particularly fond of Firecracker, but it’s a bit of work to set up, so if you cannot afford the time yet, you want to be on the safe side, do a combination of static analysis and time-boxing execution. You can use esprima to parse the code and check for any malicious act.

How to run TypeScript code?

Well, same story with one (could be optional) extra step: Transpile the code to JavaScript before running it. Simply put, you can use esbuild or typescript compiler, then continue with the above methods.

async function build(userCode: string) { const result = await esbuild.build({ stdin: { contents: `${userCode}`, loader: 'ts', resolveDir: __dirname, }, inject: [ // In case you want to inject some code ], platform: 'node', write: false, treeShaking: false, sourcemap: false, minify: false, drop: ['debugger', 'console'], keepNames: true, format: 'cjs', bundle: true, target: 'es2022', plugins: [ nodeExternalsPlugin(), // make all the non-native modules external ], }); return result.outputFiles![0].text; }
登入後複製

Notes:

  • Rust-based bundlers usually offer a web assembly version, which means you can transpile the code in the browser. Esbuild does have a web assembly version.
  • Don't include user specified imports into the bundle unless you've allow-listed them.

Additionally, you can avoid transpiling altogether by running the code usingDenoorBunin a docker container since they support TypeScript out of the box.

Conclusion

Running user code is a double-edged sword. It can provide a lot of functionality and customization to your platform, but it also exposes you to significant security risks. It’s essential to understand the risks and take appropriate measures to mitigate them and remember that the more isolated the environment, the safer it is.

References

  • January instant compilation
  • Running untrusted JavaScript in Node.js
  • How do languages support executing untrusted user code at runtime?
  • Safely Evaluating JavaScript with Context Data

以上是運行不受信任的 JavaScript 程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!