低代码系列——js沙箱设计(二)

简介: 由于自己参与过低代码平台开发,所以希望能把我自己开发低代码中遇到的问题或者一些设计思路进行总结汇总,这是开始写的第一篇,也是比较基础的一篇,关于低代码平台的介绍会放在介绍篇章,这篇就不做过多介绍。

沙箱实现


沙箱实现步骤一般如下:

  • 解析代码,动态执行
  • 修改代码的作用域,避免进行向上全局查询
  • 创建全局对象的替代对象,避免污染全局对象
  • 执行代码

基于Proxy实现


前面基础知识将到Proxy是一个可以代理对象的方法,那么其实可以按照将一些全局对象做代理后放入到沙箱里。主要有两个步骤:

  • 使用new Function将代码字符串转为可执行函数
  • with生成局部作用域
  • 使用createFake方法生成替代对象fakeWindow
  • 使用Proxy代理拦截setget操作,更新到fakeWindow
  • 当沙箱不用时,将window重置回


简易沙箱示例代码如下:

const { complieCode2Fn, createFakeWindow } = require('./complieCode.js');
class ProxySandbox {
    constructor(global) {
        const rawGlobal = global;
        const fakeWindow = createFakeWindow(global);
        this.proxyBox = new Proxy(fakeWindow, {
            // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。
            has(target, key) {
                return true;
            },
            get(target, key, receiver) {
                console.log('get', key, target[key])
                // 加固,防止逃逸
                if (key === Symbol.unscopables) {
                    return undefined;
                }
                // 通过Reflect获取
                let temp = Reflect.get(target, key, receiver);
                if(!target.hasOwnProperty(key)){
                    temp = rawGlobal[key];
                }
                return temp;
            },
            set(target, key, newValue) {
                if (!target.hasOwnProperty(key) && fakeWindow.hasOwnProperty(key)) {
                    const descriptor = Object.getOwnPropertyDescriptor(sandbox, key);
                    const { writable, configurable, enumerable } = descriptor;
                    if (writable) {
                        // 中独有的属性如果可以写,同样需要复制到fakeWindow中
                        Object.defineProperty(target, p, {
                            configurable,
                            enumerable,
                            writable,
                            newValue,
                        });
                    }
                } else {
                    target[key] = newValue;
                }
                return true;
            }
        });
        return this;
    }
    excute(code){
        const fn = complieCode2Fn(code);
        return fn(this.proxyBox);
    }
}
const fakeBox = new ProxySandbox(global);
const code = `a = 1;console.log('a:', a);return a;`
console.log(fakeBox.excute(code));  // 输出a:1
console.log('在沙箱外获取沙箱内设置的全局值a:', a); // a is not defined

问题1: 如何防止Array.isArray重写后不影响顶部window?

```js
const code = `
    Array.isArray = ()=> true;
`
console.log(Array.isArray('a')); // 输出true 正常应该是false

生成fakeWindow对象时候,遍历内置Array,通过Object.freeze冻结其修改的可能性


问题2: 提前关闭 sandbox 的 with 语境,如 '} alert(this); {'  或者使用 eval 和 new Function 直接逃逸,如何解决?


解析code字符串,利用堆栈深度检测算法,将非法字符串 {}做简单计算 或者 eval 等关键字,然后报错处理


问题3: 如何解决修改原型链方法实现逃逸,既可以获取沙箱外的对象?

const code = `
    ({}).constructor.prototype.toString = () => {
        console.log('Escape')
    }
`
console.log(({}).toString()) // 输出Escape 正常应该输出[object Object]

这种只能在原型链上下功夫,将所有的原型链做一次封装,从而

基于iframe实现


利用iframe天然隔离机制,加上postMessage通讯机制,可以快速实现一个简易沙箱,具体步骤如下:

  • 创建一个iframe,获取其window作为替代对象
  • 将function执行放到iframe里,不会影响其沙箱外程序使用
class IframeSandbox {
    constructor() {
        // 创建一个 iframe 对象,取出其中的原生浏览器全局对象作为沙箱的全局对象
        const iframe = document.createElement('iframe', {url: 'about:blank'})
        document.body.appendChild(iframe)
        this.sandboxGlobal = iframe.contentWindow // 沙箱运行时的全局对象
    }
    excute(code){
        const fn = new this.sandboxGlobal.Function('sandbox', `with(sandbox){${code}}`)
        return fn(this.sandboxGlobal);
    }
}
const fakeBox = new IframeSandbox();
const code = `a = 1;console.log(a)`
console.log(fakeBox.excute(code));  // 输出a:1
console.log('在沙箱外获取沙箱内设置的全局值a:', a); // a is not defined

问题1: 需要解决其调用parent进行逃逸获取?

  • 最佳方案是通过Proxy对iframe的window对象进行拦截代理即可

基于ShadowRealm 提案的实现


ShadowRealm API 是一个新的 JavaScript 提案,它允许一个 JS 运行时创建多个高度隔离的 JS 运行环境(realm),每个 realm 具有独立的全局对象和内建对象。

通俗的说,这是JavaScript自带的沙箱API,你可以利用它快速实现上面需要通过proxy或iframe才能实现的隔离机制。


ShadowRealm声明:

declare class ShadowRealm {
  constructor();
  evaluate(sourceText: string): PrimitiveValueOrCallable;
  importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}
  • evaluate(sourceText: string) 同步执行代码字符串,类似 eval()
  • importValue(specifier: string, bindingName: string) 异步执行代码字符串

示例:

const sr = new ShadowRealm();
globalThis.test = 'test';
sr.evaluate(`globalThis.test = 'test ShadowRealm'; console.log(globalThis.test)`) // 输出 test ShadowRealm
// 创建一个文件  my-module.js
export function sum(...values) {
  return values.reduce((prev, value) => prev + value);
}
// main.js
const sr = new ShadowRealm();
const wrappedSum = await sr.importValue('./my-module.js', 'sum'); // 加载js模块,然后获取里面函数
console.log(wrappedSum('hi', ' ', 'folks', '!')); // 输出 hi folks !

其实再来实现一个沙箱就很简单了,因为ShadownRealm本身就是一个沙箱。

其他方案


Web Workers


Web Workers 代码运行在独立的进程中,通信是异步的,无法获取当前程序一些属性或共享状态,且有一点无法不支持 DOM 操作,必须通过 postMessage 通知 UI 主线程来实现

function workerSandbox(appCode) {
 var blob = new Blob([appCode]);
 var appWorker = new Worker(window.URL.createObjectURL(blob));
} 
workerSandbox('const a = 1;console.log(a);') // 输出1
console.log(a) // a not defined

vm 模块


Node.js 上的 vm 模块,与 ShadowRealm API 类似,但具有更多功能:缓存 JavaScript 引擎、拦截 import() 等等。但它唯一的缺点就是不能跨平台,只能在 Node.js 环境下使用。

const vm = require('vm');
const sandbox = {
    a: 1
};
vm.createContext(sandbox)
const whatIsThis = vm.runInContext(`
    a = 2 ;
`, sandbox);
console.log(sandbox) // 输出2

沙箱错误捕获


在完成沙箱主体后,还需要对沙箱内部错误进行捕获再次处理,从而不影响主体程序的执行。

这一块其实就在执行动态代码那里,做一层try/catch基本上可以完成的错误捕获。

沙箱逃逸


沙箱逃逸(Sandbox Escape),沙箱于作者而言是一种安全策略,但于使用者而言可能是一种束缚。脑洞大开的开发者们尝试用各种方式摆脱这种束缚,也称之为沙箱逃逸。


沙箱逃逸的几种方式:

  • 访问沙箱执行上下文中某个对象内部属性时,如:通过window.parent
  • 通过访问原型链实现逃逸


如何解决沙箱逃逸:

自定义解释器,分析源程序结构从而手动控制每一条语句的执行逻辑,如:Babel


简单的说,就是用JS去实现JS解释器,将每行代码进行解析,然后增加一些安全机制,从而避免非法代码入侵。


后续会专门写个文章去实现一个简单的JS解释器,这里就不做多阐述。更新后会放在这里链接。

参考资料



目录
相关文章
|
6月前
|
JavaScript 前端开发
如何用JS实现团队功能
如何用JS实现团队功能
50 0
|
Web App开发 JavaScript 前端开发
低代码系列——js沙箱设计(一)
由于自己参与过低代码平台开发,所以希望能把我自己开发低代码中遇到的问题或者一些设计思路进行总结汇总,这是开始写的第一篇,也是比较基础的一篇,关于低代码平台的介绍会放在介绍篇章,这篇就不做过多介绍。
311 0
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的百货中心供应链管理系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的百货中心供应链管理系统附带文章和源代码部署视频讲解等
42 7
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的中小型企业财务管理附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的中小型企业财务管理附带文章和源代码部署视频讲解等
31 5
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的业财票务一体项目管理系统 附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的业财票务一体项目管理系统 附带文章源码部署视频讲解等
41 0
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的中小企业人力资源管理系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的中小企业人力资源管理系统附带文章和源代码部署视频讲解等
45 1
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的中小型企业财务管理系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的中小型企业财务管理系统附带文章和源代码部署视频讲解等
38 1
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的家装一体化平台附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的家装一体化平台附带文章和源代码部署视频讲解等
36 0
基于ssm+vue.js+uniapp小程序的家装一体化平台附带文章和源代码部署视频讲解等
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的企业人才引进服务平台附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的企业人才引进服务平台附带文章和源代码部署视频讲解等
29 1
|
5月前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的涪陵区特色农产品交易系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的涪陵区特色农产品交易系统附带文章和源代码部署视频讲解等
26 0