评估代码的正确方法

Proper way to evaluate code

所以我正在构建一个小应用程序,您可以在其中评估一些 JavaScript 代码,但我遇到了一个巨大的 "moral" 问题:

一开始我想用eval,但发现它的危险性,所以我很快就找了一个替代品。

我能找到的最接近的东西是函数构造函数,但一方面它不评估简单的代码片段,例如 2 + 3,因为它需要一个 return 语句,而eval 没有,而且它在安全方面也没有比 eval 好多少(至少从我收集到的信息来看)。

是否有任何其他方法可以像评估代码一样评估字符串?

如果要计算 JavaScript 代码,请使用 eval。危险吗?是的。但这只是因为评估 JavaScript 是危险的。没有安全的方法来评估 JavaScript。如果要评估 JavaScript,请使用 eval.

采取一切可能的安全预防措施。如果不了解有关您想要支持的内容以及您计划如何实施的更多详细信息,就不可能知道您应该采取哪些安全预防措施。

这可能有用:

Is It Possible to Sandbox JavaScript Running In the Browser?

https://github.com/google/caja

请永远不要使用 eval 无论如何,有更好的选择。使用 new function 而不是 evaleval 是邪恶的,这一点毫无疑问,但大多数人都跳过了 eval 最邪恶的方面:它使您可以访问本地范围内的变量。回到 90 年代,在 JIST 编译概念出现之前,eval 听起来是个好主意(确实如此):只需动态插入一些额外的行到您已经逐行执行的代码中.这也意味着 evals 并没有真正减慢速度。然而,如今 JIST 编译 eval 语句对 JIST 编译器来说非常费力,它在内部完全删除了变量名的概念。对于 JIST 编译器,为了评估 eval 语句,它必须找出其所有变量的存储位置,并将它们与 evaled 语句中找到的未知全局变量进行匹配。如果您真正掌握技术,问题会更深。

但是,使用 new function,JIST 编译器不必执行任何昂贵的变量名查找:整个代码块是自包含的并且在全局范围内。例如,采用以下非常低效的 eval 片段。请注意,这只是为了作为示例。在生产代码中,您甚至不应该使用 eval 或 new Function 从内容已知的字符串生成函数。

var a = {
    prop: -1
};
var k = eval('(function(b){return a.prop + b;})');
alert( k(3) ); // will alert 2

现在,让我们来看看更好的 new Function 替代方案。

var a = {
    prop: -1
};
var k = (new Function('a', 'b', 'return a.prop + b')).bind(undefined, a);
alert( k(3) ); // will alert 2

注意到区别了吗?有一个主要的:eval 在本地范围内执行,而 new Function 在全局范围内执行。

现在,进入下一个问题:安全性。很多人都在谈论安全是多么困难,是的,使用 eval 几乎是不可能的(e.x。如果你将整个代码包装在一个沙盒函数中,那么你所要做的就是过早地结束功能并启动一个新的功能以在当前范围内自由执行代码)。但是,使用 new Function,您可以轻松地(但不是最有效地)将任何东西沙箱化。看下面的代码。

var whitelist = ['Math', 'Number', 'Object', 'Boolean', 'Array'];
var blacklist = Object.getOwnPropertyNames(window).filter(function(x){
    return whitelist.indexOf(x) === -1 && !/^[^a-zA-Z]|\W/.test(x)
});
var listlen = blacklist.length;
var blanklist = (new Array(listlen+1)).fill(undefined);
function sandboxed_function(){
    "use-strict";
    blacklist.push.apply(blacklist, arguments);
    blacklist[blacklist.length-1] = 
        '"use-strict";' + arguments[arguments.length-1];
    var newFunc = Function.apply(
        Function,
        blacklist
    );
    blacklist.length = listlen;
    return newFunc.bind.apply(newFunc, blanklist);
}

然后,fiddle绕过白名单,随心所欲地获取它,然后就可以像new Function一样使用sandboxed_function了。例如:

var whitelist = ['Math', 'Number', 'Object', 'Boolean', 'Array'];
var blacklist = Object.getOwnPropertyNames(window).filter(function(x){
    return whitelist.indexOf(x) === -1 && !/^[^a-zA-Z]|\W/.test(x)
});
var listlen = blacklist.length;
var blanklist = (new Array(listlen+1)).fill(undefined);
function sandboxed_function(){
    "use-strict";
    blacklist.push.apply(blacklist, arguments);
    blacklist[blacklist.length-1] = 
        '"use-strict";' + arguments[arguments.length-1];
    var newFunc = Function.apply(
        Function,
        blacklist
    );
    blacklist.length = listlen;
    return newFunc.bind.apply(newFunc, blanklist);
}
var myfunc = sandboxed_function('return "window = " + window + "\ndocument = " + document + "\nBoolean = " + Boolean');
output.textContent = myfunc();
<pre id="output"></pre>

至于在这个严格的沙箱下编写代码运行ned,你可能会问,如果window 未定义,我如何测试方法的存在。有两种解决方案。 #1 只是像这样简单地使用 typeof。

output.textContent = 'typeof foobar = ' + typeof foobar;
<div id="output"></div>

正如您在上面的代码中看到的,使用 typeof 不会抛出错误,而只会 return undefined。检查全局的第二种主要方法是使用 try/catch 方法。

try {
    if (foobar)
        output.textContent = 'foobar.constructor = ' + foobar.constructor;
    else
        output.textContent = 'foobar.constructor = undefined';
} catch(e) {
    output.textContent = 'foobar = undefined';
}
<div id="output"></div>

因此,总而言之,我希望我的代码片段能让您深入了解 eval 的更好、更好、更简洁的替代方法。我希望我已经激励你实现一个更大的目标:冷落 eval。至于浏览器兼容性,虽然 sandboxed_function 在 IE9 中会 运行,但为了让它真正沙盒任何东西,IE10+ 是必需的。这是因为 "use-strict" 语句对于消除许多偷偷摸摸的沙盒破坏方式非常重要,如下所示。

var whitelist = ['Math', 'Number', 'Object', 'Boolean', 'Array'];
var blacklist = Object.getOwnPropertyNames(window).filter(function(x){
    return whitelist.indexOf(x) === -1 && !/^[^a-zA-Z]|\W/.test(x)
});
var listlen = blacklist.length;
var blanklist = (new Array(listlen+1)).fill(undefined);
function sandboxed_function(){
    blacklist.push.apply(blacklist, arguments);
    blacklist[blacklist.length-1] = 
        /*'"use-strict";' +*/ arguments[arguments.length-1];
    var newFunc = Function.apply(
        Function,
        blacklist
    );
    blacklist.length = listlen;
    return newFunc.bind.apply(newFunc, blanklist);
}
var myfunc = sandboxed_function(`return (function(){
    var snatched_window = this; // won't work in strict mode where the this
                                // variable doesn't need to be an object
    return snatched_window;
}).call(undefined)`);
output.textContent = "Successful broke out: " + (myfunc() === window);
<pre id="output"></pre>
最后的最后一条评论是,如果您要允许事件 API 进入您的沙盒环境,那么您必须小心:view 属性 可以是 window对象,制作它所以你也必须擦除它。还有其他一些东西,但我建议彻底研究并探索 Chrome 控制台中的对象。

您可以轻松地在 JS 中创建自己的 JS 解释器。我为 www.Photopea.com 制作了这样的东西(文件 - 脚本,我想让用户在 PSD 文档上执行脚本)。

Acorn 是一个高级的 JS 解析器,它接受一个字符串(JS 代码)和 returns 一个语法树。然后,从语法树的根开始,一条一条地执行命令。

"Jump"递归遍历树。使用环境的JS调用栈作为解释代码的调用栈。使用JS对象{var1: ..., var2: ...}在每次执行时存储变量的值space(全局的,函数中的局部...)。

您可以允许该代码通过某些接口从外部环境访问数据,或者使其完全沙盒化。我原以为制作我自己的口译员会花我一周的时间,但我只用了 6 个小时就完成了:)