I do not recommend to use this code as it has known vulnerabilities. You may want to have a look at some more recent sandboxing project like https://github.com/fieryprophet/php-sandbox (though I can't say anything about its quality). In general though the only really safe way to sandbox PHP is to run it in a jail.
Last active
September 25, 2015 06:28
-
-
Save nikic/878218 to your computer and use it in GitHub Desktop.
PHP Sandbox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once 'TokenStream.php'; // from https://github.com/nikic/tokenstream | |
class CodeProcessor | |
{ | |
protected $manipulators = array(); | |
protected $preparators = array(); | |
/** | |
* add a stream preparator | |
* @param callback The TokenStream is passed as only argument | |
*/ | |
public function addPreparator($callback) { | |
if (!is_callable($callback)) { | |
throw new InvalidArgumentException('Stream Preparator not callable'); | |
} | |
$this->preparators[] = $callback; | |
} | |
/** | |
* add a stream manipulator | |
* @param int|array token/array of tokens to listen for | |
* @param callback The TokenStream is passed as first argument and the index of | |
* the found token as second argument. The function may return true. | |
* In this case all matching stream manipulators are called again. | |
*/ | |
public function add($tokens, $callback) { | |
if (!is_callable($callback)) { | |
throw new InvalidArgumentException('Stream Manipulator not callable'); | |
} | |
if (!is_array($tokens)) { | |
$tokens = array($tokens); | |
} | |
foreach ($tokens as $token) { | |
$this->manipulators[$token][] = $callback; | |
} | |
} | |
/* | |
* process a source code | |
* @param string source code to process | |
* @return string processed source code | |
*/ | |
public function process($source) { | |
$tokenStream = new TokenStream($source); | |
// prepare | |
foreach ($this->preparators as $preparator) { | |
call_user_func($preparator, $tokenStream); | |
} | |
// manipulate | |
try { | |
foreach ($tokenStream as $i => $token) { | |
do { | |
$loop = false; | |
if (isset($this->manipulators[$tokenStream[$i]->type])) { | |
foreach ($this->manipulators[$tokenStream[$i]->type] as $callback) { | |
if (true === call_user_func($callback, $tokenStream, $i)) { | |
$loop = true; | |
} | |
} | |
} | |
} while ($loop); | |
} | |
} catch(TokenException $e) { | |
// add line hint ($token is the original, not yet manipulated token) | |
$e->setLine($token->line); | |
throw $e; | |
} | |
return (string) $tokenStream; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once 'CodeProcessor.php'; | |
class SandboxException extends TokenException {} | |
class Sandbox | |
{ | |
const | |
MAIL = 0, SHELL = 1, | |
FILESYSTEM = 2, FILESYSTEM_READ = 3, FILESYSTEM_WRITE = 4, FILESYSTEM_OPEN = 5; | |
protected static $blacklistTypes = array( | |
self::MAIL => 'Sending mails', | |
self::SHELL => 'Shell execution', | |
self::FILESYSTEM => 'Filesystem access', | |
self::FILESYSTEM_READ => 'Reading from the filesystem', | |
self::FILESYSTEM_WRITE => 'Writing to the filesystem', | |
self::FILESYSTEM_OPEN => 'Opening filesystem streams', | |
); | |
protected static $blacklistPrefixes = array( | |
'pcntl_' => self::SHELL, | |
'proc_' => self::SHELL, | |
'posix_' => self::SHELL, | |
'imagecreatefrom' => self::FILESYSTEM_READ, | |
); | |
protected static $blacklistFunctions = array( | |
'mail' => self::MAIL, | |
'exec' => self::SHELL, | |
'passthru' => self::SHELL, | |
'system' => self::SHELL, | |
'shell_exec' => self::SHELL, | |
'popen' => self::SHELL, | |
'pclose' => self::SHELL, | |
'apache_child_terminate' => self::SHELL, | |
'fsockopen' => self::FILESYSTEM, | |
'pfsockopen' => self::FILESYSTEM, | |
'clearstatcache' => self::FILESYSTEM, | |
'disk_free_space' => self::FILESYSTEM, | |
'disk_total_space' => self::FILESYSTEM, | |
'diskfreespace' => self::FILESYSTEM, | |
'realpath_cache_get' => self::FILESYSTEM, | |
'realpath_cache_size' => self::FILESYSTEM, | |
'umask' => self::FILESYSTEM, | |
'fopen' => self::FILESYSTEM_OPEN, | |
'gzopen' => self::FILESYSTEM_OPEN, | |
'bzopen' => self::FILESYSTEM_OPEN, | |
'chgrp' => self::FILESYSTEM_WRITE, | |
'chmod' => self::FILESYSTEM_WRITE, | |
'chown' => self::FILESYSTEM_WRITE, | |
'copy' => self::FILESYSTEM_WRITE, | |
'file_put_contents' => self::FILESYSTEM_WRITE, | |
'lchgrp' => self::FILESYSTEM_WRITE, | |
'lchown' => self::FILESYSTEM_WRITE, | |
'link' => self::FILESYSTEM_WRITE, | |
'mkdir' => self::FILESYSTEM_WRITE, | |
'move_uploaded_file' => self::FILESYSTEM_WRITE, | |
'rename' => self::FILESYSTEM_WRITE, | |
'rmdir' => self::FILESYSTEM_WRITE, | |
'symlink' => self::FILESYSTEM_WRITE, | |
'tempnam' => self::FILESYSTEM_WRITE, | |
'tmpfile' => self::FILESYSTEM_WRITE, | |
'touch' => self::FILESYSTEM_WRITE, | |
'unlink' => self::FILESYSTEM_WRITE, | |
'ftp_get' => self::FILESYSTEM_WRITE, | |
'ftp_nb_get' => self::FILESYSTEM_WRITE, | |
'iptcembed' => self::FILESYSTEM_WRITE, | |
'file_exists' => self::FILESYSTEM_READ, | |
'file_get_contents' => self::FILESYSTEM_READ, | |
'file' => self::FILESYSTEM_READ, | |
'fileatime' => self::FILESYSTEM_READ, | |
'filectime' => self::FILESYSTEM_READ, | |
'filegroup' => self::FILESYSTEM_READ, | |
'fileinode' => self::FILESYSTEM_READ, | |
'filemtime' => self::FILESYSTEM_READ, | |
'fileowner' => self::FILESYSTEM_READ, | |
'fileperms' => self::FILESYSTEM_READ, | |
'filesize' => self::FILESYSTEM_READ, | |
'filetype' => self::FILESYSTEM_READ, | |
'glob' => self::FILESYSTEM_READ, | |
'is_dir' => self::FILESYSTEM_READ, | |
'is_executable' => self::FILESYSTEM_READ, | |
'is_file' => self::FILESYSTEM_READ, | |
'is_link' => self::FILESYSTEM_READ, | |
'is_readable' => self::FILESYSTEM_READ, | |
'is_uploaded_file' => self::FILESYSTEM_READ, | |
'is_writable' => self::FILESYSTEM_READ, | |
'is_writeable' => self::FILESYSTEM_READ, | |
'linkinfo' => self::FILESYSTEM_READ, | |
'lstat' => self::FILESYSTEM_READ, | |
'parse_ini_file' => self::FILESYSTEM_READ, | |
'pathinfo' => self::FILESYSTEM_READ, | |
'readfile' => self::FILESYSTEM_READ, | |
'readlink' => self::FILESYSTEM_READ, | |
'realpath' => self::FILESYSTEM_READ, | |
'stat' => self::FILESYSTEM_READ, | |
'gzfile' => self::FILESYSTEM_READ, | |
'readgzfile' => self::FILESYSTEM_READ, | |
'ftp_put' => self::FILESYSTEM_READ, | |
'ftp_nb_put' => self::FILESYSTEM_READ, | |
'exif_read_data' => self::FILESYSTEM_READ, | |
'read_exif_data' => self::FILESYSTEM_READ, | |
'exif_thumbnail' => self::FILESYSTEM_READ, | |
'exif_imagetype' => self::FILESYSTEM_READ, | |
'hash_file' => self::FILESYSTEM_READ, | |
'hash_hmac_file' => self::FILESYSTEM_READ, | |
'hash_update_file' => self::FILESYSTEM_READ, | |
'md5_file' => self::FILESYSTEM_READ, | |
'sha1_file' => self::FILESYSTEM_READ, | |
'getimagesize' => self::FILESYSTEM_READ, | |
//'highlight_file' => self::FILESYSTEM_READ, | |
//'show_source' => self::FILESYSTEM_READ, | |
//'php_strip_whitespace' => self::FILESYSTEM_READ, | |
'get_meta_tags' => self::FILESYSTEM_READ, | |
); | |
protected static $blacklistFunctionParams = array( | |
'imagepng' => 1, | |
'imagewbmp' => 1, | |
'image2wbmp' => 1, | |
'imagejpeg' => 1, | |
'imagexbm' => 1, | |
'imagegif' => 1, | |
'imagegd' => 1, | |
'imagegd2' => 1, | |
); | |
protected static $evaluators = array( | |
'assert' => 0, | |
'create_function' => 1, | |
); | |
protected static $callers = array( | |
'ob_start' => 0, | |
'array_diff_uassoc' => -1, | |
'array_diff_ukey' => -1, | |
'array_filter' => 1, | |
'array_intersect_uassoc' => -1, | |
'array_intersect_ukey' => -1, | |
'array_map' => 0, | |
'array_reduce' => 1, | |
'array_udiff_assoc' => -1, | |
'array_udiff_uassoc' => array(-1, -2), | |
'array_udiff' => -1, | |
'array_uintersect_assoc' => -1, | |
'array_uintersect_uassoc' => array(-1, -2), | |
'array_uintersect' => -1, | |
'array_walk_recursive' => 1, | |
'array_walk' => 1, | |
'uasort' => 1, | |
'uksort' => 1, | |
'usort' => 1, | |
'assert_options' => 1, | |
'preg_replace_callback' => 1, | |
'spl_autoload_register' => 0, | |
'iterator_apply' => 1, | |
'call_user_func' => 0, | |
'call_user_func_array' => 0, | |
'register_shutdown_function' => 0, | |
'register_tick_function' => 0, | |
'set_error_handler' => 0, | |
'set_exception_handler' => 0, | |
'session_set_save_handler' => array(0, 1, 2, 3, 4, 5), | |
); | |
public static function preprocess($source) { | |
$codeProcessor = new CodeProcessor(); | |
$codeProcessor->add( | |
T_BACKTICK, | |
array(__CLASS__, 'processBacktick') | |
); | |
$codeProcessor->add( | |
array(T_INCLUDE, T_INCLUDE_ONCE, T_REQUIRE, T_REQUIRE_ONCE), | |
array(__CLASS__, 'processInclude') | |
); | |
$codeProcessor->add( | |
T_EVAL, | |
array(__CLASS__, 'processEval') | |
); | |
$codeProcessor->add( | |
T_STRING, | |
array(__CLASS__, 'processLabel') | |
); | |
$codeProcessor->add( | |
array(T_DOLLAR, T_VARIABLE, T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES), | |
array(__CLASS__, 'processVariable') | |
); | |
return $codeProcessor->process($source); | |
} | |
public static function preprocessString($code) { | |
return '?>' . self::preprocess('<?php ' . $code); | |
} | |
public static function checkFunction($funcName) { | |
$funcName = strtolower($funcName); | |
if ($funcName == 'preg_replace') | |
throw new SandboxException('preg_replace is currently deactivated!'); | |
$blacklisted = false; | |
if (isset(self::$blacklistFunctions[$funcName])) { | |
$blacklisted = true; | |
$type = self::$blacklistFunctions[$funcName]; | |
} else { | |
foreach (self::$blacklistPrefixes as $prefix => $type) { | |
if (strpos($funcName, $prefix) === 0) { | |
$blacklisted = true; | |
break; | |
} | |
} | |
} | |
if ($blacklisted) | |
throw new SandboxException(self::$blacklistTypes[$type] . ' is not allowed'); | |
} | |
public static function checkVarFunction($funcName) { | |
self::checkFunction($funcName); | |
if (isset(self::$evaluators[$funcName]) || isset(self::$callers[$funcName]) || isset(self::$blacklistFunctionParams[$funcName])) | |
throw new SandboxException('Dynamic functions must not call potentially dangerous functions'); | |
return $funcName; | |
} | |
public static function checkReflector($reflector) { | |
if (!$reflector instanceof ReflectionFunction) | |
return $reflector; | |
self::checkVarFunction($reflector->name); | |
return $reflector; | |
} | |
public static function processBacktick($tokens, $i) { | |
throw new SandboxException('Shell execution is not allowed'); | |
} | |
public static function processInclude($tokens, $i) { | |
throw new SandboxException('Inclusion is not allowed'); | |
} | |
public static function processEval(TokenStream $tokens, $i) { | |
$iBracketOpen = $tokens->skipWhitespace($i); | |
if (!$tokens[$iBracketOpen]->is(T_OPEN_ROUND)) | |
throw new TokenException('T_EVAL must be followed by \'(\''); | |
$tokens->insert($tokens->complementaryBracket($iBracketOpen), ')'); | |
$tokens->insert($iBracketOpen + 1, array( | |
new Token( | |
T_STRING, | |
__CLASS__ | |
), | |
new Token( | |
T_PAAMAYIM_NEKUDOTAYIM, | |
'::' | |
), | |
new Token( | |
T_STRING, | |
'preprocessString' | |
), | |
'(', | |
)); | |
} | |
public static function processLabel(TokenStream $tokens, $i) { | |
if ($tokens[$tokens->skipWhitespace($i, true)]->is(T_PAAMAYIM_NEKUDOTAYIM, T_OBJECT_OPERATOR) | |
|| !$tokens[$tokens->skipWhitespace($i)]->is(T_OPEN_ROUND) | |
) { | |
return; | |
} | |
self::checkFunction($tokens[$i]->content); | |
if (isset(self::$evaluators[$tokens[$i]->content])) { | |
$type = 'eval'; | |
$arguments = self::$evaluators[$tokens[$i]->content]; | |
} elseif (isset(self::$callers[$tokens[$i]->content])) { | |
$type = 'call'; | |
$arguments = self::$callers[$tokens[$i]->content]; | |
} elseif (isset(self::$blacklistFunctionParams[$tokens[$i]->content])) { | |
$type = 'blacklist'; | |
$arguments = self::$blacklistFunctionParams[$tokens[$i]->content]; | |
} else { | |
return; | |
} | |
$arguments = is_array($arguments) ? $arguments : array($arguments); | |
$i = $tokens->skipWhitespace($i); | |
if ($arguments[0] >= 0) { | |
$arg = 0; | |
$argBegin = $i; | |
$iEnd = $tokens->complementaryBracket($i); | |
for (++$i; $i <= $iEnd; ++$i) { | |
if ($tokens[$i]->is(T_OPEN_ROUND, T_OPEN_SQUARE, T_OPEN_CURLY, T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES)) { | |
$i = $tokens->complementaryBracket($i); | |
} elseif ($tokens[$i]->is(T_COMMA, T_CLOSE_ROUND)) { | |
if (in_array($arg, $arguments)) { | |
if ($type == 'blacklist') { | |
throw new SandboxException('Function uses blacklisted parameter'); | |
} | |
$tokens->insert($i, ')'); | |
$tokens->insert($argBegin + 1, array( | |
new Token( | |
T_STRING, | |
__CLASS__ | |
), | |
new Token( | |
T_PAAMAYIM_NEKUDOTAYIM, | |
'::' | |
), | |
new Token( | |
T_STRING, | |
$type == 'call' ? 'checkVarFunction' : 'processString' | |
), | |
'(', | |
)); | |
$i += 5; | |
$iEnd += 5; | |
} | |
++$arg; | |
$argBegin = $i; | |
} | |
} | |
} else { | |
$iBegin = $i; | |
$i = $tokens->complementaryBracket($i); | |
$arg = -1; | |
$argEnd = $i; | |
for (--$i; $i >= $iBegin; --$i) { | |
if ($tokens[$i]->is(T_CLOSE_ROUND, T_CLOSE_SQUARE, T_CLOSE_CURLY)) { | |
$i = $tokens->complementaryBracket($i); | |
} elseif ($tokens[$i]->is(T_COMMA, T_OPEN_ROUND)) { | |
if (in_array($arg, $arguments)) { | |
$tokens->insert($argEnd, ')'); | |
$tokens->insert($i + 1, array( | |
new Token( | |
T_STRING, | |
__CLASS__ | |
), | |
new Token( | |
T_PAAMAYIM_NEKUDOTAYIM, | |
'::' | |
), | |
new Token( | |
T_STRING, | |
$type == 'call' ? 'checkVarFunction' : 'processString' | |
), | |
'(', | |
)); | |
} | |
--$arg; | |
$argEnd = $i; | |
} | |
} | |
} | |
} | |
public static function processVariable(TokenStream $tokens, $i) { | |
if ($tokens[$tokens->skipWhitespace($i, true)]->is(T_DOLLAR, T_PAAMAYIM_NEKUDOTAYIM, T_OBJECT_OPERATOR)) { | |
return; | |
} | |
$iStart = $i; | |
do { | |
if ($tokens[$i]->is(T_DOLLAR)) { | |
$i = $tokens->skip($i, T_DOLLAR); | |
} | |
if (!$tokens[$i]->is(T_VARIABLE, T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES, T_OPEN_CURLY)) { | |
throw new TokenException('Excepted non-multiple-dollar variable'); | |
} | |
if ($tokens[$i]->is(T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES, T_OPEN_CURLY)) { | |
$i = $tokens->complementaryBracket($i); | |
} | |
if (!$tokens[$i = $tokens->skipWhitespace($i)]->is(T_OPEN_SQUARE, T_OPEN_CURLY, T_OBJECT_OPERATOR, T_PAAMAYIM_NEKUDOTAYIM)) { | |
break; | |
} | |
while ($tokens[$i]->is(T_OPEN_SQUARE, T_OPEN_CURLY, T_OBJECT_OPERATOR, T_PAAMAYIM_NEKUDOTAYIM)) { | |
if ($tokens[$i]->is(T_OPEN_SQUARE, T_OPEN_CURLY)) { | |
$i = $tokens->complementaryBracket($i); | |
} elseif($tokens[$i]->is(T_OBJECT_OPERATOR, T_PAAMAYIM_NEKUDOTAYIM)) { | |
$i = $tokens->skipWhitespace($i); | |
if ($tokens[$i]->is(T_DOLLAR, T_VARIABLE, T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES)) { | |
break; | |
} elseif (!$tokens[$i]->is(T_STRING)) { | |
throw new TokenException('Expected either variable or T_STRING'); | |
} | |
} | |
$i = $tokens->skipWhitespace($i); | |
} | |
} while ($tokens[$i]->is(T_DOLLAR, T_VARIABLE, T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES)); | |
$iEnd = $tokens->skipWhitespace($i, true); | |
if (!$tokens[$i]->is(T_OPEN_ROUND)) { | |
return; | |
} | |
if ($tokens[$iEnd]->is(T_STRING)) { | |
if (($tokens[$iEnd]->content != 'invoke' && $tokens[$iEnd]->content != 'invokeArgs') | |
|| !$tokens[$iEnd = $tokens->skipWhitespace($iEnd, true)]->is(T_OBJECT_OPERATOR) | |
) { | |
return; | |
} | |
$iEnd = $tokens->skipWhitespace($iEnd, true); | |
$funcName = 'checkReflector'; | |
} else { | |
$funcName = 'checkVarFunction'; | |
} | |
$varName = uniqid('___'); | |
$tokens->insert($iEnd + 1, array( | |
')', | |
'}', | |
)); | |
$tokens->insert($iStart, array( | |
'$', | |
'{', | |
new Token( | |
T_CONSTANT_ENCAPSED_STRING, | |
"'" . $varName . "'" | |
), | |
'.', | |
'!', | |
new Token( | |
T_VARIABLE, | |
'$' . $varName | |
), | |
'=', | |
new Token( | |
T_STRING, | |
__CLASS__ | |
), | |
new Token( | |
T_PAAMAYIM_NEKUDOTAYIM, | |
'::' | |
), | |
new Token( | |
T_STRING, | |
$funcName | |
), | |
'(', | |
)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment