在 PHP 中,何时使用 eval 函数是不好的?

90

在我多年的PHP开发经验中,我一直听说使用eval()是邪恶的。

考虑下面的代码,使用第二种(更优雅)的选项不是很合理吗?如果不是,请说明原因。

// $type is the result of an SQL statement, e.g.
// SHOW COLUMNS FROM a_table LIKE 'a_column';
// hence you can be pretty sure about the consistency
// of your string.

$type = "enum('a','b','c')";

// option one
$type_1 = preg_replace('#^enum\s*\(\s*\'|\'\s*\)\s*$#', '', $type);
$result = preg_split('#\'\s*,\s*\'#', $type_1);

// option two
eval('$result = '.preg_replace('#^enum#','array', $type).';');

3
"eval永远是有害的,在编写代码时总有更好的方法,特别是自从PHP引入匿名函数以来。在这种情况下,我会使用 $result = array(); preg_replace_callback('#^enum\s*\(\s*\'|\'\s*\)\s*$#', function($m) use($result) { $result[] = $m[1]; }, $type);" - Geoffrey
说实话,我认为 PHP 的主要问题不在于语言本身,而在于使用它的人。对于这个问题,三个正确的答案(thomasrutter、braincracking 和我的)都被投了反对票,却没有人提出任何反对意见。另一方面,有一个回答声称“有时 eval() 是唯一/正确的解决方案”,却没有举例或解释,却因此得到了最高票数... - Francois Bourgeois
21个回答

136

在称呼eval()为纯恶之前,我会保持谨慎。动态评估是一种强大的工具,有时可以拯救生命。使用eval()可以解决PHP的缺点(见下文)。

eval()的主要问题包括:

  • 可能存在不安全的输入。传递不受信任的参数是失败的一种方式。通常很难确保参数(或其中的一部分)是完全可靠的。
  • 复杂性。使用eval()使代码变得更加聪明,因此更难以跟踪。引用Brian Kernighan的话说:“调试比第一次编写代码难两倍。因此,如果您尽可能聪明地编写代码,则根据定义,您不够聪明以对其进行调试

实际使用eval()的主要问题仅有一个:

  • 没有经验的开发人员缺乏足够的考虑而使用它。

作为一个经验法则,我倾向于遵循以下原则:

  1. 有时eval()是唯一/正确的解决方案。
  2. 对于大多数情况,应该尝试其他方法。
  3. 如果不确定,请转到第2步。
  4. 否则,一定要非常小心。

4
如果$className在白名单中,就可以重构此代码以避免使用eval()函数。好消息是,自5.3版本开始,$foo::bar() 是有效的。 - rojoca
@rojoca:你能给我们提供一个在PHP 5.2中不使用eval()的例子吗?谢谢。 - Michał Niedźwiedzki
30
我不确定它是否属于评估操作,但是 $result = call_user_func(array('Foo', 'bar')); 这条语句效果非常好。 - Ionuț G. Stan
3
关于难度的问题说得好 - 在你的(简单)例子中,一个变量似乎“从无到有”地出现。如果代码变得稍微复杂一点,下一个查看代码并尝试追踪该变量的人就会束手无策了(我已经历过这样的事情,最后只得了个头痛和这件不起眼的T恤)。 - Piskvor left the building
不安全的输入是可以避免的。 - user6355773

42

如果存在任何可能用户输入被包含在评估字符串中,那么eval是有害的。

如果没有来自用户的内容,你可以安全地使用eval。

尽管如此,在使用eval之前,你应该至少考虑两次。它看起来非常简单,但是考虑到错误处理(请参见VBAssassins评论)、调试等因素,它并不是那么简单。

所以作为一个经验法则: 忘记它吧。当eval成为答案时,你可能正在问错问题!;-)


6
有时候 eval 就是答案。我们开发在线游戏应用,由于实体之间非常复杂的关系,很难避免使用 eval,但在我看来,在 90% 的情况下,eval 不是最佳选择。 - Jet
20
我很好奇能否出现只有使用"eval"是答案的情况。 - Ionuț G. Stan
2
@Ionut G. Stan 数据库中存储对象/实体的自定义触发器? - Kuroki Kaze
8
我并不认为这些情况下使用 eval() 是合理的。在每种情况下,不使用 eval() 仍然是至少可能的。PHP 没有 eval() 也不会被破坏。当然,eval() 在这些情况下是一种快捷方式,但它仍然使你的代码路径更难跟踪和问题更难调试。这是我的看法。 - thomasrutter
4
@Christian:你应该先写一个例子(使用pastebin.com,并在此粘贴链接),证明避免使用 eval() 是不可能的,这是一个更好的方法。很多人说在某些情况下使用 eval() 是不可避免的,但他们没有提到任何具体的例子可以供我们争论 - 这样这场辩论就没有意义了。所以请那些声称 eval() 是不可避免的人先证明它! - Sk8erPeter
显示剩余6条评论

21

eval在任何时候都同样“危险”。

如果您认为它是危险的,那么在任何时候它都同样危险。许多人描述它为危险的原因并不随着上下文的改变而消失。

通常使用eval()是一个坏主意,因为它会降低代码的可读性,影响您在运行时之前预测代码路径的能力(这可能存在安全隐患),从而影响分析和调试代码的能力。使用eval()还可以防止被类似于PHP 5.5及以上版本中集成的Zend Opcache或HHVM中的JIT编译器等opcode缓存优化。

此外,在PHP中没有绝对必要使用eval() - PHP是一种完全有能力的编程语言,即使没有它。无论您想使用eval()做什么,都有另一种方法在PHP中实现。

无论您是否真正将这些视为危险,或者您个人是否可以证明使用eval()是合理的,这取决于您。对于某些人来说,风险太大,无法证明其正确性;而对于其他人来说,eval()是一个便捷快捷方式。


2
你是一个非常优秀的程序员。 - Christian
4
我的回答是:我认为将 PHP 代码存储在数据库中同样是不好的,也是没有必要的。无论你想要实现什么目标,都有其他方法可以做到,而不必将 PHP 存储在数据库中或使用 eval() 函数。如果你喜欢这样做,并且它对你有帮助,那就去做吧,但如果你认为 eval() 函数是不好的,那么你可能也会认为将 PHP 存储在数据库中同样是不好的。 - thomasrutter
13
它增加了另一个攻击途径——SQL注入可能在Web服务器上运行任意PHP代码。它需要使用eval()函数。它不能被字节码缓存优化。它违反了将代码与数据分离的原则。它可能会使您的应用程序不太容易移植——更新/修复PHP还需要更新数据库。 - thomasrutter
1
那些足够了解安全性以能够安全地使用eval()的人不使用eval(),因为他们足够了解安全性而不使用它。仅仅因为人们做出了糟糕的设计选择,比如在数据库中编写代码,最终需要使用eval(),并不意味着这是正确的选择。 - Kevin Nelson
3
如果某些事情只能通过使用eval来实现,那么最好重新考虑是否这么做。在运行时动态创建类似乎不是一个好主意。为什么不使用一些代码生成,在运行前生成定义它们的代码呢? - thomasrutter
显示剩余5条评论

15

在这种情况下,如果用户无法创建表中的任意列,则eval可能足够安全。

不过,这并不是特别优雅的解决方案。这实际上是一个文本解析问题,并且滥用PHP解析器来处理它似乎有点粗糙。如果你想要滥用语言特性,为什么不滥用JSON解析器呢?至少在JSON解析器中,根本没有可能进行代码注入。

$json = str_replace(array(
    'enum', '(', ')', "'"), array)
    '',     '[', ']', "'"), $type);
$result = json_decode($json);

正则表达式可能是最明显的方法。您可以使用一个正则表达式从此字符串中提取所有值:

$extract_regex = '/
    (?<=,|enum\()   # Match strings that follow either a comma, or the string "enum("...
    \'      # ...then the opening quote mark...
    (.*?)       # ...and capture anything...
    \'      # ...up to the closing quote mark...
    /x';
preg_match_all($extract_regex, $type, $matches);
$result = $matches[1];

1
尽管这并没有直接回答问题,但这是一个非常好的回答:解析enum()的最佳方法是什么...谢谢;) - Pierre Spring

14

eval()虽然运行速度较慢,但我不会称其为邪恶。

是我们错误地使用它可能导致代码注入并变得邪恶。

以下是一个简单的例子:

$_GET = 'echo 5 + 5 * 2;';
eval($_GET); // 15
一个有害的例子:
$_GET = 'system("reboot");';
eval($_GET); // oops

我建议您不要使用 eval(),但如果您确实需要使用,请确保验证/白名单所有输入。


12

当您在 eval 函数中使用外部数据(例如用户输入)时。

在上面的示例中,这不是一个问题。


7
我会毫不客气地复制这里的内容:
  1. 由于其本质,eval 总是会成为一个安全问题。

  2. 除了安全问题之外,eval 还存在一个极其缓慢的问题。在我对 PHP 4.3.10 进行测试时,它比正常代码慢 10 倍,在 PHP 5.1 beta1 上慢 28 倍。

blog.joshuaeichorn.com: using-eval-in-php


6

eval()总是有害的。

  • 出于安全原因
  • 出于性能原因
  • 出于可读性/重用性原因
  • 出于IDE/工具原因
  • 出于调试原因
  • 总是有更好的方法

@bracketworks:但你错了——所有这些固有问题不会在某些情况下魔法般地消失。为什么要消失呢?以性能为例:您真的认为解释器会因为您以某种神秘的“非邪恶”方式使用eval()函数而以更高的速度执行它吗?或者,有一天幸运地在eval子句中进行逐步调试会奏效吗? - Francois Bourgeois
3
坦白地说,我认为@MichałRudnicki的回答说得最好。别误会,每当我问自己一个问题,而答案是eval()时,我往往会认为我问错了问题;很少有情况下它是“正确”的答案,但是说它始终是邪恶的是不正确的。 - Dan Lugg
如果我想将代码存储在数据库中,我该如何执行它? - Konstantin XFlash Stratigenas
@KonstantinXFlashStratigenas 你应该问问自己为什么要在数据库中存储可执行代码。数据库是用于存储代码生成的数据,而不是代码本身。至少,这样做会增加攻击向量。 - Jack B

4

我建议您考虑到维护您的代码的人。

eval()函数并不容易看懂,不容易知道它应该做什么,尽管您的示例并不算太糟糕,但在其他地方可能会成为一个噩梦。


4
个人认为这段代码仍然非常邪恶,因为你没有注释它正在做什么。它也没有测试其输入的有效性,使其非常脆弱。
我还觉得,由于 eval 的使用中 95%(或更多)是有风险的,所以它在其他情况下可能提供的小潜在节省时间不值得沉迷于使用它的不良习惯。此外,您将来必须向您的下属解释为什么您使用 eval 是好的,而他们使用 eval 是坏的。
当然,你的 PHP 最终看起来像 Perl ;)
eval() 有两个关键问题(作为“注入攻击”场景):
1)它可能会造成伤害 2)它可能只是崩溃
还有一个比技术更社交化的问题:
3)它会引诱人们在其他地方不适当地使用它作为捷径
在第一种情况下,您面临着任意代码执行的风险(显然,当您 eval'ing 已知字符串时不会出现此风险)。但是您的输入可能并不像您想象的那样已知或固定。
在这种情况下更有可能(在本例中)只是崩溃,并且您的字符串将以过度复杂的错误消息终止。在我看来,所有代码都应该尽可能地失败,否则它应该抛出异常(作为最可处理的错误形式)。
我建议,在这个例子中,您正在通过巧合而不是通过行为进行编码。是的,SQL 枚举语句(你确定该字段是枚举吗?-你调用了正确的表格的正确版本的正确字段吗?它真的回答了吗?)看起来像 PHP 中的数组声明语法,但我建议您真正想要做的是不要找到从输入到输出的最短路径,而是解决指定的任务:
1. 确定您有一个枚举 2. 提取内部列表 3. 解包列表值
这大致是您的选项一所做的,但我会在其周围添加一些 if 和注释以获得清晰度和安全性(例如,如果第一个匹配不匹配,则引发异常或设置空结果)。
仍然存在一些可能存在转义逗号或引号的问题,您可能需要解压数据然后取消引号,但它至少将数据视为数据,而不是视为代码。
使用 preg_version,您最坏的结果可能是 $result=null,使用 eval 版本最坏的情况是未知的,但至少会崩溃。

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