Skip to content

Instantly share code, notes, and snippets.

@nikic
Last active September 25, 2015 06:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nikic/878218 to your computer and use it in GitHub Desktop.
Save nikic/878218 to your computer and use it in GitHub Desktop.
PHP Sandbox

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.

<?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;
}
}
<?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