js常用的两种沙箱机制

211 阅读3分钟

为了安全,浏览器提供了很多安全机制,比如沙箱,渲染器进程其实也是一个沙箱,里面跑着js、css、html代码,渲染器进程通过IPC跟其它进程之间通讯,比如网络进程通过IPC把数据传递到渲染进程,当我们执行我们不了解的js代码时,我们需要一个安全的执行环境来保障执行的js代码不会危害到应用本身,就是经常说的js沙箱机制

使用with+new Function+proxy代理的方式来实
// js的沙箱机制,使用new Function函数执行
var out = "global"
// 使用new Function+with实现沙箱
function compileCode(code) {
  return new Function("obj", `with(obj) { ${code} }`)
}
// 创建沙箱
function createSandbox(code) {
  let current = {
    name: "may",
    obj: {
      age: 18
    }
  }
  let obj = proxyObj(current)
  compileCode(code).call(obj, obj)
}
// 通过proxy的has监听属性的访问
function proxyObj(obj) {
  let proxyObj = new Proxy(obj, {
    has: (target, key) => {
      // 如果代码访问中有"console", "Math", "Date"全局变量给予放行
      if (["console", "Math", "Date"].indexOf(key) >= 0) {
        return target[key]
      }
      // 访问的属性不在对象中,抛出异常
      if (!target.hasOwnProperty(key)) {
        throw new Error(key)
      }
      return target[key]
    }
  })
  return proxyObj
}
createSandbox(`console.log(name);console.log(out)`)

最后的输出结果 console.log(name) 输出了may,因为may在传入进去的current中 image.png console.log(out) 页面报错了, image.png

虽然全局变量中out变量,但是在current中没有对应out属性,触发了Proxy中的has函数中的,异常,成功劫持了访问全局变量

      // 访问的属性不在对象中,抛出异常
      if (!target.hasOwnProperty(key)) {
        throw new Error(key)
      }

whit+proxy+Function可以实现简单的沙箱机制,在proxy进行代理拦截,例如以下在沙箱中执行获取cookie的操作

 createSandbox(`console.log(name);console.log(document.cookie)`)

image.png 因为document没有放在白名单中放行,所以在使用document时会报错 image.png 在使用proxy还发现一个神奇的问题,var会出发has,但是let,const不会触发,不知道是什么原因,比如执行以以下代码不会报错

  createSandbox(`let age = 23;const nationality="china";console.log(age);console.log(nationality); console.log(name);`)

image.png 但是里面使用var命名后就会触发proxy的has函数,把 let换成var就报错了

   createSandbox(`var age = 23;var nationality="china";console.log(age);console.log(nationality); console.log(name);`)

image.png 使用Function+whit还是有漏洞,不如直接在沙箱中访问obj.age.__proto__是不会触发prxoy的has方法, 这样就可以直接重写原型上的方法,比如以下 image.png 在沙箱外层执行toString方法后就会实现XSS攻击 以下代码也会绕过沙箱

createSandbox(`console.log(a);console.log(document.cookie); new (() => { }).constructor("out ='inner';console.log(out);console.log(this)")()`)

这个还是有一定的漏洞

使用iframe

iframe有着天然的优势,会起到css,js的完全隔离,通过postMessage传递数据 起一个3000的主应用平台,里面嵌入一个iframe,访问3001的页面

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>host主应用</title>
</head>

<body>
    <h6>主应用</h6>
    <iframe src="http://localhost:3001/sandboxIframe.html" id="pluginBox" name="pluginBox" width="100%" height="800px"
        sandbox="allow-scripts allow-same-origin" frameborder="0"></iframe>
</body>
<script>
    let name = "host"
    window.addEventListener('message', messageHandler);
    function messageHandler(e) {
        console.log("接受到子级传递上来信息", e)
    }
    var iframe = document.getElementById('pluginBox');
    iframe.onload = function () {
        iframe.contentWindow.postMessage("132132", '*');
    }
</script>

</html>

通过contentWindow.postMessage 传递信息到嵌入的iframe中 嵌入的iframe页面内容

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>沙箱iframe</title>
</head>

<body>
    <p>嵌入的沙箱执行环境</p>
</body>
<script>
    (function () {
        window.addEventListener('message', messageHandler);
        function messageHandler(e) {
            console.log("接受到父级信息", e)
            window.top.postMessage("子级发送消息到父级", '*');
        }

    })()
</script>

</html>

image.png 这样在嵌入的iframe就无法访问主应用的数据,比如在iframe修改主应用中的变量就会报跨域问题 image.png这样就达到了沙箱的作用