首页 > 解决方案 > 如何对在画布上绘制的任意用户提交的 Javascript 代码进行沙箱处理?

问题描述

我正在尝试构建一个应用程序,用户可以在其中编写自定义 Javascript 代码以便在画布上绘制图形。我希望用户能够与其他人共享此代码,并让他们在浏览器中安全地运行其他人贡献的绘图功能。

我正在寻找的基本上是一个 Javascript 沙箱,可以防止任何重大安全漏洞。我还希望能够在 Web Worker 中执行繁重的绘图逻辑,以免阻塞主线程并保持 UI 响应。

到目前为止,我已经提出了一个使用 iframe、多个域和 Web Worker 的最小实现,如下所示:

  1. 用户在页面上的画布上绘图(例如,从 www.drawing.com 检索)。
  2. 当画布上发生鼠标事件时(例如用户点击它),一个 postMessage() 被执行到来自不同来源的 iframe (www.drawing-scripts.com)。postMessage 提供了应该运行的代码(作为字符串),以及一堆自定义库函数(例如 circle()、line())和一些用于绘图的附加参数(例如笔画粗细)。在这里,使用不同的来源可以防止任何在 iframe 中评估的代码访问从主要来源存储的 cookie(例如,与身份验证相关的)。iframe 使用sandbox属性和allow-scripts属性进行沙盒化,以允许其运行 JS 代码。
  3. iframe 以字符串的形式接收代码和库,并对加载时生成的 web worker 执行 postMessage()。这样做是为了避免在(可能)高开销的绘图操作期间阻塞主浏览器线程并阻塞 UI 渲染。
  4. Web Worker 在加载时和在任何代码发送给它之前,使用白名单删除所有其他功能,就像在Node-SO-bot中所做的那样。
  5. 然后,网络工作者从 iframe 接收带有代码和库作为字符串的消息,并评估自定义代码。
  6. 我提供的库函数(例如circle()line())只是将绘图信息附加到列表中。这从 web worker 通过 a 返回到 iframe postMessage(),然后通过 another 返回到主页面postMessage()。最后,主页面简单的在画布上一一进行列表中的绘制操作。

所以流程基本上看起来像

mouseClick --> postMessage 代码到 iframe --> postMessage 代码到白名单的 web worker --> eval() 代码,将绘图操作附加到列表 --> 将绘图操作列表返回到 iframe --> 将绘图操作列表返回到主页 --> 按要求执行操作。

这目前有效,但有一些问题:

标签: javascriptcanvasiframeevalsandbox

解决方案


你不需要这一切。

根据您的第2点,您的用户没有传递必须执行的“任意”代码。
他们发送的只是您的应用程序生成的数据、坐标、颜色、可能是文本或您的应用程序将处理的其他内容。

你绝对不需要eval

您需要的是一种对应用程序命令进行字符串化,然后对其进行解析的方法。

例如,如果您的用户从 10、10 到 15,15 画一条线,您可以存储类似

{ type: "line", x1: 10, y1: 10, x2: 15, y2: 15 }

甚至 SVG 之类的M10,10L15,15.

然后,当您检索用户的数据时,您只需解析它并调用匹配绘图命令。

const ctx = canvas.getContext('2d');

inp.oninput = e => parseAndDraw(inp.value);
const availableCommands = {
  clear({x=0, y=0, w=canvas.width, h=canvas.height}) {
    ctx.clearRect(x, y, w, h);
  },
  beginPath(cmd) {
    ctx.beginPath();
  },
  line({x1, y1, x2, y2}) {
    if(!isNaN(x1))
      ctx.lineTo(x1, y1);
    ctx.lineTo(x2, y2);
  },
  stroke({color = "black", lineWidth = 1}) {
    ctx.strokeStyle = color;
    ctx.lineWidth = lineWidth;
    ctx.stroke();
  }
}

inp.oninput();

function parseAndDraw(str) {
  // a few checks
  try {
    commands = JSON.parse(str);
  }
  catch(e) {
    return wrongData("invalid JSON format");
  }
  if(!Array.isArray(commands)) {
    return wrongData("must be an Array of commands");
  }
  if(
    commands.some(command => !command || typeof command.type !== 'string')
    ) {
    return wrongData("all commands must have a type");
  }
  commands.forEach(draw);
  console.clear();
}

function draw(command) {
  const known_command = availableCommands[command.type];
  if(typeof known_command !== 'function') {
    return wrongData("unknown command");
  }
  known_command(command);
}
function wrongData(reason) {
  console.clear();
  console.error('[wrong data]:', reason);
}
canvas { border: 1px solid; }
#inp { width: 100%; height: 100vh; background: ivory }
<canvas id="canvas"></canvas><br>
<textarea id="inp" row="15">
[
  {
    "type": "clear",
    "x": 0,
    "y": 0,
    "w": 300,
    "h": 150
  },  
  {
    "type": "beginPath"
  },
  {
    "type": "line", 
    "x1": 10,
    "y1": 10,
    "x2": 35,
    "y2": 35.5
  },  
  {
    "type": "line",
    "x2": 80,
    "y2": 35.5
  },
  {
    "type": "line",
    "x2": 50,
    "y2": 70
  },
  {
    "type": "stroke",
    "color": "red",
    "lineWidth": 1
  }
]
</textarea>

没有理由比首先使用您的应用程序更安全。

请记住,2D 画布 API 有一个Path2d接口,它可以极大地简化使用 SVG 语法构建复杂路径的过程:

const ctx = canvas.getContext('2d');
const user_input = "M10,10L35,35.5L80,35.53L50,70";
const path = new Path2D(user_input);
ctx.strokeStyle = "red";
ctx.stroke(path);
canvas { border: 1px solid }
<canvas id="canvas"></canvas>

如果您允许您的用户与 ImageData 交互,可能会有点困难。但是虽然更难,但您也可以很好地仅使用一组允许的算术运算来对这些对象执行。

唯一可能存在非常小的安全漏洞(实际上是更多的隐私漏洞)的部分是,如果您确实允许您的用户绘制外包图像,那么他们将能够知道谁访问了该图像,但这是任何网站的事情确实显示外包图像必须处理,并且没有简单的解决方案。

当然,零风险是一个神话,但是当某些浏览器在将“gimme-the-code”作为输入传递给 JSON.parse 时,可能会在整个网络上吐出用户的信用卡号,但你不会是一个责任。


推荐阅读