I think Runkit_Sandbox is the tool for this job - but I do not. assuming you work on a Unix system, can I suggest setting up a chroot jail instead?
mkdir /jail /jail/bin /jail/lib /jail/lib64 /jail/usr /jail/etc /jail/etc/alternatives chmod -R 0711 /jail chown -R root:root /jail mount -o bind,ro /bin /jail/bin mount -o bind,ro /lib /jail/lib mount -o bind,ro /lib64 /jail/lib64 mount -o bind,ro /usr /jail/usr mount -o bind,ro /etc/alternatives /jail/etc/alternatives
(others to consider include / dev / sys / proc)
and I assume that your code was originally received & processed by an unprivileged user, let me call it www-data , if so, you can use sudo to allow www data to run a specific command with sudo, for this add
www-data ALL = (root) NOPASSWD: /usr/bin/php /jail/jailexecutor.php
in / etc / sudoers
this will allow www data to run a specific command sudo /usr/bin/php /jail/jailexecutor.php
now for jailexecutor.php, it takes the source code from STDIN, executes it with php bound to / jail as the nobody user, and outputs the STDOUT and STDERR generated by the code back and terminates it if it takes longer than 5 seconds,
<?php declare(strict_types = 1); const MAX_RUNTIME_SECONDS = 5; if (posix_geteuid () !== 0) { fprintf ( STDERR, "this script must run as root (only root can chroot)" ); die (); } $code = stream_get_contents ( STDIN ); if (! is_string ( $code )) { throw new \RuntimeException ( 'failed to read the code from stdin! (stream_get_contents failed)' ); } $file = tempnam ( __DIR__, "unsafe" ); if (! is_string ( $file )) { throw new \RuntimeException ( 'tempnam failed!' ); } register_shutdown_function ( function () use (&$file) { if (! unlink ( $file )) { throw new \RuntimeException ( 'failed to clean up the file! (unlink failed!?)' ); } } ); if (strlen ( $code ) !== file_put_contents ( $file, $code )) { throw new \RuntimeException ( 'failed to write the code to disk! (out of diskspace?)' ); } if (! chmod ( $file, 0444 )) { throw new \RuntimeException ( 'failed to chmod!' ); } $starttime = microtime ( true ); $unused = [ ]; $ph = proc_open ( 'chroot --userspec=nobody /jail /usr/bin/php ' . escapeshellarg ( basename ( $file ) ), $unused, $unused ); $terminated = false; // OPTIMIZE ME: use stream_select() instead of usleep() while ( ($status = proc_get_status ( $ph )) ['running'] ) { usleep ( 100 * 1000 ); // 100 ms if (! $terminated && microtime ( true ) - $starttime > MAX_RUNTIME_SECONDS) { $terminated = true; echo 'max runtime reached (' . MAX_RUNTIME_SECONDS . ' seconds), terminating...'; pkilltree ( ( int ) ($status ['pid']) ); // proc_terminate ( $ph, SIGKILL ); } } echo "\nexit status: " . $status ['exitcode']; proc_close ( $ph ); function pkilltree(int $pid) { system ( "kill -s STOP " . $pid ); // stop it first, so it can't make any more children $children = shell_exec ( 'pgrep -P ' . $pid ); if (is_string ( $children )) { $children = trim ( $children ); } if (! empty ( $children )) { $children = array_filter ( array_map ( 'trim', explode ( "\n", $children ) ), function ($in) { return false !== filter_var ( $in, FILTER_VALIDATE_INT ); // shouldn't be necessary, but just to be safe.. } ); foreach ( $children as $child ) { pkilltree ( ( int ) $child ); } } system ( "kill -s KILL " . $pid ); }
PHP code can now be safely executed from www data as follows:
<?php declare(strict_types = 1); header ( "content-type: text/plain;charset=utf8" ); $unsafeCode = ( string ) ($_POST ['code'] ?? ''); $pipes = [ ]; $proc = proc_open ( "sudo /usr/bin/php /jail/jailexecutor.php", array ( 0 => array ( "pipe", "rb" ), 1 => array ( "pipe", "wb" ), 2 => array ( "pipe", "wb" ) ), $pipes ); fwrite ( $pipes [0], $unsafeCode ); fclose ( $pipes [0] ); while ( ($status = proc_get_status ( $proc )) ['running'] ) { usleep ( 100 * 1000 ); // 100 ms echo stream_get_contents ( $pipes [2] ); echo stream_get_contents ( $pipes [1] ); } // var_dump($status); echo stream_get_contents ( $pipes [2] ); // just to be safe, it technically possible that we dont get any cpu time between proc_open, the child finishes, and proc_get_status.. just it unlikely. fclose($pipes[2]); echo stream_get_contents ( $pipes [1] ); fclose($pipes[1]); proc_close ( $proc );
and for a quick test, curl -d code='<?php echo rand()."it works!";' url curl -d code='<?php echo rand()."it works!";' url (you can even add system("rm -rfv --no-preserve-root /"); without worrying)
- tested on Debian 9 Stretch