28

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?

Kendall Hopkins
  • 43,213
  • 17
  • 66
  • 89
  • Presumably you want the `require` to only pull classes and functions into the global scope? How do you intend to deal with collisions? – Oliver Charlesworth Nov 09 '11 at 01:08
  • I don't know enough about PHP to write a proper answer, but [namespaces](http://php.net/manual/en/language.namespaces.php) may be a solution to your problem. – Oliver Charlesworth Nov 09 '11 at 01:09
  • @OliCharlesworth I don't really understand what your asking. Could you explain a bit more? – Kendall Hopkins Nov 09 '11 at 01:10
  • A PHP script contains variables, classes and functions. You've said you want to avoid polluting *variables*, which implies that you're really just after the classes and functions. Perhaps you need to to edit your question to explain what the overall goal is here. – Oliver Charlesworth Nov 09 '11 at 01:13
  • The leaked variable in your case is `$_file`. That's a local variable too. Why is this a real concern? – mario Nov 09 '11 at 01:13
  • @mario I'm wanting to do templating with PHP files (in my case I'd also have an `extract` call in the closure), and I was wanting to prevent any unwanted variables from leaking in. I'd also like to know because it seemed like an interesting question from a purity standpoint. – Kendall Hopkins Nov 09 '11 at 01:18
  • Puritans might go to hell too. A simple function scope is enough either way. And it's just that one variable, impossible to avoid, so make its name reasonably unique. – mario Nov 09 '11 at 01:22
  • Includes were **designed** to "leak" variables into the included file's scope. If you want PHP to behave differently, reassess what you are doing. – NullUserException Nov 09 '11 at 01:28
  • @Kendall, mario: Writing templates in raw PHP sounds somewhat dangerous if other users can contribute, as it allows arbitrary code execution (as well as the pollution problem). I'd suggest looking for a different mechanism. – Oliver Charlesworth Nov 09 '11 at 01:30
  • I was planning on building into the "design" if I couldn't avoid it, but I was curious if there was a way of requiring a file that I've overlooked. – Kendall Hopkins Nov 09 '11 at 01:31
  • @OliCharlesworth The templates would be written by a trusted source, and PHP was designed to be a "[templating](http://stackoverflow.com/questions/62605/php-as-a-template-language-or-some-other-php-templating-script)" language. – Kendall Hopkins Nov 09 '11 at 01:32
  • @Kendall: Indeed, but not a "meta-templating" language! That's why templating frameworks (such as Smarty) exist! – Oliver Charlesworth Nov 09 '11 at 01:35
  • 2
    @OliCharlesworth Smarty (like any other templating engine on top of PHP) is highly unnecessary overhead. On top of that, using a templating engine severely limits what you can do. They are not just worthless, but actually do more harm than good. – NullUserException Nov 11 '11 at 00:32
  • Instead of trying to scrub the current scope of all variables, could you not just call out to the file and run it on its own rather then trying to force it into the scope of your current application (via file_get_contents or something similar), while you may be able to clean out most of the other variables, i dont think you would be able to get them ALL. – Shane Fright Nov 17 '11 at 03:08

4 Answers4

23

Look at this:

$scope = function() {
    // It's very simple :)
    extract(func_get_arg(1));
    require func_get_arg(0);
};
$scope("RequiredFile.php", []);
lisachenko
  • 5,952
  • 3
  • 31
  • 51
  • This is such a phenomenal answer! In case someone wants to do this without declaring a function (e.g. `$scope` in the above code), you can use an anonymous function like so: `call_user_func(function() { extract( func_get_arg(1) ); return require func_get_arg(0); }, "RequiredFile.php", array( 'your_var_1' => 'hello', 'your_var_2' => 'world', ));` – Jordan Lev Aug 17 '15 at 16:38
0

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() );
Kendall Hopkins
  • 43,213
  • 17
  • 66
  • 89
  • As one of the [biggest opponent to `eval`](http://stackoverflow.com/questions/3499672/when-if-ever-is-eval-not-evil) I know of, I didn't think you'd come up with, or even accept a solution like this one for something like templating... Seems like you may have open your mind after all. In this solution, it's also possible to use the `global` keyword and/or a superglobal to access variables. ;-) – netcoder Nov 09 '11 at 01:53
  • 1
    @netcoder I'm not going to accept this answer as I wouldn't ever want it to be used, but I'm more interested in if there is a "correct" answer to this question, even if it mean resorting to using the "evil" `eval` :P – Kendall Hopkins Nov 09 '11 at 02:05
  • @KendallHopkins bear with my reasoning but why are you passing an empty array as the scope array? Or is the empty array a pseudonym for get_defined_vars? What's the motive behind extracting an empty array? – I Want Answers May 05 '17 at 09:03
0

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.

netcoder
  • 66,435
  • 19
  • 125
  • 142
  • Does this protect `$this->_backup = array()` breaking the restore or `$this = NULL` causing an error? – Kendall Hopkins Nov 09 '11 at 03:28
  • Also why can't you backup the `$GLOBALS` var by just storing it? – Kendall Hopkins Nov 09 '11 at 03:30
  • @Kendall Hopkins: Assigning `$this = NULL` (direct assignment) always cause a fatal error. `$this->_backup = array()` seems to break it... There might be a way around that... :P – netcoder Nov 09 '11 at 03:33
  • You could probably use a similar method to what I'm doing to use scope and closures to naturally hide the variables from the template. – Kendall Hopkins Nov 09 '11 at 03:37
  • I think this could be done without `eval` or classes. You could do something like `require( specialFunction() )`, where `function specialFunction( $input = NULL ) { static $cache = NULL; if( is_null( $input ) ) { return $cache; } $cache = $input; }`. – Kendall Hopkins Nov 09 '11 at 14:56
  • @Kendall Hopkins: I don't see how that fixes anything... o.O A solution to avoid `eval` is the use of temp files, like in my updated answer. Could be done in a single function, but I kept the class around for the `_backup()` and `_restore()` methods. – netcoder Nov 09 '11 at 15:14
0

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']);

Denis
  • 5,061
  • 1
  • 20
  • 22
  • I do actually use `Twig`, this is more of thought experiment honestly. Also storing `$_file` in a superglobal isn't ready hiding it, since it could still be accessed during the `include`. – Kendall Hopkins Dec 12 '11 at 16:29
  • I know, that's why I've said "hide". :) – Denis Dec 29 '11 at 21:13