Sandboxing Untrusted Code With PHPSandbox

  • Written by Elijah Horton on Thursday April 25, 2013 in the category
  • PHP.
  • 5 comments

"Eval is evil"

Few quotes related to the PHP language are as pithy and resoundingly accurate as the phrase: "Eval is evil." The reasons are myriad: the eval() function basically gives whatever code is passed to it unlimited control of the parser, and this freedom makes eval() both a temptation for developers, who may need to dynamically control PHP at runtime, and a panacea for hackers who are ever-searching for more servers to add to their botnets.

So, how does one make use of the extreme power available through runtime evaluation of PHP, without exposing one's server to near-certain rooting?

Through a sandbox. In our case, making use of Nikita Popov's excellent PHPParser library, wrapped neatly in the form of the PHPSandbox library (developed by yours truly.)

Let's use an example of an online PHP code tester. The typical approach for a developer who wishes to make this feature available for his visitors is to read the input code and search for various "bad" keywords, like mail() or system()

Note: This example is intentionally poorly written, to demonstrate the vulnerability.

$code = $_POST["code"];
if(stripos($code, "mail(") !== false){
    die("Cannot use the mail() function!");
}
@eval($code);

The problem with this approach should be immediately apparent: PHP is an extremely fluid language, particularly in its implementation of variable names, and the mail() function call can be obfuscated easily within the submitted code.

For example, the following code will still call the mail() function and yet not be caught by the server's check:

$evil_var = "mail";

$evil_var("spam@spam.com", "FREE SPAM", "Free spam, only $9.99!");

Yikes. Despite the developer's attempt at keeping his server from being use to distribute spam, it is easily induced to do that very thing.

Some developers will attempt to use a mixture of regular expressions, or PHP's own disable_functions directive in php.ini, or any number of methods to mitigate this massive loophole, but invariably their efforts will fail due to some obscure or creative way to use PHP's inherent flexibility against itself.

PHPSandbox takes a different approach. Utilizing the PHPParser library, PHPSandbox will statically analyze submitted code, checking function calls, class instantiations, and a wide variety of PHP actions against developer-defined whitelists and blacklists. By default, PHPSandbox will throw an exception if a mail() function call is made without the mail() function being defined as part of its function whitelist. It also can intercept the value of variable variables, so the $evil_var example above would also throw an exception. Almost every aspect of submitted PHP code can be analyzed and checked against rules defined by the developer, so rather than working with an all-powerful eval() function and attempting to limit potential damage, the developer can instead start with an empty, sandboxed enivronment and only expose the functionality he wishes to the user-submitted code.

Installing PHPSandbox

PHPSandbox is a composer package, so installing it to your project is as simple as adding the following lines to your composer.json file:

{
    "minimum-stability": "dev",
    "require": {
        "fieryprophet/php-sandbox": "dev-master"
    }
}

Once you have installed the PHPSandbox package, you can create a new PHPSandbox instance like so:

$sandbox = new \PHPSandbox\PHPSandbox;

Configuring PHPSandbox

Let's say that we wanted to allow client-side users to submit mathematical expressions in PHP, without allowing access to any functions, globals, or other values outside the scope of the submitted code. The default configuration of the PHPSandbox instance will throw an exception if anything but echo() and variables are called.

So, if a client submits the following code:

echo (100 * 3);

And this code is passed to the PHPSandbox:

$sandbox->execute($_POST['code']);

The client will receive the following response:

300

If, however, the client attempts to submit the following code:

mail("spam@spam.com", "FREE SPAM", "Free spam, only $9.99!");

They will receive the following response:

Fatal error: Uncaught exception 'PHPSandbox\Error' with message 'Sandboxed code attempted to call non-whitelisted function: mail'

Now, let's say we want to allow our clients to have access to the rand() function for use in their mathematical expressions. We simply configure the PHPSandbox instance to whitelist the rand() function, and it will now be available for use in user-submitted code.

$sandbox->whitelist_func('rand');

The following code will now work without throwing an exception:

echo 'Your random value is: ' . rand();

And will respond like so:

Your random value is: 14448

Overwriting PHP With PHPSandbox

Previously, we used the mail() function as an example of a dangerous function that hackers may attempt to induce the server to execute, in order to spread spam and other nefarious emails. But what if, rather than throwing an exception when these attempts are made, you want the server to quietly log these attempts at misuse and yet still not send any emails? PHPSandbox allows for the developer to overwrite internal PHP functions, classes, and more within the sandboxed environment, so then the sandbox can intercept overwritten function calls and instead pass them to developer-defined functions.

Let's define a replacement function for the PHP internal mail() function:

$sandbox->define_func('mail', function(){
    file_put_contents("bad_calls_log.txt",
    "Mail function called with following arguments: " .
    print_r(func_get_args(), true) . "\r\n");
});

Now, rather than the sandbox instance throwing an exception when the mail() function is called, it will instead write a log entry detailing the attempted use of the function and otherwise ignore the call. To the attempted hacker, the function call appears to have succeeded, leaving them no inkling that their actions have instead been intercepted and observed.

Summary

PHPSandbox supports a vast array of configuration options, which can be explored further via the PHPSandbox manual. The library also includes a toolkit that can be used to experiment with different configuration settings. (NOTE: Do NOT use the toolkit on a publicly accessible server! It is for local testing only!)

My motivation for building this library is that the evil that is eval() may be minimized by allowing developers to start with barebones access to PHP's incredible power, rather than attempting to constrain the nearly unlimited control eval() provides.

Hope it helps!

Comments

PunKeel wrote 1 year ago

Does it also clean up \mail(...) ? :)

Elijah Horton wrote 1 year ago

@PunKeel. Yes, it would catch even namespaced calls, as they have to be specifically whitelisted in order to be allowed. With PHPSandbox, you don't worry about blocking dangerous functions, you simply whitelist the ones you want the user to be able to use.

abide wrote 1 year ago

Nice job, man.

VojToshik

VojToshik wrote 1 year ago

What about such code?

$func = 'mail';

$func();

Will it filter that?

Elijah Horton wrote 1 year ago

@VojToshik

Yes. PHPSandbox parses variable variables to get their runtime value, then checks that against the function whitelist (if it's calling for a function, for example.)

If the called function isn't in the whitelist it will throw an exception.