Prototype: 0.1.0
Sourceforge doesnt provide PHP5 hosting so I couldn't host this script. So heres the code.
Current Features:
- Starts from scratch (cant access any functions/classes)
- You can allow certain functions
- You can allow certain functions
- All user-defined functions and classes can be accessed
Known issues:
- Var restrictions arent set up yet.
- This means $GLOBALS['sandbox']->allowFunction() is possible, but don't do it now.
- The other project had measures for preventing shell execution as a built-in language construct, or something like that. I'm not aware of this feature of PHP so somebody please enlighten me.
PHPSandbox.php
Code: Select all
<?php
/**
* @package PHPSandbox
* @version 0.1.0
* @author Verminox <verminox@gmail.com>
*/
error_reporting(E_ALL);
header("Content-type: text/plain");
function TestFunc(){ echo "Test Function\n"; }
class TestClass{ public static function testMethod(){ echo "Test Method\n"; } }
try {
$sandbox = new PHPSandbox();
$sandbox->allowFunction('var_dump');
$sandbox->allowFunction('TestFunc');
$sandbox->allowClass('TestClass');
$sandbox->evaluate(file_get_contents('source.php'));
}
catch (Exception $e)
{
echo $e;
}
/**
* The Sandbox Environment
*/
class PHPSandbox
{
/**
* List of global functions accessible from the sandbox
* @var array
*/
protected $allowedFunctions = array();
/**
* List of classes accessible from the sandbox
* @var array
*/
protected $allowedClasses = array();
/**
* Default constructor.
*/
public function __construct()
{
// TODO
}
/**
* Allow access to a global function from the sandbox
*
* @param string Name of the global function
*/
public function allowFunction($function)
{
if ( !in_array( $function, $this->allowedFunctions ) )
{
$this->allowedFunctions[] = $function;
}
}
/**
* Whether a given function is allowed access from the sandbox
*
* @param string Name of the global function
* @return bool
*/
public function isAllowedFunction($function)
{
return in_array( $function, $this->allowedFunctions );
}
/**
* Allow access to a class from the sandbox
*
* @param string Name of the class
*/
public function allowClass($class)
{
if ( !in_array( $class, $this->allowedClasses ) )
{
$this->allowedClasses[] = $class;
}
}
/**
* Whether a given class is allowed access from the sandbox
*
* @param string Name of the class
* @return bool
*/
public function isAllowedClass($class)
{
return in_array( $class, $this->allowedClasses );
}
/**
* Evaluate PHP Code in the sandbox.
*
* The given PHP code will be evaluated within the confines of the sandbox. It will only be
* able to access data that has been granted to the sandbox.
* @param string The PHP source code to be executed (all PHP code must occur after opening a PHP tag)
* @return mixed On success, whatever was returned in the source will be returned, or else NULL is returned.
* If a parse-error occured then FALSE is returned.
*/
public function evaluate($source)
{
// Get all tokens in the source
$tokens = token_get_all($source);
$token_manager = new PHPSandboxTokenManager($tokens);
// Go through each token and check accessibility
for($i=0;$i<$token_manager->size();$i++)
{
// Initialize current, previous and next tokens
$current = $token_manager->getTokenAt($i);
$previous = $token_manager->getTokenBefore($i);
$next = $token_manager->getTokenAfter($i);
// Handling tokens of type T_STRING
if( $current[0] == T_STRING )
{
if( $previous[0] == T_CLASS )
{
// Class Name: Definition
// Since the class is being defined within the source, it is hereby allowed
$this->allowClass($current[1]);
}
else if ( $previous[0] == T_FUNCTION )
{
// Function Name: Definition
// Since the function is being defined within the source, it is hereby allowed
$this->allowFunction($current[1]);
}
else if ( $previous[0] == T_EXTENDS )
{
// Class Name: Inheritence
if( $this->isAllowedClass($current[1]) )
{
continue;
}
else
{
throw new PHPSandboxException("Use of restricted class '$current[1]'",$current[2]);
}
}
else if ( $previous[0] == T_NEW )
{
// Class Name: New Object Initialization
if( $this->isAllowedClass($current[1]) )
{
continue;
}
else
{
throw new PHPSandboxException("Use of restricted class '$current[1]'",$current[2]);
}
}
else if ( $next[0] == T_CHARACTER && $next[1] == '(' )
{
if ( $previous[0] == T_DOUBLE_COLON )
{
// Method Name: Static Method
} else if ($previous[0] == T_OBJECT_OPERATOR )
{
// Method Name: Non-static Method
} else
{
// Function Name: Global Function
if( $this->isAllowedFunction($current[1]) )
{
continue;
}
else
{
throw new PHPSandboxException("Use of restricted function '$current[1]'",$current[2]);
}
}
}
else if ( $next[0] == T_DOUBLE_COLON )
{
// Class Name: Static Method
if( $this->isAllowedClass($current[1]) )
{
continue;
}
else
{
throw new PHPSandboxException("Use of restricted class '$current[1]'",$current[2]);
}
}
else
{
// Constant (NOTE: It could also be true or false or NULL)
// NEW NOTE: Actually, it could be anything else that we have not thought of
}
}
// Make sure variable function names or class names are not used
else if ( $current[0] == T_VARIABLE )
{
if ( $previous[0] == T_NEW )
{
// Class Name: New Object Initialization
throw new PHPSandboxException("Restricted '$current[1]' - Variable class names not allowed",$current[2]);
}
else if ( $next[0] == T_CHARACTER && $next[1] == '(' )
{
if ( $previous[0] == T_DOUBLE_COLON )
{
// Method Name: Static Method
throw new PHPSandboxException("Restricted '$current[1]' - Variable method names not allowed",$current[2]);
} else if ($previous[0] == T_OBJECT_OPERATOR )
{
// Method Name: Non-static Method
throw new PHPSandboxException("Restricted '$current[1]' - Variable method names not allowed",$current[2]);
} else
{
// Function Name: Global Function
throw new PHPSandboxException("Restricted '$current[1]' - Variable function names not allowed",$current[2]);
}
}
}
// Make sure variable variables are not allowed (i.e any standalone '$' is suspicious)
else if ( $current[0] == T_CHARACTER && $current[1] == '$' )
{
// Standalone '$'
throw new PHPSandboxException("Restricted '$'",$current[2]);
}
// Make sure eval() can never be called.
else if ( $current[0] == T_EVAL )
{
// eval() attempted. Seriously... some nerve.
throw new PHPSandboxException("Restricted call to eval()",$current[2]);
}
// Check for includes
else if ( $current[0] == T_INCLUDE ||
$current[0] == T_INCLUDE_ONCE ||
$current[0] == T_REQUIRE ||
$current[0] == T_REQUIRE_ONCE )
{
// Including other files not allowed
throw new PHPSandboxException("Attempt to include external file",$current[2]);
}
}
// Now this is scary
return eval('?>' . $source);
}
}
/**
* Manages tokens acquired from the PHP Tokenizer extension
*/
class PHPSandboxTokenManager
{
/**
* Array of tokens
* @var array
*/
public $tokens = array();
/**
* Constructs a TokenManager
*
* @param array A token array returned by token_get_all()
*/
public function __construct($tokens)
{
// Convert $tokens array into pure array of arrays
// For single characters, make it of type T_CHARACTER
// Also add line numbers
$line = 1;
foreach($tokens as $token)
{
if(!is_array($token))
{
$this->tokens[] = array( T_CHARACTER, $token, $line );
}
else
{
if($token[0]==T_DOC_COMMENT){$token[0]=T_COMMENT;} // All comments are T_COMMENTs
$this->tokens[] = array( $token[0], $token[1], $line );
$line += ( substr_count($token[1],"\n") );
}
}
}
/**
* Returns the number of tokens
*
* @return int Number of tokens
*/
public function size()
{
return count($this->tokens);
}
/**
* Returns the token at given index
*
* @param int offset of the token to retrieve
* @return array The token if exists, or a token with both elements NULL otherwise
*/
public function getTokenAt($i)
{
if( isset($this->tokens[$i]) )
{
return $this->tokens[$i];
}
else
{
return array(NULL,NULL);
}
}
/**
* Returns the next token
*
* @param int offset of the current token
* @param bool Whether to ignore whitespace
* @return array The next token if exists, or a token with both elements NULL otherwise
*/
public function getTokenAfter($i,$ignore_whitespace=true,$ignore_comments=true)
{
if( isset($this->tokens[$i+1]) )
{
if( $this->tokens[$i+1][0] == T_WHITESPACE && $ignore_whitespace==true )
{
return $this->getTokenAfter($i+1,$ignore_whitespace,$ignore_comments);
}
else if( $this->tokens[$i+1][0] == T_COMMENT && $ignore_comments==true)
{
return $this->getTokenAfter($i+1,$ignore_whitespace,$ignore_comments);
}
else
{
return $this->tokens[$i+1];
}
}
else
{
return array(NULL,NULL);
}
}
/**
* Returns the previous token
*
* @param int offset of the current token
* @param bool Whether to ignore whitespace
* @return array The previous token if exists, or a token with both elements NULL otherwise
*/
public function getTokenBefore($i,$ignore_whitespace=true,$ignore_comments=true)
{
if( isset($this->tokens[$i-1]) )
{
if( $this->tokens[$i-1][0] == T_WHITESPACE && $ignore_whitespace==true )
{
return $this->getTokenBefore($i-1,$ignore_whitespace,$ignore_comments);
}
else if( $this->tokens[$i-1][0] == T_COMMENT && $ignore_comments==true)
{
return $this->getTokenBefore($i-1,$ignore_whitespace,$ignore_comments);
}
else
{
return $this->tokens[$i-1];
}
}
else
{
return array(NULL,NULL);
}
}
}
/**
* Exception thrown by the PHPSandbox evaluator
*/
class PHPSandboxException extends Exception
{
/**
* Line number of source that is being evaluated where the exception occurred.
* @var int
*/
protected $sourceLine;
/**
* Assigns $sourceLine and calls parent constructor
*/
public function __construct($message, $line, $code=0)
{
$this->sourceLine = $line;
parent::__construct($message, $code);
}
/**
* Returns the line number of the source that is being evaluated where the exception occurred.
* @return int The line number
*/
public function getSourceLine()
{
return $this->sourceLine;
}
/**
* Converts exception to string
* @return string Exception message
*/
public function __toString()
{
return "PHPSandboxException: " . $this->message . " on line " . $this->sourceLine;
}
}
?>
source.php
Code: Select all
<?php
class MyClass //extends SuperClass
{
public $member;
static function staticMethod()
{
echo "Static Method\n";
}
function method()
{
echo "Method\n";
}
}
function MyFunc()
{
print "Global Function\n";
}
// This stuff works
TestFunc();
TestClass::testMethod();
MyFunc();
$obj = new MyClass();
$obj->member = array('Hello','World');
$obj->method();
MyClass::staticMethod();
var_dump($obj);
// This stuff shouldnt work
$var_MyFunc = 'MyFunc';
$var_MyClass = 'MyClass';
$var_method = 'method';
$var_staticMethod = 'staticMethod';
//$var_MyFunc();
//$obj = new $var_MyClass;
//$obj->$var_method( );
//MyClass::$var_staticMethod();
?>