Kendall Hopkins
Kendall Hopkins

Reputation: 44124

Require an arbitrary PHP file without leaking variables into scope

Is it possible in PHP to require an arbitrary file without leaking any variables from the current scope into the required file's variable namespace or polluting the global variable scope?

I'm wanting to do lightweight templating with PHP files and was wondering for purity sake if it was possible to load a template file without any variables in it's scope but the intended ones.

I have setup a test that I would like a solution to pass. It should beable to require RequiredFile.php and have it return Success, no leaking variables..

RequiredFile.php:

<?php

print array() === get_defined_vars()
    ? "Success, no leaking variables."
    : "Failed, leaked variables: ".implode(", ",array_keys(get_defined_vars()));

?>

The closest I've gotten was using a closure, but it still returns Failed, leaked variables: _file.

$scope = function( $_file, array $scope_variables ) {
    extract( $scope_variables ); unset( $scope_variables );
    //No way to prevent $_file from leaking since it's used in the require call
    require( $_file );
};
$scope( "RequiredFile.php", array() );

Any ideas?

Upvotes: 29

Views: 3231

Answers (4)

lisachenko
lisachenko

Reputation: 6092

Look at this:

$scope = function() {
    // It's very simple :)
    extract(func_get_arg(1));
    require func_get_arg(0);
};
$scope("RequiredFile.php", []);

Upvotes: 24

Denis
Denis

Reputation: 5271

If you need a very simple templating engine, your approach with a function is good enough. Tell me, what are the real disadvantages of exposing that $_file variable?

If you need to do real work, grab Twig and stop worrying. Any proper templating engine compiles your templates into pure PHP anyway, so you don't lose speed. You also gain significant advantages - simpler syntax, enforced htmlspecialchars and other.

You could always hide your $_file in a superglobal:
$_SERVER['MY_COMPLEX_NAME'] = $_file;
unset($_file);
include($_SERVER['MY_COMPLEX_NAME']);
unset($_SERVER['MY_COMPLEX_NAME']);

Upvotes: 0

netcoder
netcoder

Reputation: 67745

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 set
var_dump($_GET['get'], $hello);

// Those shouldn't be set anymore
var_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 later

var_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 75

Call Stack:
    0.0003     658056   1. {main}() /var/www/temp/test.php:0

Notice: Undefined index: stack in /var/www/temp/test.php on line 75

Call Stack:
    0.0003     658056   1. {main}() /var/www/temp/test.php:0

NULL
NULL

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.

Upvotes: 0

Kendall Hopkins
Kendall Hopkins

Reputation: 44124

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() );

Upvotes: 0

Related Questions