首页 > web前端 > js教程 > 正文

运行不受信任的 JavaScript 代码

WBOY
发布: 2024-07-22 07:10:29
原创
1187 人浏览过

Running Untrusted JavaScript Code

重要这仅与运行 JavaScript 和 TypeScript 代码有关。话虽如此,写作也可能是用其他语言运行其他代码的方向。

允许用户在您的应用程序中执行他们的代码打开了一个自定义和功能的世界,但它也使您的平台面临重大的安全威胁。

鉴于它是用户代码,一切都在预料之中,从停止服务器(可能是无限循环)到窃取敏感信息。

本文将探讨缓解运行用户代码的各种策略,包括 Web Workers、静态代码分析等等......

你应该关心

有很多场景需要运行用户提供的代码,从 CodeSandbox 和 StackBiltz 这样的协作开发环境到像 January 这样的可定制 API 平台。即使是代码游乐场也容易受到风险的影响。

也就是说,安全运行用户提供的代码的两个基本优势是:

  1. 获得用户的信任:即使用户值得信赖,他们也可能会执行从其他故意坏人那里复制的代码。
  2. 保护您的环境:您最不需要的就是一段代码来停止您的服务器。边思考 (true) {}

定义“敏感信息”

运行用户代码并没有什么害处,除非您担心这可能会导致某些数据被盗。无论您关心什么数据,都将被视为敏感信息。例如,在大多数情况下,JWT 是敏感信息(也许用作身份验证机制时)

可能会出现什么问题

考虑 JWT 存储在随每个请求发送的 cookie 中的潜在风险。用户可能会无意中触发将 JWT 发送到恶意服务器的请求,并且...

  • 跨站脚本 (XSS)。
  • 拒绝服务 (DoS) 攻击。
  • 数据泄露。如果没有适当的保护措施,这些威胁可能会损害应用程序的完整性和性能。

方法

邪恶的评估

最简单,但风险最大。

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
登录后复制

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 using Deno or Bun in 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学习者快速成长!