88

在我从事 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).';');
4

20 回答 20

135

我会谨慎地将 eval() 称为纯粹的邪恶。动态评估是一种强大的工具,有时可以挽救生命。使用 eval() 可以解决 PHP 的缺点(见下文)。

eval() 的主要问题是:

  • 潜在的不安全输入。传递不受信任的参数是一种失败的方式。确保参数(或其中的一部分)被完全信任通常不是一项简单的任务。
  • 诡计。使用 eval() 使代码更聪明,因此更难遵循。引用 Brian Kernighan 的话:“首先,调试的难度是编写代码的两倍。因此,如果你尽可能巧妙地编写代码,那么根据定义,你就不够聪明,无法调试它

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

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

根据经验,我倾向于遵循以下原则:

  1. 有时 eval() 是唯一/正确的解决方案。
  2. 在大多数情况下,应该尝试其他方法。
  3. 如果不确定,请转到 2。
  4. 否则,要非常非常小心。
于 2009-06-04T17:08:24.517 回答
42

当用户输入包含在评估字符串中的可能性很小时,eval 是邪恶的。当您在没有来自用户的内容的情况下进行评估时,您应该是安全的。

尽管如此,在使用 eval 之前您至少应该三思而后行,它看起来非常简单,但考虑到错误处理(参见 VBAssassins 评论)、可调试性等,它不再那么简单了。

所以作为一个经验法则:忘记它。当 eval 是答案时,您可能是在问错误的问题!;-)

于 2009-06-04T15:47:52.923 回答
19

eval 在任何时候都同样“邪恶”。

如果你认为它是邪恶的,那么它在任何时候都是同样邪恶的。许多人将其描述为邪恶的原因并没有随着上下文而消失。

使用 eval() 通常是一个坏主意,因为它会降低代码的可读性,它会削弱您在运行前预测代码路径的能力(这可能会带来安全隐患),从而影响分析和调试代码的能力。使用 eval() 还可以防止被评估的代码及其周围的代码被操作码缓存(如集成到 PHP 5.5 及更高版本中的 Zend Opcache)或 JIT 编译器(如 HHVM 中的编译器)优化。

此外,没有绝对必要使用 eval() 的情况——没有它,PHP 是一种功能齐全的编程语言。不管你想用 eval() 做什么,在 PHP 中都会有另一种方法。

无论您是否真的将这些视为邪恶,或者您是否可以个人使用 eval() 来证明是正确的,这取决于您。对一些人来说,陷阱太大了,无法证明它是正确的,而对另一些人来说,eval() 是一个方便的捷径。

于 2009-06-05T01:36:37.000 回答
15

在这种情况下, eval 可能足够安全,只要用户永远不可能在表中创建任意列。

不过,它实际上并不优雅。这基本上是一个文本解析问题,滥用 PHP 的解析器来处理似乎有点 hacky。如果你想滥用语言特性,为什么不滥用 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];
于 2009-06-04T16:21:19.317 回答
14

eval()很慢,但我不会称之为邪恶。

正是我们对它的不良使用可能导致代码注入和邪恶。

一个简单的例子:

$_GET = 'echo 5 + 5 * 2;';
eval($_GET); // 15

一个有害的例子:

$_GET = 'system("reboot");';
eval($_GET); // oops

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

于 2010-01-14T02:36:04.450 回答
12

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

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

于 2009-06-04T15:46:50.773 回答
7

我会在这里公然窃取内容:

  1. 从本质上讲,评估始终是一个安全问题。

  2. 除了安全问题之外,eval 还存在速度非常慢的问题。在我对 PHP 4.3.10 的测试中,它比普通代码慢 10 倍,在 PHP 5.1 beta1 上慢 28 倍。

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

于 2009-06-04T16:51:01.823 回答
5

eval()总是邪恶的。

  • 出于安全原因
  • 出于性能原因
  • 出于可读性/可重用性的原因
  • 出于 IDE / 工具的原因
  • 出于调试原因
  • 总有更好的方法
于 2013-01-31T13:50:07.560 回答
4

我也会考虑维护您的代码的人。

eval() 并不容易查看并知道应该发生什么,您的示例还不错,但在其他地方它可能是一场正确的噩梦。

于 2009-06-04T16:00:30.163 回答
4

就个人而言,我认为该代码仍然很邪恶,因为您没有评论它在做什么。它也没有测试其输入的有效性,使其非常脆弱。

我还认为,由于 eval 的 95%(或更多)使用是非常危险的,因此它在其他情况下可能提供的少量潜在时间节省不值得沉迷于使用它的不良做法。另外,您稍后必须向您的爪牙解释为什么您使用 eval 是好的,而他们的使用是坏的。

而且,当然,您的 PHP 最终看起来像 Perl ;)

eval() 有两个关键问题,(作为“注入攻击”场景):

1)它可能会造成伤害 2)它可能会简单地崩溃

还有一个比技术更社交的:

3) 它会诱使人们在别处不恰当地使用它作为捷径

在第一种情况下,您冒着执行任意代码的风险(显然,不是在评估已知字符串时)。不过,您的输入可能并不像您想象的那样广为人知或固定不变。

更有可能(在这种情况下)您会崩溃,并且您的字符串将以无缘无故的晦涩错误消息终止。恕我直言,所有代码都应该尽可能整齐地失败,失败应该抛出异常(作为最可处理的错误形式)。

我建议,在这个例子中,你是巧合地编码而不是编码行为。是的,SQL 枚举语句(您确定该字段的枚举吗?-您是否调用了正确版本数据库的正确表的正确字段?它真的回答了吗?)恰好看起来像 PHP 中的数组声明语法,但我建议您真正想做的不是找到从输入到输出的最短路径,而是解决指定的任务:

  • 确定您有一个枚举
  • 提取内部列表
  • 解压列表值

这大致是您的选项所做的,但为了清楚和安全起见,我会在其周围加上一些 if 和注释(例如,如果第一个匹配项不匹配,则抛出异常或设置 null 结果)。

转义逗号或引号仍然存在一些可能的问题,您可能应该解压缩数据然后取消引用它,但它至少将数据视为数据,而不是代码。

对于 preg_version,您最坏的结果可能是 $result=null,对于 eval 版本,最坏的结果是未知的,但至少是崩溃。

于 2009-06-04T16:19:59.070 回答
3

eval 将字符串评估为代码,问题在于如果字符串以任何方式“被污染”,它可能会暴露出巨大的安全威胁。通常问题是在字符串中评估用户输入的情况下,在许多情况下,用户可以输入代码(例如 php 或 ssi),然后在 eval 中运行,它将以与您的 php 脚本相同的权限运行,并且可以用于获取信息/访问您的服务器。在将用户输入交给 eval 之前确保正确清除用户输入可能非常棘手。还有其他问题……其中一些值得商榷

于 2009-06-04T16:24:00.190 回答
3

PHP 建议您以这样一种方式编写代码:它可以通过 call_user_func 执行,而不是执行显式 eval。

于 2009-06-05T01:13:44.893 回答
2

另一个原因eval是邪恶的,它不能被 PHP 字节码缓存(如 eAccelertor 或 ACP)缓存。

于 2010-01-14T04:00:45.713 回答
2

糟糕的编程让 eval() 变得邪恶,而不是函数。我有时会使用它,因为我无法在多个站点的动态编程中绕过它。我不能在一个站点上解析 PHP,因为我不会收到我想要的东西。我只会收到一个结果!我很高兴 eval() 存在一个函数,因为它让我的生活更加轻松。用户输入?只有糟糕的程序员才会被黑客勾引。我不担心那个。

于 2011-02-27T12:29:27.357 回答
2

糟糕的编程让 eval() 变得邪恶,而不是函数。我有时会使用它,因为我无法在多个站点的动态编程中绕过它。我不能在一个站点上解析 PHP,因为我不会收到我想要的东西。我只会收到一个结果!我很高兴 eval() 存在一个函数,因为它让我的生活更加轻松。用户输入?只有糟糕的程序员才会被黑客勾引。我不担心那个。

我预测你很快就会遇到严重的问题......

老实说,在 PHP 这样的解释性语言中,像 eval 这样的过高函数绝对没有用处。我从未见过 eval 执行无法使用其他更安全的方式执行的程序功能......

对于所有认为测试用户输入会有所帮助的人,我完全同意,Eval 是万恶之源。三思而后行,用户输入可以有多种不同的形式,正如我们所说,黑客正在利用你不够关心的功能。在我看来,完全避免 eval 。

我见过精心设计的例子来滥用超出我自己创造力的 eval 函数。从安全的角度来看,不惜一切代价避免,我什至会要求它至少是 PHP 配置中的一个选项,而不是“给定的”。

于 2011-05-26T15:00:02.173 回答
1

我以前经常使用 eval(),但我发现大多数情况下你不必使用 eval 来做技巧。好吧,您在 PHP 中有 call_user_func() 和 call_user_func_array()。静态和动态调用任何方法就足够了。

要执行静态调用,请将回调构造为 array('class_name', 'method_name'),甚至是像 'class_name::method_name' 这样的简单字符串。要执行动态调用,请使用 array($object, 'method') 样式回调。

eval() 唯一合理的用途是编写自定义编译器。我做了一个,但 eval 仍然是邪恶的,因为它很难调试。最糟糕的是,评估代码中的致命错误会使调用它的代码崩溃。我至少使用 Parsekit PECL 扩展来检查语法,但仍然没有乐趣 - 尝试引用未知类和整个应用程序崩溃。

于 2010-01-14T01:56:17.440 回答
1

这是运行从数据库中提取的 PHP 代码而不使用 eval 的解决方案。允许所有范围内的功能和例外:

$rowId=1;  //database row id
$code="echo 'hello'; echo '\nThis is a test\n'; echo date(\"Y-m-d\");"; //php code pulled from database

$func="func{$rowId}";

file_put_contents('/tmp/tempFunction.php',"<?php\nfunction $func() {\n global \$rowId;\n$code\n}\n".chr(63).">");

include '/tmp/tempFunction.php';
call_user_func($func);
unlink ('/tmp/tempFunction.php');

基本上,它使用文本文件中包含的代码创建一个独特的函数,包含该文件,调用该函数,然后在完成后删除该文件。我正在使用它来执行日常数据库摄取/同步,其中每个步骤都需要唯一的代码来处理。这解决了我面临的所有问题。

于 2015-04-02T18:12:53.877 回答
0

除了安全问题,eval() 不能编译、优化或缓存操作码,因此它总是比普通的 php 代码慢 -慢得多。因此,使用 eval 是没有性能的,尽管这并不会使它变得邪恶。(goto是邪恶的,eval只是不好的做法/臭代码/丑陋)

于 2012-06-29T01:59:13.670 回答
0

大多数人会指出,当您处理用户输入(这是可以处理的)时,这可能很危险。

对我来说,最糟糕的是它降低了代码的可维护性

  • 难以调试
  • 很难更新
  • 限制工具和助手的使用(如 IDE)
于 2020-07-17T10:29:55.067 回答
0

另一种选择

参考:闭包流

class ClosureStream
{
    const STREAM_PROTO = 'closure';

    protected static $isRegistered = false;

    protected $content;

    protected $length;

    protected $pointer = 0;

    function stream_open($path, $mode, $options, &$opened_path)
    {
        $this->content = "<?php\nreturn " . substr($path, strlen(static::STREAM_PROTO . '://')) . ";";
        $this->length = strlen($this->content);
        return true;
    }

    public function stream_read($count)
    {
        $value = substr($this->content, $this->pointer, $count);
        $this->pointer += $count;
        return $value;
    }

    public function stream_eof()
    {
        return $this->pointer >= $this->length;
    }

    public function stream_set_option($option, $arg1, $arg2)
    {
        return false;
    }

    public function stream_stat()
    {
        $stat = stat(__FILE__);
        $stat[7] = $stat['size'] = $this->length;
        return $stat;
    }

    public function url_stat($path, $flags)
    {
        $stat = stat(__FILE__);
        $stat[7] = $stat['size'] = $this->length;
        return $stat;
    }

    public function stream_seek($offset, $whence = SEEK_SET)
    {
        $crt = $this->pointer;

        switch ($whence) {
            case SEEK_SET:
                $this->pointer = $offset;
                break;
            case SEEK_CUR:
                $this->pointer += $offset;
                break;
            case SEEK_END:
                $this->pointer = $this->length + $offset;
                break;
        }

        if ($this->pointer < 0 || $this->pointer >= $this->length) {
            $this->pointer = $crt;
            return false;
        }

        return true;
    }

    public function stream_tell()
    {
        return $this->pointer;
    }

    public static function register()
    {
        if (!static::$isRegistered) {
            static::$isRegistered = stream_wrapper_register(static::STREAM_PROTO, __CLASS__);
        }
    }

}
ClosureStream::register();
// Your code here!
$closure=include('closure://function(){echo "hola mundo";}');
$closure();
于 2021-09-17T15:44:15.413 回答