检测寄存器全局变量的使用

时间:2012-08-07 14:52:49

标签: php code-analysis

PHP中有没有办法进行静态代码分析并检测对register_globals计划的依赖?手动检查文件并查找尚未初始化的变量并从中推断可能依赖于它的变量相对简单,但我需要为数百个脚本执行此操作,因此我正在寻找自动化解决方案

我的最后一招是设置一个dev环境,关闭指令并严格报告错误并让QA玩了很长时间,然后修复错误日志捕获的实例,但不能保证找到100%这些案例,如果存在自动化解决方案,当然也不能很好地利用资源。

3 个答案:

答案 0 :(得分:13)

我刚刚攻击一个小脚本来检测简单的未定义变量。您需要PHP-Parser

<?php

error_reporting(E_ALL);

$dir = './foo';

require_once './lib/bootstrap.php';

class Scope {
    protected $stack;
    protected $pos;

    public function __construct() {
        $this->stack = array();
        $this->pos = -1;
    }

    public function addVar($name) {
        $this->stack[$this->pos][$name] = true;
    }

    public function hasVar($name) {
        return isset($this->stack[$this->pos][$name]);
    }

    public function pushScope() {
        $this->stack[++$this->pos] = array();
    }

    public function popScope() {
        --$this->pos;
    }
}

class UndefinedVariableVisitor extends PHPParser_NodeVisitorAbstract {
    protected $scope;
    protected $parser;
    protected $traverser;

    public function __construct(Scope $scope, PHPParser_Parser $parser, PHPParser_NodeTraverser $traverser) {
        $this->scope = $scope;
        $this->parser = $parser;
        $this->traverser = $traverser;
    }

    public function enterNode(PHPParser_Node $node) {
        if (($node instanceof PHPParser_Node_Expr_Assign || $node instanceof PHPParser_Node_Expr_AssignRef)
            && $node->var instanceof PHPParser_Node_Expr_Variable
            && is_string($node->var->name)
        ) {
            $this->scope->addVar($node->var->name);
        } elseif ($node instanceof PHPParser_Node_Stmt_Global || $node instanceof PHPParser_Node_Stmt_Static) {
            foreach ($node->vars as $var) {
                if (is_string($var->name)) {
                    $this->scope->addVar($var->name);
                }
            }
        } elseif ($node instanceof PHPParser_Node_Expr_Variable && is_string($node->name)) {
            if (!$this->scope->hasVar($node->name)) {
                echo 'Undefined variable $' . $node->name . ' on line ' . $node->getLine() . "\n";
            }
        } elseif ($node instanceof PHPParser_Node_Stmt_Function || $node instanceof PHPParser_Node_Stmt_ClassMethod) {
            $this->scope->pushScope();

            // params are always available
            foreach ($node->params as $param) {
                $this->scope->addVar($param->name);
            }

            // methods always have $this
            if ($node instanceof PHPParser_Node_Stmt_ClassMethod) {
                $this->scope->addVar('this');
            }
        } elseif ($node instanceof PHPParser_Node_Expr_Include && $node->expr instanceof PHPParser_Node_Scalar_String) {
            $file = $node->expr->value;
            $code = file_get_contents($file);
            $stmts = $this->parser->parse($code);

            // for includes within the file
            $cwd = getcwd();
            chdir(dirname($file));

            $this->traverser->traverse($stmts);

            chdir($cwd);
        }
    }

    public function leaveNode(PHPParser_Node $node) {
        if ($node instanceof PHPParser_Node_Stmt_Function || $node instanceof PHPParser_Node_Stmt_ClassMethod) {
            $this->scope->popScope();
        }
    }
}

$parser = new PHPParser_Parser(new PHPParser_Lexer());

$scope = new Scope;

$traverser = new PHPParser_NodeTraverser;
$traverser->addVisitor(new UndefinedVariableVisitor($scope, $parser, $traverser));

foreach (new RecursiveIteratorIterator(
             new RecursiveDirectoryIterator($dir),
             RecursiveIteratorIterator::LEAVES_ONLY)
         as $file
) {
    if (!preg_match('/\.php$/', $file)) continue;

    echo 'Checking ' . $file . ':', "\n";

    $code = file_get_contents($file);
    $stmts = $parser->parse($code);

    // for includes within the file
    $cwd = getcwd();
    chdir(dirname($file));

    $scope->pushScope();
    $traverser->traverse($stmts);
    $scope->popScope();

    chdir($cwd);

    echo "\n";
}

这只是一个非常基本的实现,我没有对它进行过广泛的测试,但是它应该适用于那些与$GLOBALS$$varVars不相符的脚本。它基本上包括解决方案。

答案 1 :(得分:0)

这应该有效(来自php手册评论之一):

if (ini_get('register_globals')) {
    foreach ($GLOBALS as $int_temp_name => $int_temp_value) {
        if (!in_array($int_temp_name, array (
                'GLOBALS',
                '_FILES',
                '_REQUEST',
                '_COOKIE',
                '_SERVER',
                '_ENV',
                '_SESSION',
                ini_get('session.name'),
                'int_temp_name',
                'int_temp_value'
            ))) {
            unset ($GLOBALS[$int_temp_name]);
        }
    }
}

答案 2 :(得分:0)

用最新的解析器调整上面的代码:用它把 5.4 register_globals 变成 7.2

    <?php
/*
 * pgParser.php is a "Abstract syntax tree" progam based on the Nikic PHP-Parser  https://github.com/nikic/PHP-Parser
 * Tweaked for use to lose register global variables.
 */

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Parser;

error_reporting(E_ALL);

/*
 * $dir contains a base dir to use when no PARAM directory eq "php ./pgParser.php /home/hrobben/php/ > results.txt"
 */
$dir = '/home/hrobben/php/';
/*
 * $nonecheck : variables not to check....
 * e.g ['GLOBALS','_COOKIE', 'properties'] -> $GLOBALS, $_COOKIE, $properties >>>>> will not been checked.
 */
$nonecheck = ['_COOKIE', '_SERVER', '_SESSION'];
/*
 * if those names part or directory not include...
 * this can be usefull when certain users (developers) have own directories
 * empty = [] no directories will be excluded
 * e.g. ['george', 'henry']  all directories /tree/git/base/src/henry/  and /template/george/blog/  will be excluded.
 */
$nonesubdir = ['henry', 'ckeditor'];
/*
 * $excludeMatch can be filled with text in directory plus file to exclude.
 * e.q.  _MA. will exclude the file index_MA.php3 to scan.
 * e.g.  _MA will exclude the file /git/src/test_MAIN/index.php3
 * be carefull.
 * Not for include files, only base files.
 */
$excludeMatch = ['_HR.', '_ed.'];
/*
 * you can give a base directory to begin scanning.
 */
if (count($argv) > 0) {
    $dir = $argv[1];
}

require_once './vendor/autoload.php';

class Scope
{
    protected $stack;
    protected $pos;

    public function __construct()
    {
        $this->stack = array();
        $this->pos = -1;
    }

    public function addVar($name)
    {
        $this->stack[$this->pos][$name] = true;
    }

    public function hasVar($name): bool
    {
        return isset($this->stack[$this->pos][$name]);
    }

    public function pushScope()
    {
        $this->stack[++$this->pos] = array();
    }

    public function popScope()
    {
        --$this->pos;
    }

    public function getPos()
    {
        return $this->pos;
    }
}

class UndefinedVariableVisitor extends NodeVisitorAbstract
{
    protected $scope;
    protected $parser;
    protected $traverser;
    protected $nonecheck;
    protected $isInclude = false;

    public function __construct(Scope $scope, Parser $parser, NodeTraverser $traverser, array $nonecheck)
    {
        $this->scope = $scope;
        $this->parser = $parser;
        $this->traverser = $traverser;
        $this->nonecheck = $nonecheck;
    }

    public function enterNode(Node $node)
    {
        $includePath = ['/home/hrobben/php/include/class/'];
        $nonesubdir = ['jp', 'jp117']; // for the include exclusion directories.

        if (($node instanceof PhpParser\Node\Expr\Assign || $node instanceof PhpParser\Node\Expr\AssignRef)
            && $node->var instanceof PhpParser\Node\Expr\Variable
            && is_string($node->var->name)
        ) {
            // if preg_replace uses the same var name as second argument, its not setting a var, it must also be on the first level.
            if ($node->expr->name->parts[0] == 'preg_replace' && $node->expr->args[2]->value->name == $node->var->name && $this->scope->getPos() == 0) {
                //echo 'found $'.$node->expr->args[2]->value->name.' level '.$this->scope->getPos()."\n";
            } else {
                //echo 'add var $' . $node->var->name . "\n";
                $this->scope->addVar($node->var->name);
            }
        } elseif ($node instanceof PhpParser\Node\Stmt\Global_ || $node instanceof PhpParser\Node\Stmt\Static_) {
            foreach ($node->vars as $var) {
                if (is_string($var->name)) {
                    $this->scope->addVar($var->name);
                    //echo 'add global or static var $' . $var->name . "\n";
                }
            }
        } elseif ($node instanceof PhpParser\Node\Stmt\Foreach_) {
            $this->scope->addVar($node->valueVar->name);
            if ($node->keyVar) {
                $this->scope->addVar($node->keyVar->name);
            }
        } elseif ($node instanceof PhpParser\Node\Stmt\StaticVar) {
            $this->scope->addVar($node->var->name);
        } elseif ($node instanceof PhpParser\Node\Expr\FuncCall && ($node->name->parts[0] == 'preg_match')) {
            foreach ($node->args as $args) {
                if ($node->args[2]) {
                    $this->scope->addVar($node->args[2]->value->name);
                }
            }
        } elseif ($node instanceof PhpParser\Node\Expr\List_) {
            foreach ($node->items as $item) {
                $this->scope->addVar($item->value->name);
            }
        } elseif ($node instanceof PhpParser\Node\Expr\Closure) {
            foreach ($node->params as $param) {
                $this->scope->addVar($param->var->name);
            }
        } elseif ($node instanceof PhpParser\Node\Expr\Variable && is_string($node->name) && !$node instanceof PhpParser\Node\Const_) {
            if (!$this->scope->hasVar($node->name) && !in_array($node->name, $this->nonecheck)) {
                echo 'Undefined variable $' . $node->name . ' on line ' . $node->getLine() . "\n";
            }
        } elseif ($node instanceof PhpParser\Node\Stmt\Function_ || $node instanceof PhpParser\Node\Stmt\ClassMethod) {
            $this->scope->pushScope();

            // params are always available
            foreach ($node->params as $param) {
                //echo 'param name : '. $param->var->name. "\n";
                $this->scope->addVar($param->var->name);
            }

            // methods always have $this
            if ($node instanceof PhpParser\Node\Stmt\ClassMethod) {
                $this->scope->addVar('this');
            }
        } elseif ($node instanceof PhpParser\Node\Expr\Include_ && $node->expr instanceof PhpParser\Node\Scalar\String_) {
            $file = $node->expr->value;

            $code = file_get_contents($file);
            if (strlen($code) < 5) {
                foreach ($includePath as $path) {
                    if (strlen($code) < 5) {
                        $code = file_get_contents($path . $file);
                    }
                }
            }

            $match_string = implode("\/|", $nonesubdir);
            if (preg_match("/$match_string\//", $file)) {
                $code = '';
                echo 'file skipped: ' . $file . "\n";
            }

            $stmts = $this->parser->parse($code);
            // for includes within the file
            $cwd = getcwd();
            chdir(dirname($file));

            $this->isInclude = true;
            $this->traverser->traverse($stmts);
            $this->isInclude = false;

            chdir($cwd);
            if (strlen($code) < 5) {
                echo 'Include <- ' . $file . ' not found or empty.' . "\n";
            } else {
                echo 'Include <- ' . $file . "\n";
            }
        }
    }

    public function leaveNode(Node $node)
    {
        if ($node instanceof PhpParser\Node\Stmt\Function_ || $node instanceof PhpParser\Node\Stmt\ClassMethod) {
            $this->scope->popScope();
        }
    }
}

$lexer = new PhpParser\Lexer();
$parser = (new PhpParser\ParserFactory)->create(
    PhpParser\ParserFactory::PREFER_PHP7,
    $lexer
);

$scope = new Scope;

$traverser = new NodeTraverser();
$traverser->addVisitor(new UndefinedVariableVisitor($scope, $parser, $traverser, $nonecheck));

foreach (new RecursiveIteratorIterator(
             new RecursiveDirectoryIterator($dir),
             RecursiveIteratorIterator::LEAVES_ONLY)
         as $file
) {
    if (!preg_match('/\.php$|\.html$|\.php3$/', $file)) continue;

    $match_string = implode("\/|\/", $nonesubdir);
    if (preg_match("/\/$match_string\//", $file)) continue;

    $match_string = implode("|", $excludeMatch);
    if (preg_match("/$match_string/", $file)) continue;

    echo 'Checking ' . $file . ':', "\n";

    $code = file_get_contents($file);
    $stmts = $parser->parse($code);
    // print_r($stmts);
    // for includes within the file
    $cwd = getcwd();
    chdir(dirname($file));

    $scope->pushScope();
    $traverser->traverse($stmts);
    //print_r($stmts);
    $scope->popScope();

    chdir($cwd);

    echo "\n";
}

使用它来制作大量脚本文件,其中包含没有全局注册变量的代码。我们去用 PHP7.2