javascript代码_如何安全的运行第三方javascript代码(中)

d899a4765fcbf676e4740966052ab3dd.gif

(接上文)

dbb2010c92f864baabc068af8ac8d2d1.png在主线程上运行的含义

在我们深入进行第二种尝试之前,我们需要先退一步,并重新考察允许插件在主线程上运行到底意味着什么。毕竟,我们一开始并没有考虑它,因为我们知道这可能是危险的。在主线程上运行听起来很像eval(UNSAFE_CODE)方式。

在主线程上运行的好处是插件可以:

1.直接编辑文档而不是副本,避免加载时间问题。

2.可以运行复杂的组件更新和约束逻辑,而无需为代码置办两个副本。

3.在需要同步API时,可以使用同步API调用。这样的话,更新的加载或刷新就不会发生混淆。

4.以更直观的方式编写代码:插件只是自动执行用户可以使用UI手动执行的操作。

但是,这时我们又遇到了下列问题:

1.插件可挂起,但无法中断插件。

2.插件可以像figma.com一样发出网络请求。

3.插件可以访问和修改全局状态,例如修改UI,甚至可以执行恶意操作,例如修改({}).__proto__的值,从而危害所有新建的和现有的JavaScript对象。

经过斟酌之后,我们决定放弃第1项要求。当插件被冻结时,会影响Figma的稳定性。然而,我们的插件模型的工作原理是,它们只处理显式的用户操作。通过在插件运行时更改UI,冻结将始终被认为是插件所致。这也意味着插件无法“破坏”文档。

eval的危险性体现在哪些方面?

为了解决插件能够发出网络请求和访问全局状态的问题,我们必须首先确切地了解“通过eval函数执行任意JavaScript代码是危险的”这句话到底意味着什么。

对于某些只能进行7*24*60*60这样的算术运算的JavaScript变体,我们称之为SimpleScript,那么使用eval方法的话还是很安全的。

如果继续为SimpleScript添加其他特性,如变量赋值和if语句,使其更像编程语言,这时它仍然非常安全。归根结底,它本质上仍然归结为做算术。如果继续添加函数求值(function evaluation)特性,现在该语言就具备了λ演算和图灵完备性。

换句话说,JavaScript未必一定就是危险的。在最简化的形式中,它只是一种做算术的扩展方式。真正的危险源是它的输入和输出访问权限,其中包括网络访问、DOM访问等,即危险的是浏览器的应用程序接口。

我们知道,API都是全局变量,因此,我们需要隐藏全局变量!

隐藏全局变量

现在,隐藏全局变量在理论上听起来不错,但仅通过“隐藏”它们来创建安全的实现还是很困难的。例如,我们可以考虑删除window对象的所有属性,或将它们设置为null,但代码仍然可以访问全局值,例如({}).constructor。所以,找出泄漏全局变量值得所有可能方式是非常具有挑战性的。

相反,我们需要一些更强大的沙箱形式,使得这些全局变量值从一开始就不存在。

换句话说,JavaScript并不一定非常危险。

考虑前面介绍的仅支持算术的SimpleScript语言,大家可以试着编写一个算术运算程序。在该程序的任何合理实现中,SimpleScript将无法执行除算术之外的任何操作。

现在,我们继续扩展SimpleScript,使其支持更多语言功能,直到它变成JavaScript为止,现在,我们将该程序称为解释器,它决定了JavaScript(动态解释语言)的运行方式。

dbb2010c92f864baabc068af8ac8d2d1.png尝试#2:将JavaScript解释器编译为WebAssembly

对于像我们这样的小型创业公司来说,实现JavaScript编译器是不太现实的。相反,为了验证这种方法,我们采用了Duktape,这是一个用C++编写的轻量级JavaScript解释器,并将其编译为WebAssembly。

为了确认它是否有效,我们运行了test262测试,它是标准的JavaScript测试套件。它通过了所有ES5测试,只有少量不重要的测试失败了。要使用Duktape运行插件代码,我们需要使用编译为WebAssembly的解释器来调用eval函数。

这种方法有哪些特性?

这个解释器在主线程中运行,这意味着我们可以创建一个基于主线程的API。

它是安全的,因为Duktape不支持任何浏览器API,此外,它是作为WebAssembly运行的,而后者是一个无法访问浏览器API的沙箱环境。换句话说,默认情况下,插件代码只能通过显式的白名单API与外界进行通信。

它比常规JavaScript的速度要慢,因为这个解释器不支持JIT,但这并不重要。

它需要浏览器编译一个中等大小的WASM二进制文件,这需要一些开销。

默认情况下,浏览器调试工具无法使用,但我们花了一天时间为解释器实现了一个控制台,以验证它至少可以调试插件。

Duktape仅支持ES5,但在Web社区中,通常会使用[Babel](https://babeljs.io/)等工具交叉编译较新的JavaScript版本。

(提示:几个月后,Fabrice Bellard发布了[QuickJS](https://bellard.org/quickjs/),它原生支持ES6。)

现在,我们要编译一个JavaScript解释器!根据你作为程序员的爱好或审美倾向,您可能会想:

这太棒了!

或者

……这是要搞啥?还要自己搞JavaScript引擎,那操作系统是不是也要自己搞一个呀?

当然,这些质疑声是非常正常的!除非我们有绝对的必要,否则最好避免重新实现浏览器。在实现整个渲染系统方面,我们花费的大量的精力,因为这对于性能和跨浏览器支持来说是非常必要的,并且令人高兴的是,我们的确做到了,但我们仍然要郑重对读者说一声:不重新发明轮子。

注意,这并非我们最终采用的方法,因为后面还有更好的方法。那我们为什么要在这里介绍它呢?这是因为,这对于理解我们最终沙箱模型来说是非常有帮助的,毕竟我们的模型是非常复杂的。

dbb2010c92f864baabc068af8ac8d2d1.png尝试#3:Realms

虽然编译JS解释器是一种很有前途的方法,但除此之外,还有一个方法非常需要考虑——Realms shim技术,其创建者为Agoric。

这项技术将创建沙箱和支持插件描述为潜在的用例。这真是一种前途无量的描述方法!Realms API看起来大致如下所示:

let g = window; // outer global

let r = new Realm(); // realm object

let f = r.evaluate("(function() { return 17 })");

f() === 17 // true

Reflect.getPrototypeOf(f) === g.Function.prototype // false

Reflect.getPrototypeOf(f) === r.global.Function.prototype // true

这种技术实际上可以使用现有的JavaScript特性来实现,尽管这些特性鲜为人知。沙箱的一项任务就是隐藏全局变量。这个shim库的核心功能大致如下所示:

function simplifiedEval(scopeProxy, userCode) {

 'use strict'

 with (scopeProxy) {

    eval(userCode)

  }

}

这是用于演示目的的简化版本;真实版本中还是有一些细微差别的。但是,它展示了其中最关键的部分:with语句和Proxy对象。

其中,with(obj)语句创建了一个作用域,在该作用域内可以使用obj的属性查找变量。在这个例子中,我们可以将变量PI、cos和sin解析为Math对象的属性。另一方面,console并不是Math的属性,因此需要在全局作用域内进行解析。

with (Math) {

  a = PI * r * r

  x = r * cos(PI)

 y = r * sin(PI)

 console.log(x,  y)

}

代理对象是JavaScript对象最动态的一种形式。

· 最基本的JavaScript对象可以通过访问obj.x返回属性的值。

· 更高级的JavaScript对象可以具有getter属性,用于返回函数的计算结果。实际上,访问obj.x就是调用x的getter属性。

· 代理可以通过运行函数get来访问任意属性。

对于下面的代理(由于它仅用于演示,所以进行了相应的简化处理)来说,当我们尝试访问它的任何属性时,都将返回undefined,而不是对象whitelist中的属性值。

const scopeProxy = new Proxy(whitelist, {

  get(target, prop) {

    // here, target === whitelist

    if (prop in target) {

      return target[prop]

    }

    return undefined

  }

}

现在,当您将这个代理用作with对象的参数时,它将拦截所有变量的解析过程,并且永远不会使用全局作用域来解析变量:

with (proxy) {

  document // undefined!

 eval("xhr") // undefined!

}

不过,这种方法仍然可以通过诸如({}).constructor之类的表达式来访问某些全局变量。此外,沙箱也确实需要访问一些全局变量。例如,Object是一个全局对象,并且许多合法的JavaScript代码(例如Object.keys)都需要用到它。

为了让插件既能够访问这些全局变量又不会捅娄子,Realms沙箱支持通过创建同源的iframe来实例化所有这些全局变量的新副本。当然,这个iframe不会像在尝试#1中那样用作沙箱。并且,同源iframe不会受CORS的限制。

相反,当在与父文档同源的情况下创建时:

1.它附带了所有全局变量的单独副本,例如Object.prototype等。

2.可以从父文档访问这些全局变量。

8dffcf0677c68d0fe7ae246e267d186c.png

这些全局变量将被放进代理对象的“白名单”中,这样的话,插件就可以访问它们了。最后,这个新的还附带了一个新的“eval”函数副本,它与现有的函数有一个重要的区别:即使只有通过({}).constructor这样的语法才能访问的内置值,也将会解析为iframe的副本。

这种基于Realms的沙箱方法有许多优秀的属性:

它在主线程上运行。

速度很快,因为它可以使用浏览器的JavaScript JIT来执行代码。

浏览器开发工具仍可以正常使用。

即使如此,我们还面临令一个非常重要的问题:这种方法安全吗?

(未完待续)

053ff856dbc913eb986365969a4118ac.png

48a00edd7ce0aa7cdfe6f923877a7f68.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值