限制 eval() 的范围

38

我有一个JavaScript文件,它会读取另一个文件,该文件可能包含需要执行eval()的JavaScript片段。这些脚本片段应符合一组限制其操作和更改变量的JavaScript严格子集标准,但我想知道是否有一种方法可以通过防止eval看到全局范围内的变量来强制执行这些限制。类似以下方式:

function safeEval( fragment )
{
    var localVariable = g_Variable;

    {
        // do magic scoping here so that the eval fragment can see localVariable
        // but not g_Variable or anything else outside function scope

        eval( fragment );
    }
}

实际的代码不必像这样--我对所有关于闭包等奇怪技巧都持开放态度。但是我想知道这是否 可能


我不知道它是否适用于 eval,但你可以尝试切换其执行上下文(谷歌激活对象)。 - jfs
请查看 dojox.secure.sandbox 的实现。 - jfs
现在,你可能想要的是一个Web Worker。 - bjb568
10个回答

58

简短回答:不行。如果变量在全局作用域中,那么可以被任何地方访问。

详细回答:如果你使用 eval() 来运行不受信任的代码,并且这段代码想要读取或更改你的执行环境,那么你就会遇到麻烦。但是,如果你拥有并信任所有被执行的代码,包括那些被 eval() 运行的代码,你可以通过覆盖执行上下文来模拟它。

function maskedEval(scr)
{
    // set up an object to serve as the context for the code
    // being evaluated. 
    var mask = {};
    // mask global properties 
    for (p in this)
        mask[p] = undefined;

    // execute script in private context
    (new Function( "with(this) { " + scr + "}")).call(mask);
}

我再强调一遍:

这只会保护可信赖的代码免受执行环境的影响。如果你不信任代码,请勿使用 eval()(或将其传递给新的Function(),或以任何其他类似于eval()的方式使用它)。


2
使用 with 方法时有一个小问题,曾经让我吃过亏。在 with 块内声明的函数无法访问 with 语句的目标。 - Dale Anderson
3
请在末尾阅读警告,@meph - 不要输入任何你没有编写/控制的内容。 - Shog9
3
你能否举例说明这可能会有什么不良后果?我查了一些资料,但没有找到任何例子...我将禁止变量定义,例如var test = 1。 - Mephiztopheles
1
我会检查表达式是否包含 =,如果下一个字符不是 =,则会阻止它 :D 我必须检查更多的表达式,但我不知道还有哪些需要尝试... 我想要一些坏表达式的例子。 - Mephiztopheles
5
我能想到的最简单的不良表达式是: (function() { this.document.anythingYouWant... }).call(null);当没有使用“this”参数调用时,“this”将变为全局上下文。 - Delus
显示剩余3条评论

7
Shog9♦的回答很好。但是如果你的代码只是一个表达式,则代码将被执行并返回空值。对于表达式,请使用。
function evalInContext(context, js) {

  return eval('with(context) { ' + js + ' }');

}

以下是如何使用它的步骤:
var obj = {key: true};

evalInContext(obj, 'key ? "YES" : "NO"');

它将返回"YES"

如果您不确定要执行的代码是表达式还是语句,可以将它们组合使用:

function evalInContext(context, js) {

  var value;

  try {
    // for expressions
    value = eval('with(context) { ' + js + ' }');
  } catch (e) {
    if (e instanceof SyntaxError) {
      try {
        // for statements
        value = (new Function('with(this) { ' + js + ' }')).call(context);
      } catch (e) {}
    }
  }

  return value;
}

哇,这相当令人印象深刻。 - Daniel Node.js
在严格模式下不允许使用 with() - Damian Drygiel

6
与上文中用with块方法进行的动态函数封装脚本类似,这种方法允许您向要执行的代码添加伪全局变量。您可以通过将特定内容添加到上下文中来“隐藏”它们。
function evalInContext(source, context) {
    source = '(function(' + Object.keys(context).join(', ') + ') {' + source + '})';

    var compiled = eval(source);

    return compiled.apply(context, values());

    // you likely don't need this - use underscore, jQuery, etc
    function values() {
        var result = [];
        for (var property in context)
            if (context.hasOwnProperty(property))
                result.push(context[property]);
        return result;
    }
}

请参考http://jsfiddle.net/PRh8t/来查看一个示例。需要注意的是,Object.keys不受所有浏览器支持。


5

我偶然发现可以使用代理来限制作用域对象,这似乎更容易地屏蔽变量不在作用域内。我不确定这种方法是否有缺点,但迄今为止它对我很有效。

function maskedEval(src, ctx = {})
{
    ctx = new Proxy(ctx, {
        has: () => true
    })
    // execute script in private context
    let func = (new Function("with(this) { " + src + "}"));
    func.call(ctx);
}

a = 1;
maskedEval("console.log(a)", { console });
maskedEval("console.log(a)", { console, a: 22});

maskedEval("a = 1", { a: 22 })
console.log(a)

这是一个非常聪明的方法。 - Waruyama

3

不要执行你不信任的代码。全局变量始终可以访问。 如果你信任这段代码,你可以按照以下方式在它的范围内执行它:

(new Function("a", "b", "alert(a + b);"))(1, 2);

这相当于:

(function (a, b) {
    alert(a + b);
})(1, 2);

另一个例子:(new Function("a", "b", "return a + b"))(1, 2) - human

3

3

你不能限制eval的范围。

顺便提一下,参见这篇文章

在整个计划中,可能有其他方法来完成您想要实现的目标,但是您无法以任何方式限制eval的范围。您可以将某些变量隐藏为JavaScript中的伪私有变量,但我认为这不是您想要的。


3

这里有个想法。如果你使用静态分析器(例如,你可以使用esprima构建一个),以确定eval的代码使用哪些外部变量,并为它们取一个别名。我所说的“外部代码”是指eval的代码使用但没有声明的变量。以下是一个示例:

eval(safeEval(
     "var x = window.theX;"
    +"y = Math.random();"
    +"eval('window.z = 500;');"))

safeEval函数返回一个修改了上下文环境,阻止访问外部变量的JavaScript字符串:

";(function(y, Math, window) {"
  +"var x = window.theX;"
  +"y = Math.random();"
  +"eval(safeEval('window.z = 500;');"
"})();"

现在有几件事情可以做:

  • 您可以通过将undefined作为函数参数传递或不传递参数来确保 eval 代码无法读取外部变量的值,也无法写入它们。或者在访问变量不安全的情况下,可以简单地抛出异常。
  • 还可以确保由 eval 创建的变量不会影响周围的作用域。
  • 您可以允许 eval 在周围的作用域中创建变量,方法是将这些变量声明到闭包之外而不是作为函数参数。
  • 您可以通过复制外部变量的值并将其用作函数参数来允许只读访问。
  • 您可以通过告诉 safeEval 不使用别名来允许特定变量的读写访问。
  • 您可以检测 eval 修改特定变量的情况,并自动将其排除在别名之外(例如,在此示例中,Math 没有被修改)。
  • 您可以通过传递与周围上下文不同的参数值来为 eval 提供运行环境。
  • 您还可以通过从函数返回函数参数来捕获上下文更改,以便在 eval 外部检查它们。

请注意,使用eval是一种特殊情况,因为由于其本质,它实际上无法包装在另一个函数中(这就是为什么我们必须执行eval(safeEval(...))的原因)。

当然,做所有这些工作可能会减慢代码的速度,但肯定有一些不会受到影响的地方。希望这对某些人有所帮助。如果有人创建了一个概念验证,我很乐意在此处看到它的链接 ;)。


2
不要使用eval。有一个替代方法,即js.js用JS编写的JS解释器,因此您可以在任何您设置的环境中运行JS程序。以下是项目页面上其API的示例:
var jsObjs = JSJS.Init();
var rval = JSJS.EvaluateScript(jsObjs.cx, jsObjs.glob, "1 + 1");
var d = JSJS.ValueToNumber(jsObjs.cx, rval);
window.alert(d); // 2
JSJS.End(jsObjs);

没有什么可怕的,如你所见。


3MB,经过gzip压缩后仅为594KB。 - prototype
@user645715 1) 尝试使用Closure Compiler来进一步压缩它。2) 这会导致200倍的减速,比3MB的源代码更糟糕。3) 目前还没有其他解决方案。 - polkovnikov.ph

0

一个作用域限定的 eval 示例,用于 JavaScript 模块并返回一个已评估的表达式:

function scopedExpr(src, ctx) {
    const scope = Object.assign(Object.keys(globalThis).reduce((acc,k) => { 
        acc[k] = undefined; return acc 
        }, {})
        ,ctx)
    return (new Function("with(this) { return (" + src + ") }")).call(scope)
}

使用方法

const result = scopedExpr('{ a: b + c }', { b: 1, c: 2 }) //= { a: 3 }

TypeScript 版本

function scopedExpr(src:string, ctx:Record<string,any>) {
    const scope = Object.assign(Object.keys(globalThis).reduce((acc,k) => { 
        acc[k] = undefined; return acc 
        }, {} as Record<string,any>)
        ,ctx)
    return (new Function("with(this) { return (" + src + ") }")).call(scope)
}

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