5
\$\begingroup\$

PHP's eval() is general considered insecure with any user input.

However, with filtering of input (i.e. [-+/*0-9]) you can make eval() secure for a small subset of PHP; mathematical expressions.

I've tried to think of a way to extend that by allowing a limited set of math functions and I believe I've found a secure way of doing it.

function expression($expression) {
    static $function_map = array(
        'floor'     => 'floor',
        'ceil'      => 'ceil',
        'round'     => 'round',         
        'sin'       => 'sin',
        'cos'       => 'cos',
        'tan'       => 'tan',           
        'asin'      => 'asin',
        'acos'      => 'acos',
        'atan'      => 'atan',          
        'abs'       => 'abs',
        'log'       => 'log',           
        'pi'        => 'pi',
        'exp'       => 'exp',
        'min'       => 'min',
        'max'       => 'max',
        'rand'      => 'rand',
        'fmod'      => 'fmod',
        'sqrt'      => 'sqrt',
        'deg2rad'   => 'deg2rad',
        'rad2deg'   => 'rad2deg',
    );

    // Remove any whitespace
    $expression = strtolower(preg_replace('~\s+~', '', $expression));

    // Empty expression
    if ($expression === '') {
        trigger_error('Empty expression', E_USER_ERROR);
        return null;
    }

    // Illegal function
    $expression = preg_replace_callback('~\b[a-z]\w*\b~', function($match) use($function_map) {
        $function = $match[0];
        if (!isset($function_map[$function])) {
            trigger_error("Illegal function '{$match[0]}'", E_USER_ERROR);
            return '';
        }
        return $function_map[$function];
    }, $expression);

    // Invalid function calls
    if (preg_match('~[a-z]\w*(?![\(\w])~', $expression, $match) > 0) {
        trigger_error("Invalid function call '{$match[0]}'", E_USER_ERROR);
        return null;
    }

    // Legal characters
    if (preg_match('~[^-+/%*&|<>!=.()0-9a-z,]~', $expression, $match) > 0) {
        trigger_error("Illegal character '{$match[0]}'", E_USER_ERROR);
        return null;
    }       

    return eval("return({$expression});");
}

It's use would be simple: `$result = expression('floor(3 / 2)') and anything but math/boolean operators, numbers and a limited set of functions should be filtered out.

I can't find a way of hacking this expression() function, but I'd really like to have a second opinion on this.

Is the above expression function secure for user input?

\$\endgroup\$
2
\$\begingroup\$

It looks pretty reasonable to me. A couple of points:

  1. You seem to be assuming that a function start with a letter. Functions CAN start with an underscore. Are you sure there is no such function that can be exploited?

  2. The regex in the section 'Invalid function calls' seems like it could be simpler. You are trying to check that any 'word' ends with a '(', is this right? If so, I don't think you need the second \w

Finally although this seems like a good job, the ideally ideal way to do something like this would be to make your own tokenizer. A tokenizer looks at a string char by char and decides at each point whether it's looking at part of an identifier, an operator, etc. You would simply need to call your checks at each point that it decides it has identified a complete token. The advantage of this is that it would be easy to extend the syntax you accept and if needed to make translations from the language you accept to valid PHP for eval().

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Re: 1. It shouldn't be a problem as the legal characters check does not recognize underscore as a legal character. \$\endgroup\$ – Martijn Jan 17 '14 at 11:59
  • \$\begingroup\$ Re: 2. The check is to find words that do not end with a (. The second \w is required as without it, abc( would match as ab. The \w ensures it matches at word boundary. Which is not to say there might be a more better regex that does the same job. \$\endgroup\$ – Martijn Jan 17 '14 at 12:00
  • \$\begingroup\$ Obviously you are right about 1. I may have tested 2 wrongly, will check \$\endgroup\$ – jwg Jan 17 '14 at 13:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.