Подтвердить что ты не робот

Требовать произвольный файл PHP без утечки переменных в область

Возможно ли в PHP require произвольный файл без утечки каких-либо переменных из текущей области в требуемое пространство имен переменных файлов или загрязнение области глобальных переменных?

Я хочу делать легкие шаблоны с PHP файлами и задавался вопросом, насколько это возможно, если бы можно было загрузить файл шаблона без каких-либо переменных в его области, кроме предполагаемых.

У меня есть тест, который я бы хотел решить. Должно потребоваться RequiredFile.php и вернуть его 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()));

?>

Самое близкое, что я получил, - это использовать закрытие, но оно все равно возвращает 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 used in the require call
    require( $_file );
};
$scope( "RequiredFile.php", array() );

Любые идеи?

4b9b3361

Ответ 1

Посмотрите на это:

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

Ответ 2

Мне удалось найти решение с помощью eval, чтобы вставить переменную в качестве константы, тем самым предотвращая ее утечку.

При использовании eval определенно не идеальное решение, оно создает "совершенно чистую" область для требуемого файла, что-то, что PHP, похоже, не может сделать изначально.

$scope = function( $file, array $scope_array ) {
    extract( $scope_array ); unset( $scope_array );
    eval( "unset( \$file ); require( '".str_replace( "'", "\\'", $file )."' );" );
};
$scope( "test.php", array() );

EDIT:

Это технически даже не идеальное решение, поскольку оно создает "тень" над переменными file и scope_array, не позволяя им естественным образом переноситься в область.

EDIT2:

Я мог бы сопротивляться попытке написать теневое решение. Выполненный код не должен иметь доступ к $this, глобальным или локальным переменным из предыдущих областей, если только не передан непосредственно.

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

Ответ 3

После некоторых исследований, вот что я придумал. Единственным (чистым) решением является использование функций-членов и переменных экземпляра/класса.

Вам необходимо:

  • Сопоставьте все, используя $this, а не аргументы функции.
  • Удалите все глобальные переменные, суперглобалы и восстановите их впоследствии.
  • Используйте возможное состояние гонки. Т.е.: В моем примере ниже render() будет устанавливать переменные экземпляра, которые будет использоваться после _render(). В многопоточной системе это создает условие гонки: поток A может вызывать render() одновременно с потоком B, и данные будут неточными для одного из них. К счастью, на данный момент PHP не многопоточен.
  • Используйте временный файл для включения, содержащий замыкание, чтобы избежать использования eval.

В классе шаблонов я придумал:

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

И вот тестовый пример:

// 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 

И файл шаблона:

<?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

?>

Короче говоря, это решение:

  • Скрывает все глобалы и суперглобалы. Сами переменные ($ _GET, $_POST и т.д.) все еще могут быть изменены, но они вернутся к тому, что было ранее.
  • Не теневые переменные. (почти) все можно использовать, включая $this. (За исключением $GLOBALS, см. Ниже).
  • Не вносит ничего в область, которая не была передана.
  • Не теряет никаких данных и не запускает деструкторы, потому что refcount никогда не достигает нуля для любой переменной.
  • Не использует eval или что-то в этом роде.

Вот результат, который я привел выше:

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

Если вы сбросите $GLOBALS после того, как он будет таким же, как и до вызова.

Единственная возможная проблема заключается в том, что кто-то еще может выполнить что-то вроде:

unset($GLOBALS);

... и вы ввернуты. И нет никакого способа обойти это.

Ответ 4

Если вам нужен очень простой механизм шаблонов, ваш подход с функцией достаточно хорош. Скажите, каковы реальные недостатки раскрытия переменной $_file?

Если вам нужно сделать настоящую работу, возьмите Twig и перестаньте беспокоиться. Любой правильный механизм шаблонов компилирует ваши шаблоны в чистый PHP в любом случае, поэтому вы не теряете скорость. Вы также получаете значительные преимущества - более простой синтаксис, принудительный htmlspecialchars и другие.

Вы всегда можете скрыть свой $_file в суперглобале:
$_SERVER['MY_COMPLEX_NAME'] = $_file;
unset($_file);
include($_SERVER['MY_COMPLEX_NAME']);
unset($_SERVER['MY_COMPLEX_NAME']);суб >