保护JavaScript eval功能

时间:2014-10-21 08:35:32

标签: javascript eval

我们希望让用户能够在我们的应用程序中执行自己创建的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的所有(本机)函数但我无法枚举它们:
  4. 例如,这不列出XMLHttpRequest

    for( var x in window) {
        if( window[x] instanceof Function) {
            console.log(x);
        }
    }
    

    有没有办法获取windowdocument的所有原生函数的列表?

    修改

    我的一个想法是在eval内执行Worker并阻止访问XMLHttpRequestdocument.createElement(请参阅上面的解决方案)。这会(在我看来)产生以下后果:

    • 无法访问原始document
    • 无法访问原始window
    • 没有机会与外部资源(没有ajax,没有脚本)进行通信

    你认为这里有任何缺点或泄漏吗?

    EDIT2:

    与此同时,我发现this question答案解决了我的许多问题以及我甚至没想过的一些事情(即浏览器死锁"while(true){}"

5 个答案:

答案 0 :(得分:16)

您的代码实际上并未阻止使用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)()")

第3种方法依赖于页面HTML中存在iframe,有人可以通过在浏览器中操作DOM来添加。我不时地用Greasemonkey做这样的操作来消除烦恼或修复破坏的GUI。

我花了大约5分钟才弄明白,而且我绝不是一个安全大师。这些只是我能够迅速找到的漏洞,可能还有其他漏洞,我不知道。这里的教训是通过eval 确保代码真的非常难。

使用工人

好的,所以使用Worker来运行代码将会处理上面的第2和第3个案例,因为Worker中没有可访问的窗口。并且......嗯..第一种情况可以通过在其范围内隐藏secure_eval来处理。故事结局?如果只是......

如果我将secure_eval放入网络工作者并运行以下代码,我可以重新获取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,等到secure_eval之外的工作程序使用该函数,然后获取保存的值。上面的第一个console.log模拟了secure_eval之外的篡改功能的使用,第二个console.log显示了捕获的值。我使用过console.log,为什么不呢?但是真的全局空间中的任何函数都可以这样修改。

实际上,为什么要等到工人可以使用我们篡改的某些功能?这是另一种更好,更快捷的访问方式XMLHttpRequest

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

即使在工作人员(具有原始console.log)中,这也会将XMLHttpRequest的实际值输出到控制台。我还要注意,传递给this的函数内setTimeout的值是全局范围对象(即不在工作者中的window,或self在工人中),不受任何变量阴影的影响。

本期提到的其他问题怎么办?

解决方案here怎么样?好多了,但在Chrome 38中运行时还有一个漏洞:

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

这将显示:

function XMLHttpRequest() { [native code] }

同样,我没有任何安全大师或破解者一心想要制造麻烦。可能还有更多我没想过的方法。

答案 1 :(得分: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桌面通知或语音合成呢?

您必须根据具体用例做出决定。

  

发送问题2.我试图掩盖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]);
    }
}
  

我的一个想法是在eval内执行Worker并阻止访问XMLHttpRequestdocument.createElement(请参阅上面的解决方案)。

这听起来不错。

答案 2 :(得分:2)

我偶然发现了一篇关于臭名昭着的Eval here的非常好的博客文章。文章确实详细讨论过。您无法减轻所有安全问题,但您可以通过为输入构建令牌来阻止跨脚本攻击。从理论上讲,这可以防止可能有害的恶意代码被引入。

你唯一的另一个障碍就是中间人攻击。我不确定这是否可能,因为你不能信任输入和输出。

Mozilla Developer Network明确说明:

  

eval()是一个危险的函数,它执行它传递的代码   具有来电者的特权。如果使用字符串运行eval()   可能会受到恶意方的影响,您最终可能会运行   具有您的权限的用户计算机上的恶意代码   网页/扩展程序。更重要的是,第三方代码可以看到   调用eval()的范围,可能导致可能的攻击   以类似函数不易受影响的方式。

     

eval()通常也比替代品慢,因为它必须   调用JS解释器,同时优化许多其他构造   通过现代JS引擎。

     

对于常见的eval()有更安全(和更快!)的替代方案   用例。

我稍微反对Eval,并在保证时真正尝试使用它。

答案 3 :(得分:2)

很久以前有一个问题很像这样。所以我把一些旧代码掸掉并修好了。

它本质上是通过利用with关键字并为其提供冻结的空对象来实现的。空对象的原型填充了null属性,其键与名称全局变量匹配,如selfwindow及其可枚举属性键;原型对象也被冻结。然后在eval语句中调用with(如果我理解正确的话,几乎与脚本使用隐式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
*/

注意:当窗口的某些属性定义较晚时(在初始化/创建隔离的eval函数之后),此技术可能会失败。在过去,我注意到在您访问该媒体资源之前不会枚举某些属性键,之后Object.keysObject.getOwnPropertyNames最终会获取其密钥。另一方面,这种技术在阻止你不想阻止的对象/函数方面也非常积极(例如parseInt);在这些情况下,您需要手动将您想要的全局对象/函数添加到 exceptionKeys 数组中。

* edit * 其他注意事项:这一切的执行情况完全取决于蒙版与窗口对象的属性键的匹配程度。每当您向文档添加元素并为其提供新ID时,您只需将新属性插入到全局窗口对象中,这可能会让我们的攻击者&#39;抓住它并打破我们设置的隔离/防火墙(即访问element.querySelector然后最终从那里窗口obj)。所以掩码(即变量禁止)要么需要不断更新,也不需要每次都重建;前者与掩码必须具有冻结界面冲突,后者有点昂贵,必须枚举每个评估窗口的所有键。

就像我之前说过的那样,这主要是我正在处理的旧代码,然后被放弃了,很快就修好了。所以它绝不是经过彻底测试的。我会把它留给你。


jsfiddle


答案 4 :(得分:2)

我已在我的问题中说明了这一点,但为了更清楚,我会将其作为答案发布:

我认为this question上接受的答案是完全隔离和约束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!
相关问题