Require an arbitrary PHP file without leaking variables into scope Require an arbitrary PHP file without leaking variables into scope php php

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.