Eval alternative to run script from string

Viewed 731
$variable = "echo 'example'";
eval ($variable);

I need to run the $variable that has code inside, without using the eval function, I'm using php 7.4. How can I do that?

Hint: I'm also using Laravel 6.x, is there a Laravel function for that as well?

1 Answers

Are you sure you have to do that? If you have only a limited number of possible functions, the safest thing to do is to simply map them, and then separate the wanted function and the arguments in the variable, using an array instead. More on that later.

If you wanted to provide a generic means for calling functions, you could for example:

$cmd = ['printf', 'foo'];
// interpreted as: printf('foo')
$cmd[0]($cmd[1]); 

I'm using printf here, since echo is a language statement and not a function, and therefore wouldn't work like this. If you had to echo here, you'd have to make your own echo wrapper function, like function do_echo($str) { echo $str; }, and call that instead.

Or, for more readable code, you could list the array as:

list($func, $arg) = $cmd;
$func($arg);

If you needed multiple arguments, you could use this structure:

$cmd = ['str_replace', ['sub', 'ob', 'subject']];
list($func, $args) = $cmd;

// Interpreted as: str_replace('sub', 'ob', 'subject'):
$result = $func(...$args);

echo $result; // 'object'

Where the arguments listed in the arguments array, unpacked with the splat operator, are in the same order as the target function's arguments.

There are of course only a limited number of useful functions that will directly output anything. Therefore, in the above example we capture the result from $func instead and then echo; this simply to illustrate the basic use of variable functions and arguments.

This approach will however provide unhinged access to any function in your system, and as such shouldn't be used on anything but data/calls from trusted sources (you). Allowing users to provide any commands will open up your system to a world of mischief and misery.

Therefore, my initial recommendation of providing a map of functions. You could:

  1. Have an array with a list of allowed core functions,
  2. Have an array with a list of your custom wrapper functions, or
  3. Check for valid wrapper functions named with a prefix

You could for example implement wrappers as follows:

function do_echo($str) {
    echo $str;
}
function do_replace($se, $re, $str) {
    // return or echo, up to you:
    echo str_replace($se, $re, $str);
}

This would give you control over what's executed, and also whether values are returned or output, etc. pre/post-process. And then iterate your variable commands as follows:

$cmd = ['echo', 'off'];
list($func, $args) = $cmd;

$func = 'do_' . $func; // adding your prefix

if(function_exists($func)) {
    // call with array, unpack arguments:
    if(is_array($args) {
        $func(...$args);
    } 
    // or call with scalar argument:
    else {
        $func($args); // interpreted as `do_echo('off')
    }
}

For an additional layer of insulation, create a class with your custom calls as its methods, instead of polluting the global space with more functions. Ciykd use a public router method that handles no-match cases; calling e.g. Func::code($cmd), where code() returns the do_* methods.

I often do something like this when I need to map user requests to class public methods that return output; named e.g. view_home, view_search etc. and view_default for a fallback for unmatched requests. Handy for quick prototyping, where index.php?view=* => $this->view_*.

If you're stuck with receiving a string command (why?), you can use regex to parse it into something you can pass to functions. Or, if you absolutely trust the data, eval isn't inherently and categorically evil, especially where a work-around would make for a more complex but equally wide back-door to your system. It's just not very elegant and smells of sloppy design.

Related