JS浏览器前端沙盒

317 阅读1分钟

本文纯属实验性质,如果有误请各位大佬指正。因使用本文所述的方法和技术所产生的安全风险,本文作者概不负责。

JS的其中一个妖艳而又危险的功能,在于其可以动态解析并执行字符串代码。例如,eval('Math.sqrt(4)')的值为2

在实际业务中,我们有时会需要在浏览器前端执行不受信任的动态脚本。这件事本身是十分危险的,如果不是业务迫不得已请避免使用这个技术。相比于直接使用eval()或者new Function()一类的动态执行方案,我们可以通过使用<iframe>标签提高动态执行脚本的安全性。

这里我编写了一个函数,用于创建沙盒并执行传入的动态脚本:

function run (fcode) {
  const instance = {}
  const iframe = document.createElement('iframe')
  iframe.sandbox = 'allow-scripts'
  iframe.width = iframe.height = 0
  iframe.src = 'data:text/html;charset=utf-8,' + encodeURI(`<script>(async()=>{${fcode}})().then(r=>parent.postMessage(r,'${window.location.origin}'))</script>`)
  instance.iframe = iframe
  instance.onReturn = () => {}
  window.onmessage = e => {
    if (e.target !== window && e.origin !== 'null') return
    instance.status = 'done'
    instance.result = e.data
    instance.onReturn(e.data)
    document.body.removeChild(iframe)
  }
  document.body.appendChild(iframe)
  instance.status = 'running'
  return instance
}

在这个函数中,我们创建了一个iframe元素,设置其为可执行脚本的沙盒,隐藏宽高,将需要执行的代码(一个函数体)嵌入iframe,执行iframe并监听来自iframe的消息。

在使用时,我们可以传入任意字符串代码,例如:

let ins = run(`let sum = 0
for (let i = 1; i <= 10; i++) {
  sum += i
}
return sum`)

run函数会返回执行实例ins,描述当前执行的状态。执行完毕以后,ins.result的值就是传入代码的返回值。例如,上例执行完毕以后:

ins
{iframe: iframe, status: 'done', result: 55}

注意:浏览器需要支持async/await,传入的函数体代码可以使用await

使用此沙盒的前端网页不能应用CSP规则限制script-src

使用此沙盒不能避免死循环等运行破坏性攻击!