什么是沙箱?
也称作:“沙箱/沙盒/沙盘”。沙箱是一种安全机制,为运行中的程序提供隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。沙箱能够安全的执行不受信任的代码,且不影响外部实际代码影响的独立环境。
javascript中沙箱的使用场景
- 在线代码编辑器:相信大家都有使用过一些在线代码编辑器,而这些代码的执行,基本都会放置在沙箱中,防止对页面本身造成影响
- vue 模板中表达式计算:vue 模板中表达式的计算被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不能够在模板表达式中试图访问用户定义的全局变量。
- vue 的服务端渲染:vue 的服务端渲染实现中,通过创建沙箱执行前端的 bundle 文件;在调用 createBundleRenderer 方法时候,允许配置 runInNewContext 为 true 或 false 的形式,判断是否传入一个新创建的 sandbox 对象以供 vm 使用
javascript中沙箱实现
一、跟浏览器宿主环境一致的沙箱实现
1.构建闭包环境
我们知道在 JavaScript 中的作用域(scope)只有全局作用域(global scope)、函数作用域(function scope)以及从 ES6 开始才有的块级作用域(block scope)。
1 | (function foo(){ |
2.原生浏览器对象模拟
模拟原生浏览器对象的目的是为了防止闭包环境,操作原生对象,篡改污染原生环境,完成模拟浏览器对象之前我们需要先关注几个不常用的 API。
eval
eval 函数可将字符串转换为代码执行,并返回一个或多个值;eval代码内部可以沿着作用域链往上找,篡改全局变量。1
2
3const b = eval("({name:'张三'})");
console.log(b.name);
console.log(eval( this.window === window )); // true
new Function
Function构造函数创建一个新的 Function 对象。直接调用这个构造函数可用于动态创建函数。
new Function ([arg1[, arg2[, …argN]],] functionBody)1
2
3
4
5
6
7
8
9
10
11const sum = new Function('a', 'b', 'return a + b');
console.log(sum(1, 2));//3
let a = 1;
function sandbox() {
let a = 2;
return new Function('return a;'); // 这里的 a 指向最上面全局作用域内的 1
}
const f = sandbox();
console.log(f());
与 eval 不同的是 Function 创建的函数只能在全局作用域中运行,它无法访问局部闭包变量,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量。new Function()是 eval()更好替代方案。它具有卓越的性能和安全性,但仍没有解决访问全局的问题。
with
with 是 JavaScript 中一个关键字,扩展一个语句的作用域链。它允许半沙盒执行。那什么叫半沙盒?语句将某个对象添加到作用域链的顶部,如果在沙盒中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出 ReferenceError。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 严格模式下以下代码运行会有问题
function sandbox(o) {
with (o){
c=2;
d=3;
// 0,1,2,3
// 每个变量首先被认为是一个局部变量,
// 如果局部变量与 obj 对象的某个属性同名,则这个局部变量会指向 obj 对象属性。
console.log(a,b,c,d);
}
}
const f = {
a:0,
b:1
}
sandbox(f);
console.log(f); // {a: 0, b: 1}
console.log(c,d); // 2,3。 c、d被泄露到window对象上
究其原理,with在内部使用in运算符。对于块内的每个变量访问,它都在沙盒条件下计算变量。如果条件是 true,它将从沙盒中检索变量。否则,就在全局范围内查找变量。但是 with 语句使程序在查找变量值时,都是先在指定的对象中查找。所以对于那些本来不是这个对象的属性的变量,查找起来会很慢,对于有性能要求的程序不适合(JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符)。with 也会导致数据泄漏(在非严格模式下,会自动在全局作用域创建一个全局变量)
with + new Function
配合 with 用法可以稍微限制沙盒作用域,先从当前的 with 提供对象查找,但是如果查找不到依然还能从更上面的作用域获取,污染或篡改全局环境1
2
3
4
5
6
7
8
9
10
11function sandbox (src) {
src = 'with (sandbox) {' + src + '}';
return new Function('sandbox', src);
}
const str = `
let a = 1;
window.name="张三";
console.log(a); // 打印:1
`;
sandbox(str)({});
console.log(window.name);//'张三'
存在的问题:
- eval 是全局对象的一个函数属性,执行的代码拥有着和应用中其它正常代码一样的的权限,它能访问「执行上下文」中的局部变量,也能访问所有「全局变量」,在这个场景下,它是一个非常危险的函数
- 使用 Function 构造器生成的函数,并不会在创建它的上下文中创建闭包,一般在全局作用域中被创建。当运行函数的时候,只能访问自己的本地变量和全局变量,不能访问 Function 构造器被调用生成的上下文的作用域
- with 一样的问题,它首先会在传入的对象中查找对应的变量,如果找不到就会往更上层的全局作用域去查找,所以也避免不了污染或篡改全局环境
Proxy
ES6 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程”1
2
3
4
5
6
7
8
9
10
11
12
13function evalute(code,sandbox) {
sandbox = sandbox || Object.create(null);
const fn = new Function('sandbox', `with(sandbox){return (${code})}`);
const proxy = new Proxy(sandbox, {
has(target, key) {
// 让动态执行的代码认为属性已存在
return true;
}
});
return fn(proxy);
}
evalute('1+2') // 3
evalute('console.log(1)') // Cannot read property 'log' of undefined
我们知道无论 eval 还是 function,执行时都会把作用域一层一层向上查找,如果找不到会一直到 global,那么利用 Proxy 的原理就是,让执行了代码在 sandobx 中找的到,以达到「防逃逸」的目的。
3.iframe的沙箱环境实现
1 | const parent = window; |
二、nodejs中沙箱实现
vm
VM是 Node.js 默认提供的一个内建模块,VM 模块提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。JavaScript 代码可以被编译并立即运行,或编译、保存然后再运行。
1 | const vm = require('vm'); |
vm2
总结
运行不信任的代码是非常困难的,只依赖软件模块作为沙箱技术,防止不受信任代码用于非正当用途是不得已的决定。这可能促使云上SAAS应用的不安全,因为通过逃逸出沙箱进程多个租户间的数据可能被访问(主进程数据获取),这样你就可能可以通过session,secret等来潜入其他租户。一个更安全的选择是依赖于硬件虚拟化,比如每个租户代码在独立的docker容器或AWS Lambada Function 中执行会是更好的选择。