动手写 js 沙箱
本文由云+社区发表
作者:ivweb villainthr
市面上现在流行两种沙箱模式,一种是使用iframe,还有一种是直接在页面上使用new Function + eval进行执行。 殊途同归,主要还是防止一些Hacker们 吃饱了没事干,收别人钱来 Hack 你的网站。 一般情况, 我们的代码量有60%业务+40%安全. 剩下的就看天意了。接下来,我们来一步一步分析,如果做到在前端的沙箱.文末 看俺有没有心情放一个彩蛋吧。
直接嵌套
这种方式说起来并不是什么特别好的点子,因为需要花费比较多的精力在安全性上.
eval执行
最简单的方式,就是使用eval进行代码的执行 eval('console.log("a simple script");');
但,如果你是直接这么使用的话, congraduations... do die... 因为,eval 的特性是如果当前域里面没有,则会向上遍历.一直到最顶层的global scope 比如window.以及,他还可以访问closure内的变量.看demo:
function Auth(username) { var password = "trustno1"; this.eval = function(name) { return eval(name) } // 相当于直接this.name } auth = new Auth("Mulder") console.log(auth.eval("username")); // will print "Mulder" console.log(auth.eval("password")); // will print "trustno1"
那有没有什么办法可以解决eval这个特性呢? 答: 没有. 除非你不用 ok,那我就不用. 我们这里就可以使用new Function(..args,bodyStr) 来代替eval。
new Function
new Function就是用来,放回一个function obj的. 用法参考:new Function. 所以,上面的代码,放在new Function中,可以写为: new Function('console.log("a simple script");')();
这样做在安全性上和eval没有多大的差别,不过,他不能访问closure的变量,即通过this来调用,而且他的性能比eval要好很多. 那有没有办法解决global var的办法呢? 有啊... 只是有点复杂先用with,在用Proxy
with
with这个特性,也算是一个比较鸡肋的,他和eval并列为js两大SB特性. 不说无用, bug还多,安全性就没谁了... 但是, with的套路总是有人喜欢的.在这里,我们就需要使用到他的特性.因为,在with的scope里面,所有的变量都会先从with定义的Obj上查找一遍。
var a = { c:1 } var c =2; with(a){ console.log(c); //等价于c.a }
所以,第一步改写上面的new Function(),将里面变量的获取途径控制在自己的手里。
function compileCode (src) { src = 'with (sandbox) {' + src + '}' return new Function('sandbox', src) }
这样,所有的内容多会从sandbox这个str上面获取,但是找不到的var则又会向上进行搜索. 为了解决这个问题,则需要使用: proxy
proxy
es6 提供的Proxy特性,说起来也是蛮牛逼的. 可以将获取对象上的所有方式改写.具体用法可以参考: 超好用的proxy. 这里,我们只要将has给换掉即可. 有的就好,没有的就返回undefined
function compileCode (src) { src = 'with (sandbox) {' + src + '}' const code = new Function('sandbox', src) return function (sandbox) { const sandboxProxy = new Proxy(sandbox, {has}) return code(sandboxProxy) } } // 相当于检查 获取的变量是否在里面 like: 'in' function has (target, key) { return true } compileCode('log(name)')(console);
这样的话,就能完美的解决掉 向上查找变量的烦恼了。 另外一些,大神,发现在新的ECMA里面,有些方法是不会被with scope 影响的. 这里,主要是通过Symbol.unscopables 这个特性来检测的.比如:
Object.keys(Array.prototype[Symbol.unscopables]); // ["copyWithin", "entries", "fill", "find", "findIndex", // "includes", "keys", "values"]
不过,经过本人测试发现也只有Array.prototype上面带有这个属性... 尴尬... 所以,一般而言,我们可以加上 Symbol.unscopables, 也可以不加。
// 还是加一下吧 function compileCode (src) { src = 'with (sandbox) {' + src + '}' const code = new Function('sandbox', src) return function (sandbox) { const sandboxProxy = new Proxy(sandbox, {has, get}) return code(sandboxProxy) } } function has (target, key) { return true } function get (target, key) { // 这样,访问Array里面的 like, includes之类的方法,就可以保证安全... 算了,就当我没说,真的没啥用... if (key === Symbol.unscopables) return undefined return target[key] }
现在,基本上就可以宣告你的代码是99.999% 的5位安全数.(反正不是100%就行)
设置缓存
如果上代码,每次编译一次code时,都会实例一次Proxy, 这样做会比较损性能. 所以,我们这里,可以使用closure来进行缓存。 上面生成proxy代码,改写为:
function compileCode(src) { src = 'with (sandbox) {' + src + '}' const code = new Function('sandbox', src) function has(target, key) { return true } function get(target, key) { if (key === Symbol.unscopables) return undefined return target[key] } return (function() { var _sandbox, sandboxProxy; return function(sandbox) { if (sandbox !== _sandbox) { _sandbox = sandbox; sandboxProxy = new Proxy(sandbox, { has, get }) } return code(sandboxProxy) } })() }
不过上面,这样的缓存机制有个弊端,就是不能存储多个proxy. 不过,你可以使用Array来解决,或者更好的使用Map. 这里,我们两个都不用,用WeakMap来解决这个problem. WeakMap 主要的问题在于,他可以完美的实现,内部变量和外部的内容的统一. WeakMap最大的特点在于,他存储的值是不会被垃圾回收机制关注的. 说白了, WeakMap引用变量的次数是不会算在引用垃圾回收机制里, 而且, 如果WeakMap存储的值在外部被垃圾回收装置回收了,WeakMap里面的值,也会被删除--同步效果.所以,毫无意外, WeakMap是我们最好的一个tricky. 则,代码可以写为:
const sandboxProxies = new WeakMap() function compileCode(src) { src = 'with (sandbox) {' + src + '}' const code = new Function('sandbox', src) function has(target, key) { return true } function get(target, key) { if (key === Symbol.unscopables) return undefined return target[key] } return function(sandbox) { if (!sandboxProxies.has(sandbox)) { const sandboxProxy = new Proxy(sandbox, { has, get }) sandboxProxies.set(sandbox, sandboxProxy) } return code(sandboxProxies.get(sandbox)) } }
差不多了, 如果不嫌写的丑,可以直接拿去用.(如果出事,纯属巧合,本人概不负责).
接着,我们来看一下,如果使用iframe,来实现代码的编译. 这里,Jsfiddle就是使用这种办法.
iframe 嵌套
最简单的方式就是,使用sandbox属性. 该属性可以说是真正的沙盒... 把sandbox加载iframe里面,那么,你这个iframe基本上就是个标签而已... 而且支持性也挺棒的,比如IE10. <iframe sandbox src=”...”></iframe>
这样已添加,那么下面的事,你都不可以做了:
1. script脚本不能执行 2. 不能发送ajax请求 3. 不能使用本地存储,即localStorage,cookie等 4. 不能创建新的弹窗和window, 比如window.open or target="_blank" 5. 不能发送表单 6. 不能加载额外插件比如flash等 7. 不能执行自动播放的tricky. 比如: autofocused, autoplay
看到这里,我也是醉了。 好好的一个iframe,你这样是不是有点过分了。 不过,你可以放宽一点权限。在sandbox里面进行一些简单设置 <iframe sandbox=”allow-same-origin” src=”...”></iframe>
常用的配置项有:
配置 | 效果 |
---|---|
allow-forms | 允许进行提交表单 |
allow-scripts | 运行执行脚本 |
allow-same-origin | 允许同域请求,比如ajax,storage |
allow-top-navigation | 允许iframe能够主导window.top进行页面跳转 |
allow-popups | 允许iframe中弹出新窗口,比如,window.open,target="_blank" |
allow-pointer-lock | 在iframe中可以锁定鼠标,主要和鼠标锁定有关 |
可以通过在sandbox里,添加允许进行的权限. <iframe sandbox=”allow-forms allow-same-origin allow-scripts” src=”...”></iframe>
这样,就可以保证js脚本的执行,但是禁止iframe里的javascript执行top.location = self.location。 更多详细的内容,请参考: please call me HR.
接下来,我们来具体讲解,如果使用iframe来code evaluation. 里面的原理,还是用到了eval.
iframe 脚本执行
上面说到,我们需要使用eval进行方法的执行,所以,需要在iframe上面添加上, allow-scripts的属性.(当然,你也可以使用new Function, 这个随你...) 这里的框架是使用postMessage+eval. 一个用来通信,一个用来执行. 先看代码:
<!-- frame.html --> <!DOCTYPE html> <html> <head> <title>Evalbox's Frame</title> <script> window.addEventListener('message', function (e) { // 相当于window.top.currentWindow. var mainWindow= e.source; var result = ''; try { result = eval(e.data); } catch (e) { result = 'eval() threw an exception.'; } // e.origin 就是原来window的url mainWindow.postMessage(result, e.origin); }); </script> </head> </html>
这里顺便插播一下关于postMessage的相关知识点.
postMessage 讲解
postMessage主要做的事情有三个:
1.页面和其打开的新窗口的数据传递 2.多窗口之间消息传递 3.页面与嵌套的iframe消息传递
具体的格式为: otherWindow.postMessage(message, targetOrigin, [transfer]);
message是传递的信息,targetOrigin指定的窗口内容,transfer取值为Boolean 表示是否可以用来对obj进行序列化,相当于JSON.stringify, 不过一般情况下传obj时,会自己先使用JSON进行seq一遍. 具体说一下targetOrigin. targetOrigin的写入格式一般为URI,即, protocol+host. 另外,也可以写为*
. 用来表示 传到任意的标签页中. 另外,就是接受端的参数.接受传递的信息,一般是使用window监听message
事件.
window.addEventListener("message", receiveMessage, false); function receiveMessage(event) { var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object. if (origin !== "http://example.org:8080") return; // ... }
event里面,会带上3个参数:
- data: 传递过来的数据. e.data
- origin: 发送信息的URL, 比如: https://example.org
- source: 发送信息的源页面的window对象. 我们实际上只能从上面获取信息.
该API常常用在window和iframe的信息交流当中. 现在,我们回到上面的内容.
<!-- frame.html --> <!DOCTYPE html> <html> <head> <title>Evalbox's Frame</title> <script> window.addEventListener('message', function (e) { // 相当于window.top.currentWindow. var mainWindow= e.source; var result = ''; try { result = eval(e.data); } catch (e) { result = 'eval() threw an exception.'; } // e.origin 就是原来window的url mainWindow.postMessage(result, e.origin); }); </script> </head> </html>
iframe里面,已经做好文档的监听,然后,我们现在需要进行内容的发送.直接在index.html写入:
// html部分 <textarea id='code'></textarea> <button id='safe'>eval() in a sandboxed frame.</button> // 设置基本的安全特性 <iframe sandbox='allow-scripts' id='sandboxed' src='frame.html'></iframe> // js部分 function evaluate() { var frame = document.getElementById('sandboxed'); var code = document.getElementById('code').value; frame.contentWindow.postMessage(code, '/'); // 只想同源的标签页发送 } document.getElementById('safe').addEventListener('click', evaluate); // 同时设置接受部分 window.addEventListener('message', function (e) { var frame = document.getElementById('sandboxed'); // 进行信息来源的验证 if (e.origin === "null" && e.source === frame.contentWindow) alert('Result: ' + e.data); });
实际demo可以参考:H5 ROCK
常用的两种沙箱模式这里差不多讲解完了. 开头说了文末有个彩蛋,这个彩蛋就是使用nodeJS来做一下沙箱. 比如像 牛客网的代码验证,就是放在后端去做代码的沙箱验证.
彩蛋--nodeJS沙箱
使用nodeJS的沙箱很简单,就是使用nodeJS提供的VM Module即可. 直接看代码吧:
const vm = require('vm'); const sandbox = { a: 1, b: 1 }; const script= new vm.Script('a + b'); const context = new vm.createContext(sandbox); script.runInContext(context);
在vm构建出来的sandbox里面,没有任何可以访问的全局变量.除了基本的syntax.
此文已由腾讯云+社区在各渠道发布
获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
深度解析vue组件之间通信【8种方式】实例
这篇文章主要介绍了vue组件之间通信方式,结合实例形式总结分析了vue.js的8种组件通信方式与相关操作注意事项,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。 对于vue来说,组件之间的消息传递是非常重要的,下面是我对组件之间消息传递的各种方式的总结,总共有8种方式。 1. props和$emit 父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的。 Vue.component('child',{ data(){ return { mymessage:this.message } }, template:` <div> <input type="text" v-model="mymessage" @input="passData(mymessage)"> </div> `, props:['message'],//得到父组件传递过来的数据 methods:{ passData(val){ //触发父组件中的事件 this.$emit('getChi...
- 下一篇
如何优雅的设计和使用缓存?
背景 在之前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将介绍如何利用好缓存。 1.确认是否需要缓存 在使用缓存之前,需要确认你的项目是否真的需要缓存。使用缓存会引入的一定的技术复杂度,后文也将会一一介绍这些复杂度。一般来说从两个方面来个是否需要使用缓存: CPU占用:如果你有某些应用需要消耗大量的cpu去计算,比如正则表达式,如果你使用正则表达式比较频繁,而其又占用了很多CPU的话,那你就应该使用缓存将正则表达式的结果给缓存下来。 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。笔者曾经有个服务,被很多其他服务调用,其他时间都还好,但是在每天早上10点的时候总是会报出数据库连接池连接不够的报警,经过排查,发现有几个服务选择了在10点做定时任务,大量的请求打过来,DB连接池不够,从而报出连接池不够的报警。这个时候有几个选择,我们可以通过扩容机器来解决,也可以通过增加数据库连接池...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- Red5直播服务器,属于Java语言的直播服务器