Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
MWPreVisitor
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
4 / 4
20
100.00% covered (success)
100.00%
1 / 1
 visitMethod
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 setTagHookParamTaint
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 setFuncHookParamTaint
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 visitAssign
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace SecurityCheckPlugin;
4
5use ast\Node;
6use Phan\Language\UnionType;
7
8/**
9 * Class for visiting any nodes we want to handle in pre-order.
10 *
11 * Copyright (C) 2017  Brian Wolff <bawolff@gmail.com>
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License along
24 * with this program; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 */
27class MWPreVisitor extends PreTaintednessVisitor {
28    /**
29     * Set taint for certain hook types.
30     *
31     * Also handles FuncDecl
32     * @param Node $node
33     */
34    public function visitMethod( Node $node ): void {
35        parent::visitMethod( $node );
36
37        $fqsen = $this->context->getFunctionLikeFQSEN();
38        $hookType = MediaWikiHooksHelper::getInstance()->isSpecialHookSubscriber( $fqsen );
39        if ( !$hookType ) {
40            return;
41        }
42        $params = $node->children['params']->children;
43
44        switch ( $hookType ) {
45            case '!ParserFunctionHook':
46                $this->setFuncHookParamTaint( $params );
47                break;
48            case '!ParserHook':
49                $this->setTagHookParamTaint( $params );
50                break;
51        }
52    }
53
54    /**
55     * Set taint for a tag hook.
56     *
57     * The parameters are:
58     *  string contents (Tainted from wikitext)
59     *  array attribs (Tainted from wikitext)
60     *  Parser object
61     *  PPFrame object
62     *
63     * @param array $params formal parameters of tag hook
64     * @phan-param array<Node|int|string|bool|null|float> $params
65     */
66    private function setTagHookParamTaint( array $params ): void {
67        // Only care about first 2 parameters.
68        $scope = $this->context->getScope();
69        for ( $i = 0; $i < 2 && $i < count( $params ); $i++ ) {
70            $param = $params[$i];
71            if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
72                // @codeCoverageIgnoreStart
73                $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
74                continue;
75                // @codeCoverageIgnoreEnd
76            }
77            $varObj = $scope->getVariableByName( $param->children['name'] );
78            $argTaint = Taintedness::newTainted();
79            self::setTaintednessRaw( $varObj, $argTaint );
80            $this->addTaintError( $varObj, $argTaint, null, 'tainted argument to tag hook' );
81            // $this->debug( __METHOD__, "In $method setting param $varObj as tainted" );
82        }
83        // If there are no type hints, phan won't know that the parser
84        // is a parser as the hook isn't triggered from a real func call.
85        $hooksHelper = MediaWikiHooksHelper::getInstance();
86        $paramTypes = [
87            2 => $hooksHelper->getMwParserClassFQSEN( $this->code_base )->__toString(),
88            3 => $hooksHelper->getPPFrameClassFQSEN( $this->code_base )->__toString(),
89        ];
90        foreach ( $paramTypes as $i => $type ) {
91            if ( isset( $params[$i] ) ) {
92                $param = $params[$i];
93                if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
94                    // @codeCoverageIgnoreStart
95                    $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
96                    // @codeCoverageIgnoreEnd
97                } else {
98                    $varObj = $scope->getVariableByName( $param->children['name'] );
99                    $varObj->setUnionType(
100                        UnionType::fromFullyQualifiedPHPDocString( $type )
101                    );
102                }
103            }
104        }
105    }
106
107    /**
108     * Set the appropriate taint for a parser function hook
109     *
110     * Basically all but the first arg comes from wikitext
111     * and is tainted.
112     *
113     * @todo This is handling SFH_OBJECT type func hooks incorrectly.
114     * @param Node[] $params Children of the AST_PARAM_LIST
115     */
116    private function setFuncHookParamTaint( array $params ): void {
117        // First make sure the first arg is set to be a Parser
118        $scope = $this->context->getScope();
119        if ( isset( $params[0] ) ) {
120            $param = $params[0];
121            if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
122                // @codeCoverageIgnoreStart
123                $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
124                // @codeCoverageIgnoreEnd
125            } else {
126                $varObj = $scope->getVariableByName( $param->children['name'] );
127                $varObj->setUnionType(
128                    MediaWikiHooksHelper::getInstance()->getMwParserClassFQSEN( $this->code_base )->asPHPDocUnionType()
129                );
130            }
131        }
132
133        foreach ( $params as $i => $param ) {
134            if ( $i === 0 ) {
135                continue;
136            }
137            if ( !$scope->hasVariableWithName( $param->children['name'] ) ) {
138                // @codeCoverageIgnoreStart
139                $this->debug( __METHOD__, "Missing variable for param \$" . $param->children['name'] );
140                continue;
141                // @codeCoverageIgnoreEnd
142            }
143            $varObj = $scope->getVariableByName( $param->children['name'] );
144            $argTaint = Taintedness::newTainted();
145            self::setTaintednessRaw( $varObj, $argTaint );
146            $this->addTaintError( $varObj, $argTaint, null, 'tainted argument to parser hook' );
147        }
148    }
149
150    /**
151     * @param Node $node
152     */
153    public function visitAssign( Node $node ): void {
154        parent::visitAssign( $node );
155
156        $lhs = $node->children['var'];
157        if ( $lhs instanceof Node && $lhs->kind === \ast\AST_ARRAY ) {
158            // Don't try interpreting the node as an HTMLForm specifier later on, both for performance, and because
159            // resolving values might cause phan to emit issues (see test undeclaredvar3)
160            // @phan-suppress-next-line PhanUndeclaredProperty
161            $lhs->skipHTMLFormAnalysis = true;
162        }
163    }
164}