# 什么是沙箱

在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。

  • 我们开发的页面程序运行在浏览器中,程序只能修改浏览器允许我们修改的那部分接口,我们无法通过这段脚本影响到浏览器之外的状态,在这个场景下浏览器本身就是一个沙箱。
  • 浏览器中每个标签页运行一个独立的网页,每个标签页之间互不影响,这个标签页就是一个沙箱。

# 沙箱的应用场景

  • 执行 JSONP 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码。

  • Vue 模板表达式的计算是运行在一个沙盒之中的,在模板字符串中的表达式只能获取部分全局对象,这一点官方文档有提到,详情可参阅源码。

  • 在线代码编辑器,如 CodeSanbox 等在线代码编辑器在执行脚本时都会将程序放置在一个沙箱中,防止程序访问/影响主页面。

  • 许多应用程序提供了插件(Plugin)机制,开发者可以书写自己的插件程序实现某些自定义功能。开发过插件的同学应该知道开发插件时会有很多限制条件,这些应用程序在运行插件时需要遵循宿主程序制定的运行规则,插件的运行环境和规则就是一个沙箱。例如下图是 Figma 插件的运行机制:

  • 总而言之,只要遇到不可信的第三方代码,我们就可以使用沙箱将代码进行隔离,从而保障外部程序的稳定运行。如果不做任何处理地执行不可信代码,在前端中最直观的副作用/危害就是污染、篡改全局 window 状态,影响主页面功能甚至被 XSS 攻击

# 沙箱的实现方式

  1. 最简陋的沙箱
// 执行上下文对象
const ctx = 
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 最简陋的沙箱
function poorestSandbox(code, ctx) {
    eval(code) // 为执行程序构造了一个函数作用域
}

// 待执行程序
const code = `
    ctx.foo = 'bar'
    ctx.func(ctx.foo)
`
poorestSandbox(code, ctx) // bar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. 简陋的沙箱(With)
// 执行上下文对象
const ctx = {
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 非常简陋的沙箱
function veryPoorSandbox(code, ctx) {
    with(ctx) { // Add with
        eval(code)
    }
}

// 待执行程序
const code = `
    foo = 'bar'
    func(foo)
`
veryPoorSandbox(code, ctx) // bar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 不太简陋(With + Proxy)
// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
  code = 'with(globalObj) {' + code + '}'
  return new Function('globalObj', code)
}


// 可访问全局作用域的白名单列表
const access_white_list = ['Math', 'Date']


// 待执行程序
const code = `
    Math.random()
    location.href = 'xxx'
    func(foo)
`

// 执行上下文对象
const ctx = {
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 执行上下文对象的代理对象
const ctxProxy = new Proxy(ctx, {
    has: (target, prop) => { // has 可以拦截 with 代码块中任意属性的访问
      if (access_white_list.includes(prop)) { // 在可访问的白名单内,可继续向上查找
          return target.hasOwnProperty(prop)
      }

      if (!target.hasOwnProperty(prop)) {
          throw new Error(`Invalid expression - ${prop}! You can not do that!`)
      }

      return true
    }
})

// 没那么简陋的沙箱

function littlePoorSandbox(code, ctx) {

    withedYourCode(code).call(ctx, ctx) // 将 this 指向手动构造的全局代理对象

}

littlePoorSandbox(code, ctxProxy) 

// Uncaught Error: Invalid expression - location! You can not do that!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  1. 天然的优质沙箱(iframe)

听到上面这个问题 iframe 直呼内行,iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离。在 iframe 中运行的脚本程序访问到的全局对象均是当前 iframe 执行上下文提供的,不会影响其父页面的主体功能,因此使用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法。 试想一个这样的场景:一个页面中有多个沙箱窗口,其中有一个沙箱需要与主页面共享几个全局状态(eg: 点击浏览器回退按钮时子应用也会跟随着回到上一级),另一个沙箱需要与主页面共享另外一些全局状态(eg: 共享 cookie 登录态)。 虽然浏览器为主页面和 iframe 之间提供了 postMessage 等方式进行通信,但单单使用 iframe 来实现这个场景是比较困难且不易维护的。

  1. 应该能用的沙箱(With + Proxy + iframe)

    • 利用 iframe 对全局对象的天然隔离性,将 iframe.contentWindow 取出作为当前沙箱执行的全局对象
    • 将上述沙箱全局对象作为 with 的参数限制内部执行程序的访问,同时使用 Proxy 监听程序内部的访问。
    • 维护一个共享状态列表,列出需要与外部共享的全局状态,在 Proxy 内部实现访问控制。
// 沙箱全局代理对象类
class SandboxGlobalProxy {
    constructor(sharedState) {
        // 创建一个 iframe 对象,取出其中的原生浏览器全局对象作为沙箱的全局对象
        const iframe = document.createElement('iframe', {url: 'about:blank'})
        document.body.appendChild(iframe)
        const sandboxGlobal = iframe.contentWindow // 沙箱运行时的全局对象
        return new Proxy(sandboxGlobal, {
            has: (target, prop) => { // has 可以拦截 with 代码块中任意属性的访问
                if (sharedState.includes(prop)) { // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
                    return false
                }

                if (!target.hasOwnProperty(prop)) {
                    throw new Error(`Invalid expression - ${prop}! You can not do that!`)
                }
                return true
            }
        })
    }

}

function maybeAvailableSandbox(code, ctx) {
    withedYourCode(code).call(ctx, ctx)
}

const code_1 = `

    console.log(history == window.history) // false

    window.abc = 'sandbox'

    Object.prototype.toString = () => {

        console.log('Traped!')

    }

    console.log(window.abc) // sandbox

`

const sharedGlobal_1 = ['history'] // 希望与外部执行环境共享的全局对象

const globalProxy_1 = new SandboxGlobalProxy(sharedGlobal_1)

maybeAvailableSandbox(code_1, globalProxy_1)
window.abc // undefined 
Object.prototype.toString() // [object Object] 并没有打印 Traped
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  1. “无瑕疵”的沙箱(Customize Interpreter)

通过上述的种种方式来实现一个沙箱或多或少存在一些缺陷,那是否存在一个趋于完备的沙箱呢? 其实有不少开源库已经在做这样一件事情,也就是分析源程序结构从而手动控制每一条语句的执行逻辑,通过这样一种方式无论是指定程序运行时的上下文环境还是捕获妄想逃脱沙箱控制的操作都是在掌控范围内的。实现这样一个沙箱本质上就是实现一个自定义的解释器。

function almostPerfectSandbox(code, ctx, illegalOperations) {

    return myInterpreter(code, ctx, illegalOperations) // 自定义解释器

}
1
2
3
4
5

# 参考

浅析 JavaScript 沙箱机制 (opens new window)