Require an arbitrary PHP file without leaking variables into scope
Look at this:
$scope = function() { // It's very simple :) extract(func_get_arg(1)); require func_get_arg(0);};$scope("RequiredFile.php", []);
I've been able to come up with a solution using eval
to inline the variable as a constant, thus preventing it from leaking.
While using eval
is definitely not a perfect solution, it does create a "perfectly clean" scope for the required file, something that PHP doesn't seem to be able to do natively.
$scope = function( $file, array $scope_array ) { extract( $scope_array ); unset( $scope_array ); eval( "unset( \$file ); require( '".str_replace( "'", "\\'", $file )."' );" );};$scope( "test.php", array() );
EDIT:
This technically isn't even a perfect solution as it creates a "shadow" over the file
and scope_array
variables, preventing them from being passed into the scope naturally.
EDIT2:
I could resist trying to write a shadow free solution. The executed code should have no access to $this
, global or local variables from previous scopes, unless directly passed in.
$scope = function( $file, array $scope_array ) { $clear_globals = function( Closure $closure ) { $old_globals = $GLOBALS; $GLOBALS = array(); $closure(); $GLOBALS = $old_globals; }; $clear_globals( function() use ( $file, $scope_array ) { //remove the only variable that will leak from the scope $eval_code = "unset( \$eval_code );"; //we must sort the var name array so that assignments happens in order //that forces $var = $_var before $_var = $__var; $scope_key_array = array_keys( $scope_array ); rsort( $scope_key_array ); //build variable scope reassignment foreach( $scope_key_array as $var_name ) { $var_name = str_replace( "'", "\\'", $var_name ); $eval_code .= "\${'$var_name'} = \${'_{$var_name}'};"; $eval_code .= "unset( \${'_{$var_name}'} );"; } unset( $var_name ); //extract scope into _* variable namespace extract( $scope_array, EXTR_PREFIX_ALL, "" ); unset( $scope_array ); //add file require with inlined filename $eval_code .= "require( '".str_replace( "'", "\\'", $file )."' );"; unset( $file ); eval( $eval_code ); } );};$scope( "test.php", array() );
After some research, here is what I came up with. The only (clean) solution is to use member functions and instance/class variables.
You need to:
- Reference everything using
$this
and not function arguments. - Unset all globals, superglobals and restore them afterwards.
- Use a possible race condition of some sorts. i.e.: In my example below,
render()
will set instance variables that_render()
will use afterwards. In a multi-threaded system, this creates a race condition: thread A may call render() at the same time as thread B and the data will be inexact for one of them. Fortunately, for now, PHP isn't multi-threaded. - Use a temporary file to include, containing a closure, to avoid the use of
eval
.
The template class I came up with:
class template { // Store the template data protected $_data = array(); // Store the template filename protected $_file, $_tmpfile; // Store the backed up $GLOBALS and superglobals protected $_backup; // Render a template $file with some $data public function render($file, $data) { $this->_file = $file; $this->_data = $data; $this->_render(); } // Restore the unset superglobals protected function _restore() { // Unset all variables to make sure the template don't inject anything foreach ($GLOBALS as $var => $value) { // Unset $GLOBALS and you're screwed if ($var === 'GLOBALS') continue; unset($GLOBALS[$var]); } // Restore all variables foreach ($this->_backup as $var => $value) { // Set back all global variables $GLOBALS[$var] = $value; } } // Backup the global variables and superglobals protected function _backup() { foreach ($GLOBALS as $var => $value) { // Unset $GLOBALS and you're screwed if ($var === 'GLOBALS') continue; $this->_backup[$var] = $value; unset($GLOBALS[$var]); } } // Render the template protected function _render() { $this->_backup(); $this->_tmpfile = tempnam(sys_get_temp_dir(), __CLASS__); $code = '<?php $render = function() {'. 'extract('.var_export($this->_data, true).');'. 'require "'.$this->_file.'";'. '}; $render();' file_put_contents($this->_tmpfile, $code); include $this->_tmpfile; $this->_restore(); }}
And here's the test case:
// Setting some global/superglobals$_GET['get'] = 'get is still set';$hello = 'hello is still set';$t = new template;$t->render('template.php', array('foo'=>'bar', 'this'=>'hello world'));// Checking if those globals/superglobals are still setvar_dump($_GET['get'], $hello);// Those shouldn't be set anymorevar_dump($_SERVER['bar'], $GLOBALS['stack']); // undefined indices
And the template file:
<?php var_dump($GLOBALS); // prints an empty list$_SERVER['bar'] = 'baz'; // will be unset later$GLOBALS['stack'] = 'overflow'; // will be unset latervar_dump(get_defined_vars()); // foo, this?>
In short, this solution:
- Hides all globals and superglobals. The variables themselves ($_GET, $_POST, etc.) can still be modified, but they will revert back to what they were previously.
- Does not shadow variables. (Almost) everything can be used, including
$this
. (Except for$GLOBALS
, see below). - Does not bring anything into scope that wasn't passed.
- Does not lose any data nor trigger destructors, because the refcount never reaches zero for any variable.
- Does not use
eval
or anything like that.
Here's the result I have for the above:
array(1) { ["GLOBALS"]=> *RECURSION*}array(2) { ["this"]=> string(11) "hello world" ["foo"]=> string(3) "bar"}string(10) "get is still set"string(12) "hello is still set"Notice: Undefined index: bar in /var/www/temp/test.php on line 75Call Stack: 0.0003 658056 1. {main}() /var/www/temp/test.php:0Notice: Undefined index: stack in /var/www/temp/test.php on line 75Call Stack: 0.0003 658056 1. {main}() /var/www/temp/test.php:0NULLNULL
If you dump $GLOBALS
after the fact it should be just like it was before the call.
The only possible issue is that someone still can execute something like:
unset($GLOBALS);
... and you're screwed. And there is no way around that.