保护JavaScript eval函数

14
我们希望为用户提供在我们的应用程序中执行自己创建的JavaScript代码的能力。为此,我们需要使用eval来评估代码。为了将所有安全问题降至最低(如果不是零),我们的想法是阻止代码中使用任何windowdocument函数。因此,不允许使用XMLHttpRequest或类似的内容。
这是代码:
function secure_eval(s) {
    var ret;

    (function(){
        var copyXMLHttpRequest = XMLHttpRequest; // save orginal function in copy

        XMLHttpRequest = undefined; // make orignal function unavailable

        (function() {
            var copyXMLHttpRequest; // prevent access to copy

            try {
                ret = eval(s)
            } catch(e) {
                console.log("syntax error or illegal function used");
            }

        }())
        XMLHttpRequest = copyXMLHttpRequest; // restore original function
    }())
    return ret;
}

它的运作方式如下:

secure_eval('new XMLHttpRequest()'); // ==> "illegal function used"

现在我有几个问题:

  1. 这种方式是保护eval的正确方法吗?
  2. windowdocument的哪些功能被认为是有害的?
  3. 为了避开问题2,我试图屏蔽window的所有本地函数,但我无法枚举它们:

这不会列出XMLHttpRequest,例如:

for( var x in window) {
    if( window[x] instanceof Function) {
        console.log(x);
    }
}
是否有一种方法可以获取所有windowdocument本地函数的列表?
EDIT:
我的一个想法是在Worker中执行eval并防止访问XMLHttpRequestdocument.createElement(见上面的解决方案)。这将导致以下后果:
- 无法访问原始document - 无法访问原始window - 没有与外部资源通信的机会(没有ajax,没有脚本)
您是否看到任何缺点或泄漏?
EDIT2:
同时,我已经找到了这个问题的答案,它解决了我的许多问题,还有一些我甚至没有考虑过的问题(即浏览器死锁"while(true){}")。

1
请查看ADsafe。但请注意,用户可以通过打开JavaScript控制台(Ctrl + Shift + I)来执行任何代码。 - Aadit M Shah
我无法阻止控制台选项是明确的。我只想保护使用eval执行的代码不来自用户,而是“来自任何其他地方...”。同时,我会尽力防止这种情况发生! - heinob
1
这个检查可能会很复杂。考虑以下内容:var u = window; u["al"+"ert"]("hello"); - heinob
之前有类似的问题并得到了很好的回答:https://dev59.com/NU3Sa4cB1Zd3GeqPt0q3 - Paul
5
你具体在做什么?如果每个用户都可以执行自己创建的代码,那么就不存在需要处理的安全问题。每个用户都有这个权利,并且无论如何你也无法阻止他这样做。 - Bergi
显示剩余4条评论
6个回答

20

你的代码实际上并没有防止使用 XMLHttpRequest我可以使用以下方法来实例化一个 XMLHttpRequest 对象:

secure_eval("secure_eval = eval"); // Yep, this completely overwrites secure_eval.
secure_eval("XMLHttpRequest()");
或:
secure_eval("new (window.open().XMLHttpRequest)()")

或者:

secure_eval("new (document.getElementById('frame').contentWindow.XMLHttpRequest)()")

这第三种方法依赖于页面HTML中存在一个iframe,某些人可以通过操作DOM在浏览器中添加它。我有时使用Greasemonkey进行这样的操作来消除烦恼或修复损坏的GUI。

我大约花了5分钟才弄清楚,而我绝不是安全专家。这些只是我能够快速发现的漏洞,可能还有其他我不知道的漏洞。这里的教训是:通过eval真的非常非常难以保护代码安全

使用Worker

好的,使用Worker运行代码将处理上述第二和第三种情况,因为Worker中没有可访问的窗口。还有...嗯...第一种情况可以通过在其作用域内隐藏secure_eval来处理。故事结束?如果那样就好了...

如果我将secure_eval放入Web Worker并运行下面的代码,我可以重新获取XMLHttpRequest

secure_eval("var old_log = console.log; console.log = function () { foo = XMLHttpRequest; old_log.apply(this, arguments); };");
console.log("blah");
console.log(secure_eval("foo"));

原则是覆盖一个函数,该函数在secure_eval之外被使用,通过将其分配给一个变量来捕获XMLHttpRequest,该变量将故意泄漏到worker的全局空间中,等待直到worker在secure_eval之外使用该函数,然后获取保存的值。上面的第一个console.log模拟了在secure_eval之外使用篡改后函数的情况,第二个console.log显示了已经捕获的值。我使用console.log是因为,没错吧?但是真的,任何全局空间中的函数都可以像这样修改。

实际上,为什么要等到worker可能使用我们篡改过的一些函数呢?下面是另一种更好、更快的访问 XMLHttpRequest 的方法:

secure_eval("setTimeout(function () { console.log(XMLHttpRequest);}, 0);");

即使在一个带有干净的 console.log 的 worker 中,这也会将 XMLHttpRequest 的实际值输出到控制台。我还要注意一下,在传递给 setTimeout 的函数内部,this 的值是全局作用域对象(即当不在 worker 中时为 window,在 worker 中为 self),不受任何变量遮蔽的影响。

那么本问题提到的其他问题呢?

那么这里的解决方案呢?好得多了,但是在 Chrome 38 中运行仍存在一些问题:

makeWorkerExecuteSomeCode('event.target.XMLHttpRequest', 
    function (answer) { console.log( answer ); });

这会显示:

function XMLHttpRequest() { [native code] }

再次声明,我不是安全专家或黑客,没有恶意行为。可能还有其他我没想到的方法。


关于Chrome 38中的漏洞,这是一个非常好的观点,但很容易解决:只需在onmessage中的try/catch周围放置一个闭包,并在闭包内重新定义var event;即可。 - heinob
是啊,但我还有什么没发现的?顺便说一下,我找到了一个恢复“console”的方法,但这并不能导致“XMLHttpRequest”,所以我没有提到它。 - Louis
makeWorkerExecuteSomeCode('var c = self.proto.proto.lookupGetter("console").call(self); c.log("FOO");', function (answer) { console.log(answer) }); 在这个代码片段中,原始的“console”对象被还原为“c”。适用于Firefox但不适用于Chrome。 - Louis
但这只有可能是因为 self 被列入白名单了,不是吗?如果我不需要 self,我可以将其从列表中删除,这样另一个漏洞就不存在了。 - heinob
据我进行的非常快速的测试,它似乎也可以与 thisglobal 一起使用。我刚刚测试了从白名单中删除 global,结果在 FF 和 Chrome 上都失败了。(所谓的“失败”是指该工作线程根本无法使用。) - Louis
“bombs”是什么意思?我认为你不能从白名单中删除global,因为遮蔽函数中需要它。更好的想法是在try/catch闭包中重新定义它(就像事件一样)。 - heinob

2

我将按照顺序尝试回答您的问题。

这种模式是保护 eval 的正确方式吗?

这部分有点主观。我没有看到任何主要的安全缺陷。我尝试了几种访问 XMLHttpRequest 的方法,但我没有成功:

secure_eval('XMLHttpRequest')
secure_eval('window.XMLHttpRequest')
secure_eval('eval("XMLHttpRequest")()')
secure_eval('window.__proto__.XMLHttpRequest') // nope, it's not inherited

然而,如果你想要将更多的东西列入黑名单,那会变得很麻烦。

windowdocument的哪些功能被认为是有害的?

这取决于你认为什么是“有害”的。DOM是否可以被访问都是不好的吗?或者像WebKit桌面通知或语音合成这样的功能呢?

你必须根据你的具体用例来决定。

为了规避第二个问题,我尝试遮蔽所有(本地)window函数,但我无法枚举它们:

这是因为大多数方法都是不可枚举的。要枚举,请使用Object.getOwnPropertyNames(window)

var globals = Object.getOwnPropertyNames(window);
for (var i = 0; i < globals.length; i++) {
    if( window[globals[i]] instanceof Function) {
        console.log(globals[i]);
    }
}

我的一个想法是在 Worker 中执行 eval 并防止访问 XMLHttpRequestdocument.createElement(请参见上面的解决方案)。
听起来像个好主意。

你的回答朝着正确的方向。与此同时,我已经改进了我的模式,遵循了这里接受的答案(https://dev59.com/a2gv5IYBdhLWcg3wXPou)。在我看来,这是对我的问题的正确回答。 - heinob

2
很久以前就有一个类似这样的问题。所以我拿出了一些旧代码并进行了修复。
它的实现基本上是利用了 with 关键字,并向其提供一个冻结的空对象。空对象的原型填充有 null 属性,其键与全局变量名(如 selfwindow 等)及其可枚举属性键相匹配;原型对象也被冻结了。然后在 with 语句中调用 eval(如果我理解正确,这几乎与脚本在隐式的 with(window){} 块中运行的方式相同)。当您尝试访问 window 或其属性时,通过 with 块会将您重定向到在空对象中找到的 null 版本(具有相同的键)的原型对象中:
function buildQuarantinedEval(){
    var empty=(function(){
        var exceptionKeys = [
                "eval", "Object", //need exceptions for these else error. (ie, 'Exception: redefining eval is deprecated')
                "Number", "String", "Boolean", "RegExp", "JSON", "Date", "Array", "Math",
                "this",
                "strEval"
        ];
        var forbiddenKeys=["window","self"];
        var forbidden=Object.create(null);
        [window,this,self].forEach(function(obj){
            Object.getOwnPropertyNames(obj).forEach(function(key){
                forbidden[key]=null;
            });
            //just making sure we get everything
            Object.keys(obj).forEach(function(key){
                forbidden[key]=null;                    
            });
            for(var key in obj){
                forbidden[key]=null;
            }
        });
        forbiddenKeys.forEach(function(key){
            forbidden[key]=null;
        });
        exceptionKeys.forEach(function(key){
            delete forbidden[key];
        });
        Object.freeze(forbidden);
        var empty=Object.create(forbidden);
        Object.freeze(empty);
        return empty;
    })();
    return function(strEval){
        return (function(empty,strEval){
            try{
                with(empty){
                    return eval(strEval);
                }               
            }
            catch(err){
                return err.message;
            }
        }).call(empty,empty,strEval);
    };
}

通过构建一个函数/闭包来设置,该函数/闭包评估一些表达式:
var qeval=buildQuarantinedEval();
qeval("'some expression'");     //evaluate

测试:
var testBattery=[
    "'abc'","8*8","console","window","location","XMLHttpRequest",
    "console","eval('1+1+1')","eval('7/9+1')","Date.now()","document",
    "/^http:/","JSON.stringify({a:0,b:1,c:2})","HTMLElement","typeof(window)",
    "Object.keys(window)","Object.getOwnPropertyNames(window)",
    "var result; try{result=window.location.href;}catch(err){result=err.message;}; result;",
    "parseInt('z')","Math.random()",
    "[1,2,3,4,8].reduce(function(p,c){return p+c;},0);"
];
var qeval=buildQuarantinedEval();
testBattery.map(function(code){
    const pad="                  ";
    var result= qeval(code);
    if(typeof(result)=="undefined")result= "undefined";
    if(result===null)result= "null";
    return (code+pad).slice(0,16)+": \t"+result;
}).join("\n");

结果:

/*
'abc'           :   abc
8*8             :   64
console         :   null
window          :   null
location        :   null
XMLHttpRequest  :   null
console         :   null
eval('1+1+1')   :   3
eval('7/9+1')   :   1.7777777777777777
Date.now()      :   1415335338588
document        :   null
/^http:/        :   /^http:/
JSON.stringify({:   {"a":0,"b":1,"c":2}
HTMLElement     :   null
typeof(window)  :   object
Object.keys(wind:   window is not an object
Object.getOwnPro:   can't convert null to object
var result; try{:   window is null
parseInt('z')   :   parseInt is not a function
Math.random()   :   0.8405481658901747
[1,2,3,4,8].redu:   18
*/

注解:当窗口的某些属性在初始化/创建我们隔离的求值函数之后定义时,该技术可能会失败。过去,我注意到有些属性键直到访问该属性后才进行枚举,此后 Object.keys 或 Object.getOwnPropertyNames 最终能够抓取它们的键。另一方面,该技术也可以非常积极地阻止您不想被阻止的对象/函数(例如像 parseInt 这样的示例); 在这些情况下,您需要手动将您需要的全局对象/函数添加到 exceptionKeys 数组中。
附加注意事项:所有操作的效果取决于掩码与窗口对象的属性键匹配的程度。每当您向文档添加一个元素并为其分配新的ID时,就会向全局窗口对象中插入一个新属性,这可能允许我们的“攻击者”抓取它并打破我们设置的隔离/防火墙(即从那里访问element.querySelector,然后是window obj)。因此,掩码(即变量forbidden)需要不断更新,可以使用watch方法或每次重建;前者与掩码必须具有冻结接口的必要性相冲突,而后者则有点昂贵,需要枚举每个评估的窗口键。
就像我之前说的那样,这主要是我曾经在工作中放弃的旧代码,在短时间内快速修复。因此,它绝不是经过彻底测试的。我将留给您去测试。

还有一个jsfiddle链接



你的代码被这个 qeval("setTimeout(function () {this.console.log(this.XMLHttpRequest);}, 0);"); 打败了。 - Louis
@Louis 嗯,这很有趣。我在这里得到了 *"(error) setTimeout is not a function"*。(Firefox w/ Developer Scratchpad)我将再次使用 userscript 进行检查。不过,这取决于您是否能够枚举窗口的所有键,这可能是不可靠的(当我以前检查时,由于某些原因,某些属性键没有被枚举)。 - jongo45
@Louis 好的,现在我也得到了与你相同的结果(通过用户脚本),某些属性键在一个环境中通过 Object.keys/Object.getOwnPropertyNames 被一致地获得,但在另一个环境中则不是如此。由于代码执行的位置不同,结果会非常微妙。 我想,如果我们能够可靠地获取所有的键... - jongo45
@Louis,我调整了代码,更详细地获取窗口属性名称(通过Object.getOwnPropertyNamesObject.keysfor...in),看起来已经在用户脚本和开发者 Scratchpad 中修复了这个问题。此时,它几乎感觉像是玩打地鼠的游戏。而且我相当确定你仍然可以访问窗口属性/方法或其他一些敏感对象(也许是通过污染全局变量空间、查找 ID 元素或篡改仍然暴露的各种原型)。有趣。 - jongo45

2

虽然我已经在问题中提到了,但为了更加清晰,我也会将其作为答案发布:

我认为这个问题上被接受的答案是完全隔离和限制eval()的正确且唯一的方法。

它也可以防止这些黑客攻击:

(new ('hello'.constructor.constructor)('alert("hello from global");'))()

(function(){return this;})().alert("hello again from global!");

while(true){} // if no worker --> R.I.P. browser tab

Array(5000000000).join("adasdadadasd") // memory --> boom!

我之前对这个答案的评论是不正确的。但是,我找到了其他东西 - Louis

2
我偶然发现了一篇非常好的关于臭名昭著的Eval在这里的博客文章。该文章详细讨论了此事。虽然您无法消除所有安全问题,但可以通过为输入构建令牌来防止跨站点脚本攻击。理论上,这将防止引入有害的恶意代码。
您唯一需要克服的障碍是中间人攻击。我不确定是否可能发生,因为您无法信任输入和输出。 Mozilla开发者网络明确指出:
eval()是一个危险的函数,它执行传递给它的代码,并使用调用者的权限。如果您使用可能受到恶意方影响的字符串运行eval(),则可能会以您网页/扩展程序的权限在用户的计算机上运行恶意代码。更重要的是,第三方代码可以看到调用eval()的作用域,这可能导致可能攻击Function不易受到的方式。
eval()通常比其他替代方法慢,因为它必须调用JS解释器,而许多其他构造由现代JS引擎进行了优化。
对于常见用例,有更安全(且更快!)的eval()替代方法。
我稍微反对Eval,并真正尝试在必要时使用它。

0

我对于安全评估有一些小想法,如果你很清楚eval的使用目的,可以创建白名单和黑名单,仅执行具有有效字符串的内容,这对于小型应用程序非常有好处,例如计算器只有几个选项(x,y)和(+,*,-,/),如果我将这些字符添加到白名单中,并添加脚本长度检查以及研究脚本运行的预期长度,那么它就是安全的,没有人可以通过它。

const x = 5;
const y = 10;

function secureEval(hack_string){
  // 0 risk eval calculator
  const whiteList = ['',' ', 'x', 'y','+','*','/','-'];
  for (let i=0; i<hack_string.length; i++){
    if (!whiteList.includes(hack_string[i])){
      return 'Sorry u can not hack my systems';
    }
  }
  return 'good code system identify result is : ' + eval(hack_string);
}
// bad code
document.getElementById("secure_demo").innerHTML = secureEval('x * y; alert("hacked")');

document.getElementById("demo").innerHTML = secureEval('x * y');
<!DOCTYPE html>
<html>
<body>

<h1>Secure Eval</h1>

<p id="secure_demo"></p>

<p id="demo"></p>
</body>
</html>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接