Detecting use of register globals

Viewed 1794

Is there any way in PHP to do static code analysis and detect reliance on the register_globals initiative? It's relatively straightforward to manually examine a file and look for variables which have not been initialized and infer from that that these may be relying on it, but I need to do this for many hundreds of scripts, so I'm looking for an automated solution.

My last resort is setting up a dev environment with the directive turned off and strict error reporting and letting QA play around for a long while, then fix the instances that the error log catches, but this is not guaranteed to find 100% of the cases, and certainly not a good use of resources if an automated solution exists.

3 Answers

Tweaked the code above with the latest parser: used it to make 5.4 register_globals to 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";
}

Used this to make a big many script files holding code to go without global registered variables. We gone use it with PHP7.2

Related