半沙盒化的Javascript eval

8

背景:我正在为一个特定的网站与Greasemonkey / Userscripts协调使用的框架/库工作。这个框架/库将允许插件支持。其工作方式是插件通过列表在库中注册所需的页面、资源等,库将等待满足所有条件后调用插件的load()函数。

问题:在这个“必需内容”的列表中,我希望插件开发人员能够指定要评估为“必需资源”的javascript(作为字符串)。例如'document.getElementById("banana")'。我想做的是对“必需资源”的评估进行半沙盒化,以便评估可以访问window和DOM对象,但不能直接更改它们。我还希望使eval和evalJS无法从沙盒中访问。


示例

  • document.getElementById("banana")->有效
  • document.getElementById("apple).id = "orange"->无效
  • window.grape->有效
  • window.grape = 'potato'->无效
  • (someObj.applesCount > 0 ? 'some' : 'none')->有效


到目前为止我所做的

function safeEval(input) {

    // Remove eval and evalJS from the window:
    var e = [window.eval, window.evalJS], a;
    window.eval = function(){};
    window.evalJS = function(){};

    try {

        /* More sanition needed before being passed to eval */

        // Eval the input, stuffed into an annonomous function
        // so the code to be evalued can not access the stored
        // eval functions:
        a = (e[0])("(function(){return "+input+"}())");
    } catch(ex){}

    // Return eval and evalJS to the window:
    window.eval = e[0];
    window.evalJS = e[1];

    // Return the eval'd result
    return a;
}


注意事项:
这是一个Greasemonkey/userscript。我没有直接修改该网站或其javascript的访问权限。
safeEval()的输入可以是任何有效的javascript,可以是DOM查询或简单的计算,只要不更改窗口对象或DOM即可。


可能想要查看:如何在网页中安全地“eval”用户代码? - Brandon Boone
Caja对于从第三方获取代码的情况下,我的Web服务器非常好用。但是Greasemonkey/Userscripts是客户端的,而在已经充斥着许多库/小部件的网站上再要求使用另一个库/小部件并不是我愿意接受这个问题的解决方案。(如果我的话听起来有些生气,我向您道歉,这并不是我的本意) - SReject
看起来从Greasemonkey使用Caja会相当简单:一个@require指令和一些GM_xhr调用到他们的API。不确定Caja是否符合您的要求。 - Brock Adams
对于深度克隆 windowdocument,您有什么想法?这样,属性就可以被更改,但是您之后可以恢复原始的 DOM。不过可能会影响性能。 - Platinum Azure
与该话题密切相关:有可能在浏览器中沙盒化运行JavaScript吗? - Akseli Palén
显示剩余2条评论
4个回答

7

没有绝对的方法可以防止最终用户或插件开发人员在JavaScript中执行特定代码。这就是为什么在像JavaScript这样的开源语言中实施安全措施被认为是不可靠的(就像只有傻瓜才会有效一样)。

话虽如此,让我们构建一个沙盒安全层,以防止经验不足的开发人员损坏您的网站。个人而言,我更喜欢使用Function构造函数而不是eval来执行用户代码,原因如下:

  1. 代码被包装在一个匿名函数中。因此,它可以存储在变量中,并根据需要多次调用。
  2. 该函数始终存在于全局范围内。因此,它无法访问创建函数块中的局部变量。
  3. 该函数可以传递任意命名参数。因此,您可以利用此功能传递或导入用户代码所需的模块(例如jQuery)。
  4. 最重要的是,您可以设置自定义this指针并创建名为windowdocument的局部变量,以防止访问全局范围和DOM。这使您可以创建自己的DOM版本并将其传递给用户代码。

但请注意,即使这种模式也有缺点。最重要的是,它只能防止直接访问全局范围。用户代码仍然可以通过简单地声明变量而不带var来创建全局变量,恶意代码可能会使用诸如创建函数并使用其this指针访问全局范围(JavaScript的默认行为)等黑客手段。

因此,让我们看一些代码:http://jsfiddle.net/C3Kw7/


为了创建一个完整的沙盒,您需要创建一个扫描器,对用户代码中声明或定义的每个函数进行沙盒处理。一个简单的词法分析器就可以胜任这项工作。您还需要匹配平衡括号 - Aadit M Shah
我希望能够评估代码以访问当前窗口和DOM。但我不希望该代码能够更改窗口或DOM。 - SReject
没有办法在不创建自己的“window”和“document”对象的情况下完成这个任务。此外,您自定义的“document”对象上的自定义“getElementById”方法必须返回自定义的“HTMLElement”对象,以防止插件开发人员能够修改DOM。基本上,您需要创建自己的DOM,这是一个非常庞大的任务。最好的方法是允许开发人员制作插件,然后对它们进行验证。我相信你不会找到任何一个铁了心要制造混乱的人。坦白地说,我认为你在一个太小的问题上浪费了太多时间。 - Aadit M Shah
这个问题已经快两周了,我已经不再关注它了。只是想知道是否有一种相当简单的方法允许执行的代码访问getter,但不允许访问setter,而无需克隆DOM / Window对象。经过思考,我意识到这对于成本太高的收益太小了。然后昨天我决定跟进回复 :) - SReject

0

我知道这是一个旧帖子,但我想分享一个升级版的Aadit M Shah解决方案,它似乎真正地将沙盒隔离开来,没有任何访问窗口(或窗口子元素)的方法:http://jsfiddle.net/C3Kw7/20/

// create our own local versions of window and document with limited functionality

var locals = {
    window: {
    },
    document: {
    }
};

var that = Object.create(null); // create our own this object for the user code
var code = document.querySelector("textarea").value; // get the user code
var sandbox = createSandbox(code, that, locals); // create a sandbox

sandbox(); // call the user code in the sandbox

function createSandbox(code, that, locals) {
    code = '"use strict";' + code;
    var params = []; // the names of local variables
    var args = []; // the local variables

    var keys = Object.getOwnPropertyNames( window ),
    value;

    for( var i = 0; i < keys.length; ++i ) {
        //console.log(keys[i]);
        locals[keys[i]] = null;  
    }

    delete locals['eval'];
    delete locals['arguments'];

    locals['alert'] = window.alert; // enable alert to be used

    for (var param in locals) {
        if (locals.hasOwnProperty(param)) {
            args.push(locals[param]);
            params.push(param);
        }
    }

    var context = Array.prototype.concat.call(that, params, code); // create the parameter list for the sandbox
    //console.log(context);
    var sandbox = new (Function.prototype.bind.apply(Function, context)); // create the sandbox function
    context = Array.prototype.concat.call(that, args); // create the argument list for the sandbox

    return Function.prototype.bind.apply(sandbox, context); // bind the local variables to the sandbox
}

3
请注意,这不安全,并且可以很容易地通过执行以下操作进行规避:var global = Function("return this")(); global.alert(global.document); - Anvaka
1
@Anvaka 当我在这个沙盘中运行它时,我会得到“未捕获的类型错误: 函数不是一个函数”。 - Ben J
这个几乎完美地工作,但是如果我的页面有一个带有 id="test" 的元素,那么代码可以通过简单地 createSandbox('test', Object.create(null), {}) 访问该 dom 节点。 - Adam Keenan
这还能用吗?我在Chrome 111+上尝试时出现了错误。 - adelriosantiago

0
如果你只需要一个简单的getter,那就编写一个,而不是试图评估任何东西。
function get(input) {
    // if input is a string, it will be used as a DOM selector
    // if it is an array (of strings), they will access properties of the global object
    if (typeof input == "string")
        return document.querySelector(input);
    else if (Array.isArray(input)) {
        var res = window;
        for (var i=0; i<input.length && typeof input[i] == "string" && res; i++)
            res = res[input[i]];
        return res;
    }
    return null;
}

这不是一个简单的documentQuery。我希望任何JavaScript都可以被访问/执行,但我不希望该代码改变window对象。类似于safeEval('8*9')这样的内容是有效的。 - SReject
这是不可能的。您想允许 document.getElementById("x").getAttribute("href"),但防止 document.getElementById("x").removeAttribute("href") - Bergi

0

你可以像这样做:http://jsfiddle.net/g68NP/

问题在于,你将不得不添加大量的代码来保护每个属性、每个本地方法等。代码的核心实际上是使用__defineGetter__,但其支持有限。由于你可能不会在IE上运行此代码,所以应该没问题。

编辑:http://jsfiddle.net/g68NP/1/ 这段代码将使所有属性都变为只读。使用hasOwnProperty()可能是可取的,也可能不是。

如果JSFiddle崩溃了:

function safeEval(input) {
    // Remove eval and evalJS from the window:
    var e = [window.eval, window.evalJS, document.getElementById], a;
    window.eval = function(){};
    window.evalJS = function(){};
    document.getElementById = function (id) {
        var elem = (e[2]).call(document, id);
        for (var prop in elem) {
            if (elem.hasOwnProperty(prop)) {
                elem.__defineGetter__(prop, function () {
                    return (function (val) {
                        return val;
                    }(elem[prop]));
                });
            }                
        }
        return elem;
    };

    try {
        /* More sanition needed before being passed to eval */

        // Eval the input, stuffed into an annonomous function
        // so the code to be evalued can not access the stored
        // eval functions:
        a = (e[0])("(function(){return " + input + "}())");
    } catch(ex){}

    // Return eval and evalJS to the window:
    window.eval = e[0];
    window.evalJS = e[1];
    document.getElementById = e[2];

    // Return the eval'd result
    return a;
}

这不是一个简单的documentQuery。我希望任何JavaScript都可以被访问/执行,但我不希望该代码改变window对象。类似于safeEval('8*9')这样的内容是有效的。 - SReject
@SReject,为什么我的safeEval中的任何JS代码都是有效的?例如:safeEval('8*9')可以正常返回72。除了更改由document.getElementById检索到的元素的属性值之外,其他所有内容都可以正常工作。 - John Kurlak
@约翰,移除eval()有什么意义呢?这很容易再次实现。此外,如果这是FF,您可能希望隐藏Components,因为可以使用Components.lookupMethod(document,'getElementById')来获取原始实现。还值得在您的函数字符串插值中添加"use strict",并将运行函数的__proto__设置为类似(x = {},x.proto = null,return x)的东西。这在FF中对您没有帮助,但肯定可以防止一些淘气的Chrome问题。 - Joe V
@JoeV 移除 eval() 的目的是为了满足“我也想使 eval 和 evalJS 在沙盒中不可访问”的要求。尽管我同意你的所有观点。 - John Kurlak

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