重要:这仅与运行 JavaScript 和 TypeScript 代码有关。话虽如此,写作也可能是用其他语言运行其他代码的方向。
允许用户在您的应用程序中执行他们的代码打开了一个自定义和功能的世界,但它也使您的平台面临重大的安全威胁。
鉴于它是用户代码,一切都在预料之中,从停止服务器(可能是无限循环)到窃取敏感信息。
本文将探讨缓解运行用户代码的各种策略,包括 Web Workers、静态代码分析等等......
有很多场景需要运行用户提供的代码,从 CodeSandbox 和 StackBiltz 这样的协作开发环境到像 January 这样的可定制 API 平台。即使是代码游乐场也容易受到风险的影响。
也就是说,安全运行用户提供的代码的两个基本优势是:
运行用户代码并没有什么害处,除非您担心这可能会导致某些数据被盗。无论您关心什么数据,都将被视为敏感信息。例如,在大多数情况下,JWT 是敏感信息(也许用作身份验证机制时)
考虑 JWT 存储在随每个请求发送的 cookie 中的潜在风险。用户可能会无意中触发将 JWT 发送到恶意服务器的请求,并且...
最简单,但风险最大。
eval('console.log("I am dangerous!")');
当您运行此代码时,它会记录该消息。本质上,eval 是一个能够访问全局/窗口范围的 JS 解释器。
const res = await eval('fetch(`https://jsonplaceholder.typicode.com/users`)'); const users = await res.json();
此代码使用在全局范围内定义的 fetch。解释器不知道这一点,但由于 eval 可以访问窗口,所以它知道。这意味着在浏览器中运行 eval 与在服务器环境或工作线程中运行它不同。
eval(`document.body`);
这个怎么样...
eval(`while (true) {}`);
此代码将停止浏览器选项卡。您可能会问为什么用户会对自己这样做。好吧,他们可能会从互联网上复制代码。这就是为什么首选进行静态分析并/或对执行进行时间限制。
您可能想查看有关 eval 的 MDN 文档
时间框执行可以通过在 Web Worker 中运行代码并使用 setTimeout 来限制执行时间来完成。
async function timebox(code, timeout = 5000) { const worker = new Worker('user-runner-worker.js'); worker.postMessage(code); const timerId = setTimeout(() => { worker.terminate(); reject(new Error('Code execution timed out')); }, timeout); return new Promise((resolve, reject) => { worker.onmessage = event => { clearTimeout(timerId); resolve(event.data); }; worker.onerror = error => { clearTimeout(timerId); reject(error); }; }); } await timebox('while (true) {}');
这与 eval 类似,但它更安全一些,因为它无法访问封闭的范围。
const userFunction = new Function('param', 'console.log(param);'); userFunction(2);
此代码将记录 2。
注意:第二个参数是函数体。
函数构造函数无法访问封闭范围,因此以下代码将抛出错误。
function fnConstructorCannotUseMyScope() { let localVar = 'local value'; const userFunction = new Function('return localVar'); return userFunction(); }
但它可以访问全局范围,因此上面的 fetch 示例可以工作。
您可以在 WebWorker 上运行“函数构造函数和 eval”,由于没有 DOM 访问,这会更安全。
要实施更多限制,请考虑禁止使用全局对象,例如 fetch、XMLHttpRequest、sendBeacon 查看本文以了解如何做到这一点。
Isolated-VM 是一个库,允许您在单独的 VM 中运行代码(v8 的 Isolate 接口)
import ivm from 'isolated-vm'; const code = `count += 5;`; const isolate = new ivm.Isolate({ memoryLimit: 32 /* MB */ }); const script = isolate.compileScriptSync(code); const context = isolate.createContextSync(); const jail = context.global; jail.setSync('log', console.log); context.evalSync('log("hello world")');
此代码将记录 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
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!)
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.
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:
Additionally, you can avoid transpiling altogether by running the code using Deno or Bun in a docker container since they support TypeScript out of the box.
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.
以上是运行不受信任的 JavaScript 代码的详细内容。更多信息请关注PHP中文网其他相关文章!