js-浏览器沙箱

问题😮

现在有一个脚本编辑器,它会在浏览器端运行js脚本,我们需要限制脚本来操作window、localStorage、发请求等操作,确保改网页下其他项目的安全。

解决方案👹

1、eval或者new Function()🤢

1、这样可以用来执行脚本,但是非常不安全,并没有对上面的操作进行限制。

2、eval是直接在局部环境执行,可以通过作用域链修改局部变量;

3、new Function()是在全局作用域执行,无法改局部变量,但是可以改全局变量。

new Function语法:

//let func = new Function ([arg1, arg2, ...argN], functionBody);

let sum = new Function('a', 'b', 'return a + b');
sum(1, 2) // 3

2、web worker🤠

如果脚本不涉及dom有关的内容,可以采用web worker构建沙箱环境。

场景

  • 需要执行复杂的计算或数据处理

  • 不需要 DOM 操作

  • 希望提高应用的响应性

  • 需要并行处理任务

  • 可以在脚本中加载js脚本

特点

1、web Worker 无法直接访问 localStorage、sessionStorage 或其他 Window 对象的属性和方法。

2、可以通过重写fetch等方法对网络请求进行控制。

代码示例

index.html:

点击运行按钮,通过postMessage将脚本交给worker执行,当脚本执行fetch等操作时,会通过message将消息传递给主线程来执行,从而在主线程做限制。

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <button onclick="run()">运行</button>

  <script>
    const worker = new Worker('sandbox-worker.js');
    // 运行
    function run() {
      let code = `console.log("Hello from sandbox");
                  //console.log(document);
                  //console.log(window.a);
                  //alert("This is a test alert");
                  fetch('http://localhost:3000/getData').then(response => console.log(response)).catch(error => console.error(error));
                  console.log('6666');
                `
      executeInSandbox(code)
    }
    function executeInSandbox(code) {
      worker.postMessage({ type: 'sandbox-execute', code: code });
    }

    // 处理worker的消息,对console、alert、请求 做处理
    worker.onmessage = function (event) {
      switch (event.data.type) {
        case 'log':
          console.log.apply(console, event.data.data);
          break;
        case 'error':
          throw event.data.data
          break;
        case 'warn':
          console.warn.apply(console, event.data.data);
          break;
        case 'alert':
          alert(event.data.data);
          break;
        case 'fetch':
          // 处理 fetch 请求
          handleFetch(event.data.id, event.data.url, event.data.options);
          break;
      }
    };



    function handleFetch(id, url, options = { method: 'get' }) {
      // 实现 URL 过滤和请求处理
      if (isAllowedUrl(url)) {
        fetch(url, options)
          .then(response => response.json())
          .then(res => worker.postMessage({ type: 'fetchResult', data: { res, id } }))
          .catch(error => worker.postMessage({ type: 'fetchError', error: error.message }));
      } else {
        worker.postMessage({ type: 'fetchError', error: 'URL not allowed' });
      }
    }

    function isAllowedUrl(url) {
      // 实现 URL 检查逻辑
      // ...
      if (url === 'http://localhost:3000/getData') {
        return true
      }

    }
  </script>

</body>

</html>

sandbox.js:
        在worker中主要是执行脚本,拦截fetch、console等操作并交由主线程来执行。

        由于postMessage不能传递函数,我们需要用map记录resolve函数和本次请求的id,并将id和请求信息传给主线程来请求,等有结果后再将id和结果传给worker线程,最后通过id获取resolve函数并执行,从而完成请求操作。

let requestId = 0
const pendingRequests = new Map()

self.onmessage = function (event) {
  if (event.data.type === 'sandbox-execute') {
    try {
      const fn = new Function(event.data.code)
      fn()
    } catch (error) {
      self.postMessage({ type: 'error', data: error })
    }
  } else if (event.data.type === 'fetchResult') {
    const { id, res, error } = event.data.data
    const request = pendingRequests.get(id)
    if (request) {
      if (error) {
        request.reject(new Error(error))
      } else {
        request.resolve(res)
      }
      pendingRequests.delete(id)
    }
  }
}

// 重写importScripts加载脚本
const originalImportScripts = self.importScripts
self.importScripts = function (...urls) {
  const allowedScripts = ['https://trusted-cdn.com/script1.js', 'https://trusted-cdn.com/script2.js']

  for (const url of urls) {
    if (!allowedScripts.includes(url)) {
      throw new Error(`不允许加载脚本: ${url}`)
    }
  }

  // 如果所有脚本都被允许,则调用原始的 importScripts
  return originalImportScripts.apply(this, urls)
}

// 重写 console 方法
self.console = {
  log: (...args) => self.postMessage({ type: 'log', data: args }),
  error: (...args) => self.postMessage({ type: 'error', data: args }),
  warn: (...args) => self.postMessage({ type: 'warn', data: args })
}

// 模拟 alert
self.alert = (message) => self.postMessage({ type: 'alert', data: message })

// 重写 fetch
self.fetch = (url, options) => {
  return new Promise((resolve, reject) => {
    // 需要一个机制来处理主线程的响应
    const id = requestId++
    pendingRequests.set(id, { resolve, reject })

    self.postMessage({ type: 'fetch', url, options, id })
  })
}

3、iframe-sandbox🤓

如果包含dom操作,脚本结果需要显示到浏览器上,这时就可以用iframe来做沙箱了。

场景

  • 需要加载和隔离完整的第三方页面

  • 需要在沙箱中进行 DOM 操作

  • 要显示可视化内容

  • 需要更严格的安全隔离

特点

1、通过设置sandbox属性,可以alert(allow-modals)等操作。

2、能阻止访问父页面的变量(allow-same-origin)。

【界面显示后用户再手动改sandbox属性时无效的】

3、要对请求拦截,可以重写fetch方法。

代码示例

index.html
通过postMessage给iframe脚本,并给iframe设置一些sandbox属性。

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>安全的iframe脚本执行</title>
</head>

<body>
  <iframe id="sandboxedFrame" src="./shandbox-iframe.html" style="position: fixed;top: 200px;left: 200px;"
    sandbox="allow-scripts allow-popups"></iframe>

  <button onclick="run()">运行</button>
  <script>
    function run() {

      const iframe = document.getElementById('sandboxedFrame');
      iframe.contentWindow.postMessage({
        type: 'sandbox-run',
        data: {
          code: `
                console.log(11) //可以console
                // 创建一个新的 div 元素
                const newDiv = document.createElement('div');

                // 设置 div 的背景色为红色
                newDiv.style.backgroundColor = 'red';

                // 设置 div 的宽度和高度(可选)
                newDiv.style.width = '100px';
                newDiv.style.height = '100px';

                // 将 div 添加到 body 中
                document.body.appendChild(newDiv);
                `
        }
      }, '*')
    }

  </script>
</body>

</html>

sandbox-iframe.html:

接收脚本,从新fetch方法,在new Function时进行传递,则在脚本中可以使用重写的fetch方法。

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div>11</div>
  <script>
    window.addEventListener('message', (res) => {
      console.log(res.data);
      const { type, data } = res.data
      if (type !== 'sandbox-run') {
        return
      }

      // 保存原始的 fetch 方法
      const originalFetch = window.fetch;
      // 允许的源列表
      const allowedOrigins = ['https://api.trusted-site.com', 'https://another-trusted-site.com'];
      // 重写 fetch 方法
      const safeFetch = function (url, options) {
        const parsedUrl = new URL(url);

        if (!allowedOrigins.includes(parsedUrl.origin)) {
          return Promise.reject(new Error('不允许访问该源'));
        }
        return originalFetch(url, options);
      };
      // 使用 new Function 创建动态函数,并传递重写的 fetch 方法
      const dynamicFunction = new Function('fetch', data.code);
      // 调用动态函数,并传递重写的 fetch 方法
      dynamicFunction(safeFetch);
    })
  </script>

</body>

</html>

sandbox属性

1. allow-forms:
   - 允许 `iframe` 中的表单提交。
   - 默认情况下,`iframe` 中的表单提交是被禁止的。

2. allow-modals:
   - 允许使用模态窗口(如 `alert`、`prompt` 和 `confirm`)。
   - 默认情况下,`iframe` 中的模态窗口是被禁止的。

3. **`allow-orientation-lock`**:
   - 允许锁定屏幕方向。
   - 默认情况下,`iframe` 中的内容不能锁定屏幕方向。

4. **`allow-pointer-lock`**:
   - 允许使用 Pointer Lock API。
   - 默认情况下,`iframe` 中的内容不能使用 Pointer Lock API。

5. **`allow-popups`**:
   - 允许弹出窗口(如 `window.open`)。
   - 默认情况下,`iframe` 中的弹出窗口是被禁止的。

6. **`allow-popups-to-escape-sandbox`**:
   - 允许弹出窗口逃离沙箱限制。
   - 默认情况下,`iframe` 中的弹出窗口也会受到沙箱限制。

7. **`allow-presentation`**:
   - 允许使用 Presentation API。
   - 默认情况下,`iframe` 中的内容不能使用 Presentation API。

8. **`allow-same-origin`**:
   - 允许 `iframe` 中的内容被视为与主页面同源。
   - 默认情况下,`iframe` 中的内容被视为不同源,不能访问主页面的内容。

9. **`allow-scripts`**:
   - 允许执行脚本。
   - 默认情况下,`iframe` 中的脚本执行是被禁止的。

10. **`allow-top-navigation`**:
    - 允许 `iframe` 中的内容导航(加载)顶级浏览上下文。
    - 默认情况下,`iframe` 中的内容不能导航顶级浏览上下文。

END🤡

        我们可能还需要代码审查,日志记录等操作,但是这些技术只是防君子不防小人,哈哈。😹

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值