33

Disclaimer; I'm fully aware of the pitfalls and "evils" of eval, including but not limited to: performance issues, security, portability etc.

The problem

Reading the PHP manual on eval...

eval() returns NULL unless return is called in the evaluated code, in which case the value passed to return is returned. If there is a parse error in the evaluated code, eval() returns FALSE and execution of the following code continues normally. It is not possible to catch a parse error in eval() using set_error_handler().

In short, no error capture except returning false which is very helpful, but I'm sur eI could do way better!

The reason

A part of the site's functionality I'm working on relies on executing expressions. I'd like not to pass through the path of sandbox or execution modules, so I've ended using eval. Before you shout "what if the client turned bad?!" know that the client is pretty much trusted; he wouldn't want to break his own site, and anyone getting access to this functionality pretty much owns the server, regardless of eval.

The client knows about expressions like in Excel, and it isn't a problem explaining the little differences, however, having some form of warning is pretty much standard functionality.

This is what I have so far:

define('CR',chr(13));
define('LF',chr(10));

function test($cond=''){
    $cond=trim($cond);
    if($cond=='')return 'Success (condition was empty).'; $result=false;
    $cond='$result = '.str_replace(array(CR,LF),' ',$cond).';';
    try {
        $success=eval($cond);
        if($success===false)return 'Error: could not run expression.';
        return 'Success (condition return '.($result?'true':'false').').';
    }catch(Exception $e){
        return 'Error: exception '.get_class($e).', '.$e->getMessage().'.';
    }
}

Notes

  • The function returns a message string in any event
  • The code expression should be a single-line piece of PHP, without PHP tags and without an ending semicolon
  • New lines are converted to spaces
  • A variable is added to contain the result (expression should return either true or false, and in order not to conflict with eval's return, a temp variable is used.)

So, what would you add to further aide the user? Is there any further parsing functions which might better pinpoint possible errors/issues?

Chris.

Christian
  • 27,509
  • 17
  • 111
  • 155
  • if you could provide more feedback on what "expressions" you will use, maybe we can help more. I could think of some nice token_get_all stuff to validate the user input ;) – NikiC Jul 11 '10 at 17:32
  • Normal PHP code? I plan to allow full access to PHP, with the possible exception of defining functions and classes, which isn't needed. – Christian Jul 11 '10 at 17:37
  • 2
    @Sz. that being said, I'd recommend using [symfony/expression-language](https://github.com/symfony/expression-language) nowadays (it was not available back in '10). – Christian Oct 02 '18 at 10:38

6 Answers6

31

Since PHP 7 eval() will generate a ParseError exception for syntax errors:

try {
    $result = eval($code);
} catch (ParseError $e) {
    // Report error somehow
}

In PHP 5 eval() will generate a parse error, which is special-cased to not abort execution (as parse errors would usually do). However, it also cannot be caught through an error handler. A possibility is to catch the printed error message, assuming that display_errors=1:

ob_start();
$result = eval($code);
if ('' !== $error = ob_get_clean()) {
    // Report error somehow
}
NikiC
  • 100,734
  • 37
  • 191
  • 225
  • Yes, I did define them, sorry for not mentioning. Fixed above code. As to exceptions, I'm pretty sure it doesn't neither, however, this code might fail in the future after perhaps adding exceptions to it, so it doesn't hurt handling them anyway. – Christian Jul 11 '10 at 17:35
  • I was not able to find `check_syntax`. – hakre Jan 31 '12 at 13:21
  • 1
    `php_check_syntax` was removed in 5.0.5, they expect you to do something like `exec("echo '/dev/null", $out, $ret);` which is _so_ much more convenient. – user9645 Jul 02 '15 at 19:13
  • `!==` operator has higher precedence than `=`. You need to add parentheses: `('' !== ($error = ob_get_clean()))` – François Sep 29 '15 at 01:41
  • 1
    @François Not if the assignment is on the RHS. That's why a Yoda condition is used here. – NikiC Sep 29 '15 at 13:57
  • @Sz. Good point, I've updated the answer to mention ParseError. – NikiC Oct 30 '18 at 22:34
  • if you're using namespaces, remember the backslash in `catch (\ParseError)`! I spent an hour trying to figure out why my script wasn't catching the error correctly... adding the backslash fixed it. – uryga Mar 19 '20 at 22:09
15

I've found a good alternative/answer to my question.

First of, let me start by saying that nikic's suggestion works when I set error_reporting(E_ALL); notices are shown in PHP output, and thanks to OB, they can be captured.

Next, I've found this very useful code:

/**
 * Check the syntax of some PHP code.
 * @param string $code PHP code to check.
 * @return boolean|array If false, then check was successful, otherwise an array(message,line) of errors is returned.
 */
function php_syntax_error($code){
    if(!defined("CR"))
        define("CR","\r");
    if(!defined("LF"))
        define("LF","\n") ;
    if(!defined("CRLF"))
        define("CRLF","\r\n") ;
    $braces=0;
    $inString=0;
    foreach (token_get_all('<?php ' . $code) as $token) {
        if (is_array($token)) {
            switch ($token[0]) {
                case T_CURLY_OPEN:
                case T_DOLLAR_OPEN_CURLY_BRACES:
                case T_START_HEREDOC: ++$inString; break;
                case T_END_HEREDOC:   --$inString; break;
            }
        } else if ($inString & 1) {
            switch ($token) {
                case '`': case '\'':
                case '"': --$inString; break;
            }
        } else {
            switch ($token) {
                case '`': case '\'':
                case '"': ++$inString; break;
                case '{': ++$braces; break;
                case '}':
                    if ($inString) {
                        --$inString;
                    } else {
                        --$braces;
                        if ($braces < 0) break 2;
                    }
                    break;
            }
        }
    }
    $inString = @ini_set('log_errors', false);
    $token = @ini_set('display_errors', true);
    ob_start();
    $code = substr($code, strlen('<?php '));
    $braces || $code = "if(0){{$code}\n}";
    if (eval($code) === false) {
        if ($braces) {
            $braces = PHP_INT_MAX;
        } else {
            false !== strpos($code,CR) && $code = strtr(str_replace(CRLF,LF,$code),CR,LF);
            $braces = substr_count($code,LF);
        }
        $code = ob_get_clean();
        $code = strip_tags($code);
        if (preg_match("'syntax error, (.+) in .+ on line (\d+)$'s", $code, $code)) {
            $code[2] = (int) $code[2];
            $code = $code[2] <= $braces
                ? array($code[1], $code[2])
                : array('unexpected $end' . substr($code[1], 14), $braces);
        } else $code = array('syntax error', 0);
    } else {
        ob_end_clean();
        $code = false;
    }
    @ini_set('display_errors', $token);
    @ini_set('log_errors', $inString);
    return $code;
}

Seems it easily does exactly what I need (yay)!

Franz Holzinger
  • 913
  • 10
  • 20
Christian
  • 27,509
  • 17
  • 111
  • 155
  • Wow, this looks nice. And again I've learned something new: `break` can have an argument! I will later cast a look at this code to really understand what it does. Only one note: If there are heavy parse errors in the code, `token_get_all` can output errors by itself, e.g. if you are using `\\` in the source (in PHP <5.3). – NikiC Jul 12 '10 at 11:22
  • 2
    It is very nice. Sadly it still does not - can not - provide protection against wrong function calls, e.g. simple spelling mistakes in function names. PHP should really think about catching fatal errors in eval(). – Steve Horvath Oct 19 '16 at 03:14
10

How to test for parse errors inside eval():

$result = @eval($evalcode . "; return true;");

If $result == false, $evalcode has a parse error and does not execute the 'return true' part. Obviously $evalcode must not return itself something, but with this trick you can test for parse errors in expressions effectively...

Robin van Baalen
  • 3,632
  • 2
  • 21
  • 35
Davide Moretti
  • 117
  • 2
  • 3
  • Very intelligent idea! I haven't tested it yet but I will. – itoctopus Nov 05 '15 at 19:04
  • 2
    You can't catch Parse Errors in `eval`'d code! It would just stop execution of your script. – DUzun May 23 '16 at 07:18
  • This worked great for me, even with my eval() returning a value. I have `$value=eval("return ".$evalcode."; return false;");` and then `if ($value===false) echo ("Error with: ".$evalcode);` Of course this means that evalcode can never return false in a normal circumstance. But that fits my use. – rgbflawed Oct 20 '16 at 20:03
3

Good news: As of PHP 7, eval() now* throws a ParseError exception if the evaluated code is invalid:

try
{
    eval("Oops :-o");
}
catch (ParseError $err)
{
    echo "YAY! ERROR CAPTURED: $err";
}

* Well, for quite a while then... ;)

Sz.
  • 3,342
  • 1
  • 30
  • 43
3

I think that best solution is

try {
    eval(/* ... */);
} catch (Throwable $t) {
    //...
}

It catches every error and exception, including Call to undefined function etc

Majkel
  • 131
  • 2
  • 5
2

You can also try something like this:

$filePath = '/tmp/tmp_eval'.mt_rand();
file_put_contents($filePath, $evalCode);
register_shutdown_function('unlink', $filePath);
require($filePath);

So any errors in $evalCode will be handled by errors handler.

barbushin
  • 5,165
  • 5
  • 37
  • 43