Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
MWPreVisitor
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 4
420
0.00% covered (danger)
0.00%
0 / 1
 visitMethod
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 setTagHookParamTaint
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 setFuncHookParamTaint
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 visitAssign
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * Copyright (C) 2017  Brian Wolff <bawolff@gmail.com>
5 *
6 * @license GPL-2.0-or-later
7 */
8
9namespace SecurityCheckPlugin;
10
11use ast\Node;
12use Phan\Language\FQSEN\FullyQualifiedClassName;
13
14/**
15 * Class for visiting any nodes we want to handle in pre-order.
16 */
17class MWPreVisitor extends PreTaintednessVisitor {
18    /**
19     * Set taint for certain hook types.
20     *
21     * Also handles FuncDecl
22     */
23    public function visitMethod( Node $node ): void {
24        parent::visitMethod( $node );
25
26        $fqsen = $this->context->getFunctionLikeFQSEN();
27        $hookType = MediaWikiHooksHelper::getInstance()->isSpecialHookSubscriber( $fqsen );
28        if ( !$hookType ) {
29            return;
30        }
31        $params = $node->children['params']->children;
32
33        switch ( $hookType ) {
34            case '!ParserFunctionHook':
35                $this->setFuncHookParamTaint( $params );
36                break;
37            case '!ParserHook':
38                $this->setTagHookParamTaint( $params );
39                break;
40        }
41    }
42
43    /**
44     * Set taint for a tag hook.
45     *
46     * The parameters are:
47     *  string contents (Tainted from wikitext)
48     *  array attribs (Tainted from wikitext)
49     *  Parser object
50     *  PPFrame object
51     *
52     * @param array $params formal parameters of tag hook
53     * @phan-param array<Node|int|string|bool|null|float> $params
54     */
55    private function setTagHookParamTaint( array $params ): void {
56        // Only care about first 2 parameters.
57        $scope = $this->context->getScope();
58        for ( $i = 0; $i < 2 && $i < count( $params ); $i++ ) {
59            $param = $params[$i];
60            if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
61                // @codeCoverageIgnoreStart
62                $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
63                continue;
64                // @codeCoverageIgnoreEnd
65            }
66            $varObj = $scope->getVariableByName( $param->children['name'] );
67            $argTaint = Taintedness::newTainted();
68            self::setTaintednessRaw( $varObj, $argTaint );
69            $this->addTaintError( $varObj, $argTaint, null, 'tainted argument to tag hook' );
70            // $this->debug( __METHOD__, "In $method setting param $varObj as tainted" );
71        }
72        // If there are no type hints, phan won't know that the parser
73        // is a parser as the hook isn't triggered from a real func call.
74        $paramFQSENs = [
75            2 => FullyQualifiedClassName::fromFullyQualifiedString( '\\MediaWiki\\Parser\\Parser' ),
76            3 => FullyQualifiedClassName::fromFullyQualifiedString( '\\MediaWiki\\Parser\\PPFrame' ),
77        ];
78        foreach ( $paramFQSENs as $i => $fqsen ) {
79            if ( isset( $params[$i] ) ) {
80                $param = $params[$i];
81                if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
82                    // @codeCoverageIgnoreStart
83                    $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
84                    // @codeCoverageIgnoreEnd
85                } else {
86                    $varObj = $scope->getVariableByName( $param->children['name'] );
87                    $varObj->setUnionType( $fqsen->asPHPDocUnionType() );
88                }
89            }
90        }
91    }
92
93    /**
94     * Set the appropriate taint for a parser function hook
95     *
96     * Basically all but the first arg comes from wikitext
97     * and is tainted.
98     *
99     * @todo This is handling SFH_OBJECT type func hooks incorrectly.
100     * @param Node[] $params Children of the AST_PARAM_LIST
101     */
102    private function setFuncHookParamTaint( array $params ): void {
103        // First make sure the first arg is set to be a Parser
104        $scope = $this->context->getScope();
105        if ( isset( $params[0] ) ) {
106            $param = $params[0];
107            if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
108                // @codeCoverageIgnoreStart
109                $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
110                // @codeCoverageIgnoreEnd
111            } else {
112                $varObj = $scope->getVariableByName( $param->children['name'] );
113                $varObj->setUnionType(
114                    FullyQualifiedClassName::fromFullyQualifiedString( '\\MediaWiki\\Parser\\Parser' )
115                        ->asPHPDocUnionType()
116                );
117            }
118        }
119
120        foreach ( $params as $i => $param ) {
121            if ( $i === 0 ) {
122                continue;
123            }
124            if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
125                // @codeCoverageIgnoreStart
126                $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
127                continue;
128                // @codeCoverageIgnoreEnd
129            }
130            $varObj = $scope->getVariableByName( $param->children['name'] );
131            $argTaint = Taintedness::newTainted();
132            self::setTaintednessRaw( $varObj, $argTaint );
133            $this->addTaintError( $varObj, $argTaint, null, 'tainted argument to parser hook' );
134        }
135    }
136
137    public function visitAssign( Node $node ): void {
138        parent::visitAssign( $node );
139
140        $lhs = $node->children['var'];
141        if ( $lhs instanceof Node && $lhs->kind === \ast\AST_ARRAY ) {
142            // Don't try interpreting the node as an HTMLForm specifier later on, both for performance, and because
143            // resolving values might cause phan to emit issues (see test undeclaredvar3)
144            // @phan-suppress-next-line PhanUndeclaredProperty
145            $lhs->skipHTMLFormAnalysis = true;
146        }
147    }
148}