编码风格 - JavaScript 的 eval() 什么时候不是邪恶的

coding style - When is JavaScript's eval() not evil?

本文关键字:什么时候 邪恶 eval 风格 JavaScript 编码      更新时间:2023-09-26

我正在编写一些JavaScript代码来解析用户输入的函数(用于类似电子表格的功能(。解析公式后,我可以将其转换为 JavaScript 并对其运行eval()以产生结果。

但是,我一直回避使用eval(),如果我能避免它,因为它是邪恶的(而且,无论对错,我一直认为它在JavaScript中更加邪恶,因为要评估的代码可能会被用户更改(。

那么,什么时候可以使用它呢?

我想花点时间解决你问题的前提 - eval(( 是">邪恶的"。编程语言人员使用的">邪恶"一词通常意味着"危险",或者更准确地说,"能够通过一个简单的命令造成很多伤害"。那么,什么时候可以使用危险的东西呢?当您知道危险是什么,以及何时采取适当的预防措施时。

首先,让我们看看使用 eval(( 的危险。可能有很多小的隐患,就像其他一切一样,但两个大风险 - eval((被认为是邪恶的原因 - 是性能和代码注入。

  • 性能 - eval(( 运行解释器/编译器。如果你的代码被编译了,那么这是一个很大的打击,因为你需要在运行时中间调用一个可能很重的编译器。然而,JavaScript 仍然主要是一种解释型语言,这意味着在一般情况下调用 eval(( 对性能的影响并不大(但请参阅下面的具体评论(。
  • 代码注入 - eval(( 可能会在提升的权限下运行一串代码。例如,以管理员/root 身份运行的程序永远不会想要 eval(( 用户输入,因为该输入可能是"rm -rf/etc/important-file"或更糟。同样,浏览器中的JavaScript没有这个问题,因为该程序无论如何都在用户自己的帐户中运行。服务器端JavaScript可能会有这个问题。

继续您的具体情况。据我了解,您自己生成字符串,因此假设您小心翼翼地不允许生成像"rm -rf something-important"这样的字符串,则没有代码注入风险(但请记住,在一般情况下很难确保这一点(。此外,如果您在浏览器中运行,那么我相信代码注入是一个很小的风险。

至于性能,您必须权衡编码的难易程度。我认为,如果您正在解析公式,您不妨在解析过程中计算结果,而不是运行另一个解析器(eval(( 中的解析器(。但是使用 eval(( 编写代码可能更容易,并且性能影响可能不会引起注意。在这种情况下,看起来 eval(( 并不比任何其他可能为您节省时间的函数更邪恶。

eval()并不邪恶。或者,如果是,它是邪恶的,就像反射、文件/网络 I/O、线程和 IPC 在其他语言中是"邪恶的"一样。

如果,就您的目的而言eval()比手动解释更快,或者使您的代码更简单或更清晰......那么你应该使用它。如果两者都不是,那么你不应该。就这么简单。

当您信任源时。

在 JSON 的情况下,或多或少很难篡改源代码,因为它来自您控制的 Web 服务器。只要 JSON 本身不包含用户上传的数据,使用 eval 就没有主要缺点。

在所有其他情况下,我会竭尽全力确保用户提供的数据符合我的规则,然后再将其提供给 eval((。

让我们得到真正的人:

  1. 现在,每个主要的浏览器都有一个内置的控制台,您的潜在黑客可以大量使用它来调用具有任何值的任何函数 - 为什么他们会费心使用评估语句 - 即使他们可以?

  2. 如果编译 2000 行 JavaScript 需要 0.2 秒,那么如果我评估四行 JSON,性能会下降什么?

甚至克罗克福德对"eval is eval"的解释也很薄弱。

eval 是 Evil,eval 函数是最容易被滥用的功能 JavaScript。避免它

正如克罗克福德自己所说:"这种说法往往会产生非理性的神经症。不要买它。

了解 eval 并知道它何时可能有用更为重要。例如,eval 是评估软件生成的服务器响应的明智工具。

顺便说一句:原型.js直接调用 eval 五次(包括在 evalJSON(( 和 evalResponse((中(。 jQuery在parseJSON(通过函数构造函数(中使用它。

我倾向于遵循克罗克福德的建议eval(),并完全避免它。即使是似乎需要它的方式也没有。例如,setTimeout()允许您传递函数而不是 eval。

setTimeout(function() {
  alert('hi');
}, 1000);

即使它是一个受信任的来源,我也不会使用它,因为 JSON 返回的代码可能会乱码,这充其量只能做一些不稳定的事情,最坏的情况是暴露一些不好的东西。

Eval 是对用于模板化代码的编译的补充。通过模板化,我的意思是你编写了一个简化的模板生成器,它生成有用的模板代码,从而提高开发速度。

我编写了一个框架,开发人员不使用 EVAL,但他们使用我们的框架,反过来该框架必须使用 EVAL 来生成模板。

可以使用

以下方法提高 EVAL 的性能;必须返回一个函数,而不是执行脚本。

var a = eval("3 + 5");

它应该组织为

var f = eval("(function(a,b) { return a + b; })");
var a = f(3,5);

缓存 f 肯定会提高速度。

此外,Chrome还允许非常轻松地调试此类功能。

关于安全性,无论是否使用eval几乎不会有任何区别,

  1. 首先,浏览器在沙盒中调用整个脚本。
  2. 任何在EVAL中是邪恶的代码,在浏览器本身中都是邪恶的。攻击者或任何人都可以轻松地在 DOM 中注入脚本节点,如果他/她可以评估任何东西,就可以做任何事情。不使用EVAL不会有任何区别。
  3. 主要是
  4. 糟糕的服务器端安全性是有害的。服务器上糟糕的 cookie 验证或糟糕的 ACL 实现会导致大多数攻击。
  5. 最近的Java漏洞等存在于Java的本机代码中。JavaScript过去和现在都被设计为在沙箱中运行,而小程序被设计为在沙箱之外运行,这些证书等会导致漏洞和许多其他事情。
  6. 编写用于模仿浏览器的代码并不困难。您所要做的就是使用您喜欢的用户代理字符串向服务器发出HTTP请求。无论如何,所有测试工具都会模拟浏览器;如果攻击者想要伤害您,EVAL 是他们的最后手段。他们有许多其他方法来处理您的服务器端安全性。
  7. 浏览器 DOM 无权访问文件,也无权访问用户名。事实上,计算机上没有任何 eval 可以访问的内容。

如果您的服务器端安全性足够可靠,任何人都可以从任何地方进行攻击,则不必担心EVAL。正如我所提到的,如果 EVAL 不存在,攻击者有许多工具可以入侵您的服务器,而不管浏览器的 EVAL 功能如何。

Eval 仅适用于生成一些模板,以基于事先未使用的内容进行复杂的字符串处理。例如,我会更喜欢

"FirstName + ' ' + LastName"

而不是

"LastName + ' ' + FirstName"

作为我的显示名称,它可以来自数据库并且不是硬编码的。

底线

如果你创建或清理了你eval的代码,它永远不会是邪恶的。

稍微详细一点

如果使用不是由开发人员创建或未经开发人员清理的客户端提交的输入在服务器上运行,则eval邪恶的。

如果在客户端上运行,即使使用客户端精心制作的未经净化的输入,eval也不是邪恶的。

显然,您应该始终清理输入,以便对代码消耗的内容进行一些控制。

推理

客户端可以运行他们想要的任何任意代码,即使开发人员没有编码它;这不仅适用于被回避的东西,也适用于eval本身的呼唤

在 Chrome (v28.0.1500.72( 中调试时,我发现如果变量未在生成闭包的嵌套函数中使用,则变量不会绑定到闭包。我想,这是对JavaScript引擎的优化。

BUT:eval()在导致闭包的函数中使用时,外部函数的所有变量都绑定到闭包,即使它们根本没有使用。如果有人有时间测试是否可以由此产生内存泄漏,请在下面给我留言。

这是我的测试代码:

(function () {
    var eval = function (arg) {
    };
    function evalTest() {
        var used = "used";
        var unused = "not used";
        (function () {
            used.toString();   // Variable "unused" is visible in debugger
            eval("1");
        })();
    }
    evalTest();
})();
(function () {
    var eval = function (arg) {
    };
    function evalTest() {
        var used = "used";
        var unused = "not used";
        (function () {
            used.toString();   // Variable "unused" is NOT visible in debugger
            var noval = eval;
            noval("1");
        })();
    }
    evalTest();
})();
(function () {
    var noval = function (arg) {
    };
    function evalTest() {
        var used = "used";
        var unused = "not used";
        (function () {
            used.toString();    // Variable "unused" is NOT visible in debugger
            noval("1");
        })();
    }
    evalTest();
})();

我想在这里指出的是,eval(( 不一定引用本机eval()函数。这完全取决于函数的名称。因此,当使用别名(例如var noval = eval;然后在内部函数noval(expression);中调用本机eval()时,当expression引用应该成为闭包一部分的变量时,的评估可能会失败,但实际上不是。

我看到人们主张不要使用eval,因为是邪恶的,但我看到同样的人动态使用函数和setTimeout,所以他们在引擎盖下使用eval:D

顺便说一句,如果您的沙盒不够确定(例如,如果您正在处理允许代码注入的站点(,eval 是您最后一个问题。安全的基本规则是所有输入都是邪恶的,但在 JavaScript 的情况下,甚至 JavaScript 本身也可能是邪恶的,因为在 JavaScript 中你可以覆盖任何函数,而你只是不能确定你使用的是真正的函数,所以,如果恶意代码在你之前启动,你不能相信任何 JavaScript 内置函数:D

现在这篇文章的尾声是:

如果你真的需要它(80% 的时间不需要 eval 并且你确定你在做什么,只需使用 eval(或更好的函数;)(,闭包和 OOP 涵盖了 80/90% 的情况,其中 eval 可以使用另一种逻辑替换,其余的是动态生成的代码(例如,如果您正在编写解释器(,并且正如您已经说过的评估 JSON(在这里您可以使用 Crockford 安全评估;)

你应该使用 eval(( 的唯一实例是当你需要动态运行动态 JS 时。我说的是你从服务器异步下载的JS...

。10 次中的 9 次,您可以通过重构轻松避免这样做。

在服务器端,eval 在处理外部脚本(如 sql 或 influxdb 或 mongo(时很有用。可以在运行时进行自定义验证,而无需重新部署服务。

例如,具有以下元数据的成就服务

{
  "568ff113-abcd-f123-84c5-871fe2007cf0": {
    "msg_enum": "quest/registration",
    "timely": "all_times",
    "scope": [
      "quest/daily-active"
    ],
    "query": "`SELECT COUNT(point) AS valid from '"${userId}/dump/quest/daily-active'" LIMIT 1`",
    "validator": "valid > 0",
    "reward_external": "ewallet",
    "reward_external_payload": "`{'"token'": '"${token}'", '"userId'": '"${userId}'", '"amountIn'": 1, '"conversionType'": '"quest/registration:silver'", '"exchangeProvider'":'"provider/achievement'",'"exchangeType'":'"payment/quest/registration'"}`"
  },
  "efdfb506-1234-abcd-9d4a-7d624c564332": {
    "msg_enum": "quest/daily-active",
    "timely": "daily",
    "scope": [
      "quest/daily-active"
    ],
    "query": "`SELECT COUNT(point) AS valid from '"${userId}/dump/quest/daily-active'" WHERE time >= '${today}' ${ENV.DAILY_OFFSET} LIMIT 1`",
    "validator": "valid > 0",
    "reward_external": "ewallet",
    "reward_external_payload": "`{'"token'": '"${token}'", '"userId'": '"${userId}'", '"amountIn'": 1, '"conversionType'": '"quest/daily-active:silver'", '"exchangeProvider'":'"provider/achievement'",'"exchangeType'":'"payment/quest/daily-active'"}`"
  }
}

然后允许,

  • 通过 json 中的文字字符串直接注入对象/值,可用于模板化文本

  • 可以用作比较器,假设我们制定规则如何在CMS中验证任务或事件

这个缺点:

  • 如果未经全面测试,可能是代码中的错误,并分解服务中的内容。

  • 如果黑客可以在您的系统上编写脚本,那么您几乎就完蛋了。

  • 验证脚本的一种方法是将脚本的哈希保存在安全的地方,以便您可以在运行前检查它们。

Eval 不是邪恶的,只是被滥用了。

如果您创建了进入其中的代码或可以信任它,那没关系。人们一直在谈论用户输入与 eval 无关紧要。嗯有点~

如果有用户输入转到服务器,则返回到客户端,并且该代码正在 eval 中使用,而无需经过清理。恭喜,您已经打开了潘多拉的盒子,可以将用户数据发送给任何人。

根据评估的位置,许多网站都使用 SPA,而 eval 可以使用户更容易访问应用程序内部,否则这并不容易。现在,他们可以制作一个虚假的浏览器扩展程序,该扩展程序可以粘贴到该评估程序并再次窃取数据。

只是必须弄清楚您使用eval的意义何在。生成代码并不是很理想,因为你可以简单地制作方法来做那种事情,使用对象,或类似的东西。

现在是使用 eval 的一个很好的例子。您的服务器正在读取您创建的 swagger 文件。许多 URL 参数都是以 {myParam} 格式创建的。因此,您希望读取 URL,然后将其转换为模板字符串,而无需执行复杂的替换,因为您有许多终端节点。所以你可能会做这样的事情。请注意,这是一个非常简单的示例。

const params = { id: 5 };
const route = '/api/user/{id}';
route.replace(/{/g, '${params.');
// use eval(route); to do something

eval很少是正确的选择。虽然在很多情况下,你可以通过将脚本连接在一起并动态运行它来完成你需要完成的事情,但你通常有更强大和可维护的技术可供使用:关联数组表示法(obj["prop"]obj.prop相同(、闭包、面向对象技术、函数技术 - 改用它们。

就客户端脚本而言,我认为安全问题是一个有争议的问题。加载到浏览器中的所有内容都会受到操纵,应被视为如此。当有更简单的方法来执行 JavaScript 代码和/或操作 DOM 中的对象时,使用 eval(( 语句的风险为零,例如浏览器中的 URL 栏。

javascript:alert("hello");

如果有人想操纵他们的 DOM,我说离开。防止任何类型的攻击的安全性应始终是服务器应用程序的责任,期间。

从务实的角度来看,在可以以其他方式完成事情的情况下使用 eval(( 没有任何好处。但是,在某些情况下,应使用评估。如果是这样,绝对可以做到,而不会有任何炸毁页面的风险。

<html>
    <body>
        <textarea id="output"></textarea><br/>
        <input type="text" id="input" />
        <button id="button" onclick="execute()">eval</button>
        <script type="text/javascript">
            var execute = function(){
                var inputEl = document.getElementById('input');
                var toEval = inputEl.value;
                var outputEl = document.getElementById('output');
                var output = "";
                try {
                    output = eval(toEval);
                }
                catch(err){
                    for(var key in err){
                        output += key + ": " + err[key] + "'r'n";
                    }
                }
                outputEl.value = output;
            }
        </script>
    <body>
</html>

由于还没有人提到它,让我补充一点,eval对于Webassembly-Javascript互操作非常有用。虽然在你的页面中包含你的WASM代码可以直接调用的预制脚本当然是理想的,但有时这是不切实际的,你需要从像C#这样的Web汇编语言传递动态Javascript才能真正完成你需要做的事情。

在这种情况下,它也是安全的,因为您可以完全控制传入的内容。好吧,我应该说,它并不比使用 C# 编写 SQL 语句安全,也就是说,每当使用用户提供的数据生成脚本时,都需要小心翼翼地完成(正确转义字符串等(。但有此警告,它在互操作情况下具有明确的位置,并且远非"邪恶"。

如果您完全控制传递给 eval 函数的代码,则可以使用它。

代码生成。我最近写了一个名为Hyperbars的库,它弥合了虚拟dom和车把之间的差距。它通过解析车把模板并将其转换为超标来实现这一点。超脚本首先生成为字符串,在返回之前,eval()将其转换为可执行代码。我发现eval()在这种特殊情况下与邪恶完全相反。

基本上从

<div>
    {{#each names}}
        <span>{{this}}</span>
    {{/each}}
</div>

对此

(function (state) {
    var Runtime = Hyperbars.Runtime;
    var context = state;
    return h('div', {}, [Runtime.each(context['names'], context, function (context, parent, options) {
        return [h('span', {}, [options['@index'], context])]
    })])
}.bind({}))
在这种情况下

eval()的性能也不是问题,因为您只需要解释生成的字符串一次,然后多次重用可执行输出。

如果您好奇,可以在此处查看代码生成是如何实现的。

没有理由不使用 eval((,只要你能确定代码的来源来自你或实际用户。即使他可以操纵发送到 eval(( 函数的内容,这不是安全问题,因为他能够操纵网站的源代码,因此可以更改 JavaScript 代码本身。

那么...什么时候不使用eval((?Eval(( 只有在第三方有可能更改它时才不应使用。就像拦截客户端和服务器之间的连接一样(但如果这是一个问题,请使用HTTPS(。您不应该使用 eval(( 来解析其他人编写的代码,就像在论坛中一样。

如果真的需要,eval 不是邪恶的。但是我偶然发现的 eval 的 99.9% 的用途都是不需要的(不包括 setTimeout 的东西(。

对我来说,邪恶不是性能,甚至不是安全问题(嗯,间接地两者兼而有之(。所有这些不必要的 eval 使用都增加了维护地狱。重构工具被抛弃了。搜索代码很困难。这些评估的意想不到的影响是大量的。

我使用 eval 的示例:导入

通常如何完成。

var components = require('components');
var Button = components.Button;
var ComboBox = components.ComboBox;
var CheckBox = components.CheckBox;
...
// That quickly gets very boring

但是在eval和一个小助手功能的帮助下,它的外观要好得多:

var components = require('components');
eval(importable('components', 'Button', 'ComboBox', 'CheckBox', ...));

importable可能看起来像(此版本不支持导入具体成员(。

function importable(path) {
    var name;
    var pkg = eval(path);
    var result = ''n';
    for (name in pkg) {
        result += 'if (name !== undefined) throw "import error: name already exists";'n'.replace(/name/g, name);
    }
    for (name in pkg) {
        result += 'var name = path.name;'n'.replace(/name/g, name).replace('path', path);
    }
    return result;
}
我认为

任何评估被证明是合理的情况都是罕见的。你更有可能使用它,认为它是合理的,而不是在它实际上是合理的时使用它。

安全问题是众所周知的。但也要注意JavaScript使用JIT编译,这在eval中效果很差。Eval有点像编译器的黑盒,JavaScript需要能够提前预测代码(在某种程度上(,以便安全正确地应用性能优化和范围。在某些情况下,性能影响甚至会影响 eval 之外的其他代码。

如果您想了解更多:https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch2.md#eval

如果

可能,仅在测试期间。另请注意,eval(( 比其他专门的 JSON 等评估器慢得多。

我的信念是 eval 对于客户端 Web 应用程序和安全...和JavaScript一样安全,但不是。安全问题本质上是服务器端的问题,因为现在,使用Firebug这样的工具,您可以攻击任何JavaScript应用程序。

JavaScript 的 eval(( 什么时候不是邪恶的?

我总是试图阻止使用eval。几乎总是有更干净、更易于维护的解决方案。即使对于 JSON 解析也不需要 eval。埃瓦尔增加了维护地狱。并非没有道理,它被道格拉斯·克罗克福德(Douglas Crockford(等大师所皱眉。

但我找到了应该使用它的一个例子:

当您需要传递表达式时。

例如,我有一个函数为我构造一个通用google.maps.ImageMapType对象,但我需要告诉它配方,它应该如何从zoom构造磁贴 URL 并coord参数:

my_func({
    name: "OSM",
    tileURLexpr: '"http://tile.openstreetmap.org/"+b+"/"+a.x+"/"+a.y+".png"',
    ...
});
function my_func(opts)
{
    return new google.maps.ImageMapType({
        getTileUrl: function (coord, zoom) {
            var b = zoom;
            var a = coord;
            return eval(opts.tileURLexpr);
        },
        ....
    });
}

Eval 对于没有宏的代码生成很有用。

举个愚蠢的例子,如果你正在编写一个 Brainfuck 编译器,你可能想要构造一个函数,该函数以字符串的形式执行指令序列,并对其进行评估以返回一个函数。

虽然在很多情况下,您可以通过将脚本连接在一起并动态运行它来完成您需要完成的任务,但您通常可以使用更强大且可维护的技术。 eval很少是正确的选择:关联数组表示法(obj["prop"]与obj.prop相同(,闭包, 面向对象的技术,函数式技术 - 改用它们。

当你使用parse函数(例如jQuery.parseJSON(解析JSON结构时,它需要一个完美的JSON文件结构(每个属性名称都用双引号引起来(。但是,JavaScript 更灵活。因此,您可以使用 eval(( 来避免它。