Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.28% covered (success)
95.28%
121 / 127
55.56% covered (warning)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TaintednessAssignVisitor
95.28% covered (success)
95.28%
121 / 127
55.56% covered (warning)
55.56%
5 / 9
43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 isRHSArray
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 visitArray
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 visitVar
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 visitProp
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 visitStaticProp
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 maybeAddNumkeyOnAssignmentLHS
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 visitDim
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 doAssignmentSingleElement
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
1 / 1
17
1<?php
2
3namespace SecurityCheckPlugin;
4
5use ast\Node;
6use Closure;
7use Phan\CodeBase;
8use Phan\Exception\IssueException;
9use Phan\Exception\NodeException;
10use Phan\Exception\UnanalyzableException;
11use Phan\Language\Context;
12use Phan\Language\Element\GlobalVariable;
13use Phan\Language\Element\Property;
14use Phan\Language\Element\TypedElementInterface;
15use Phan\PluginV3\PluginAwareBaseAnalysisVisitor;
16
17/**
18 * @see \Phan\Analysis\AssignmentVisitor
19 */
20class TaintednessAssignVisitor extends PluginAwareBaseAnalysisVisitor {
21    use TaintednessBaseVisitor;
22
23    /** @var Taintedness */
24    private $rightTaint;
25
26    /** @var Taintedness */
27    private $errorTaint;
28
29    /** @var MethodLinks */
30    private $errorLinks;
31
32    /** @var CausedByLines */
33    private $rightError;
34
35    /** @var MethodLinks */
36    private $rightLinks;
37
38    /** @var bool|null */
39    private $rhsIsArray;
40
41    /** @var Closure|null */
42    private $rhsIsArrayGetter;
43
44    /** @var int */
45    private $dimDepth;
46
47    /**
48     * @inheritDoc
49     * @param Taintedness $rightTaint
50     * @param CausedByLines $rightLines
51     * @param MethodLinks $rightLinks
52     * @param Taintedness $errorTaint
53     * @param MethodLinks $errorLinks
54     * @param Closure|bool $rhsIsArrayOrGetter
55     * @phan-param Closure():bool|bool $rhsIsArrayOrGetter
56     * @param int $depth
57     */
58    public function __construct(
59        CodeBase $code_base,
60        Context $context,
61        Taintedness $rightTaint,
62        CausedByLines $rightLines,
63        MethodLinks $rightLinks,
64        Taintedness $errorTaint,
65        MethodLinks $errorLinks,
66        $rhsIsArrayOrGetter,
67        int $depth = 0
68    ) {
69        parent::__construct( $code_base, $context );
70        $this->rightTaint = $rightTaint;
71        $this->rightError = $rightLines;
72        $this->rightLinks = $rightLinks;
73        $this->errorTaint = $errorTaint;
74        $this->errorLinks = $errorLinks;
75        if ( is_callable( $rhsIsArrayOrGetter ) ) {
76            $this->rhsIsArrayGetter = $rhsIsArrayOrGetter;
77        } else {
78            $this->rhsIsArray = $rhsIsArrayOrGetter;
79        }
80        $this->dimDepth = $depth;
81    }
82
83    private function isRHSArray(): bool {
84        if ( $this->rhsIsArray !== null ) {
85            return $this->rhsIsArray;
86        }
87        $this->rhsIsArray = ( $this->rhsIsArrayGetter )();
88        return $this->rhsIsArray;
89    }
90
91    /**
92     * @param Node $node
93     */
94    public function visitArray( Node $node ): void {
95        $numKey = 0;
96        foreach ( $node->children as $child ) {
97            if ( $child === null ) {
98                $numKey++;
99                continue;
100            }
101            if ( !$child instanceof Node || $child->kind !== \ast\AST_ARRAY_ELEM ) {
102                // Syntax error.
103                return;
104            }
105            $key = $child->children['key'] !== null ? $this->resolveOffset( $child->children['key'] ) : $numKey++;
106            $value = $child->children['value'];
107            if ( !$value instanceof Node ) {
108                // Syntax error, don't crash, and bail out immediately.
109                return;
110            }
111            $childVisitor = new self(
112                $this->code_base,
113                $this->context,
114                $this->rightTaint->getTaintednessForOffsetOrWhole( $key ),
115                $this->rightError,
116                $this->rightLinks,
117                $this->errorTaint->getTaintednessForOffsetOrWhole( $key ),
118                $this->errorLinks,
119                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
120                $this->rhsIsArray ?? $this->rhsIsArrayGetter,
121                $this->dimDepth
122            );
123            $childVisitor( $value );
124        }
125    }
126
127    /**
128     * @param Node $node
129     */
130    public function visitVar( Node $node ): void {
131        try {
132            $var = $this->getCtxN( $node )->getVariable();
133        } catch ( NodeException | IssueException $_ ) {
134            return;
135        }
136        $this->doAssignmentSingleElement( $var );
137    }
138
139    /**
140     * @param Node $node
141     */
142    public function visitProp( Node $node ): void {
143        try {
144            $prop = $this->getCtxN( $node )->getProperty( false );
145        } catch ( NodeException | IssueException | UnanalyzableException $_ ) {
146            return;
147        }
148        $this->doAssignmentSingleElement( $prop );
149    }
150
151    /**
152     * @param Node $node
153     */
154    public function visitStaticProp( Node $node ): void {
155        try {
156            $prop = $this->getCtxN( $node )->getProperty( true );
157        } catch ( NodeException | IssueException | UnanalyzableException $_ ) {
158            return;
159        }
160        $this->doAssignmentSingleElement( $prop );
161    }
162
163    /**
164     * If we're assigning an SQL tainted value as an array key
165     * or as the value of a numeric key, then set NUMKEY taint.
166     *
167     * @param Node $dimLHS
168     */
169    private function maybeAddNumkeyOnAssignmentLHS( Node $dimLHS ): void {
170        if ( $this->rightTaint->has( SecurityCheckPlugin::SQL_NUMKEY_TAINT ) ) {
171            // Already there, no need to add it again.
172            return;
173        }
174
175        $dim = $dimLHS->children['dim'];
176        if (
177            $this->rightTaint->has( SecurityCheckPlugin::SQL_TAINT )
178            && ( $dim === null || $this->nodeCanBeIntKey( $dim ) )
179            && !$this->isRHSArray()
180        ) {
181            $this->rightTaint->add( SecurityCheckPlugin::SQL_NUMKEY_TAINT );
182            $this->errorTaint->add( SecurityCheckPlugin::SQL_NUMKEY_TAINT );
183        }
184    }
185
186    /**
187     * @param Node $node
188     */
189    public function visitDim( Node $node ): void {
190        if ( !$node->children['expr'] instanceof Node ) {
191            // Invalid syntax.
192            return;
193        }
194        $dimNode = $node->children['dim'];
195        if ( $dimNode === null ) {
196            $curOff = null;
197        } else {
198            $curOff = $this->resolveOffset( $dimNode );
199        }
200        $this->dimDepth++;
201        $dimTaintWithErr = $this->getTaintedness( $dimNode );
202        $dimTaintInt = $dimTaintWithErr->getTaintedness()->get();
203        $this->rightTaint = $this->rightTaint->asMaybeMovedAtOffset( $curOff, $dimTaintInt );
204        $dimLinks = $dimTaintWithErr->getMethodLinks()->getLinksCollapsing();
205        $this->rightLinks = $this->rightLinks->asMaybeMovedAtOffset( $curOff, $dimLinks );
206        $this->errorTaint->addKeysTaintedness( $dimTaintInt );
207        $this->maybeAddNumkeyOnAssignmentLHS( $node );
208        $this( $node->children['expr'] );
209    }
210
211    /**
212     * @param TypedElementInterface $variableObj
213     */
214    private function doAssignmentSingleElement(
215        TypedElementInterface $variableObj
216    ): void {
217        $globalVarObj = $variableObj instanceof GlobalVariable ? $variableObj->getElement() : null;
218
219        // Make sure assigning to $this->bar doesn't kill the whole prop taint.
220        // Note: If there is a local variable that is a reference to another non-local variable, this will not
221        // affect the non-local one (Pass by reference arguments are handled separately and work as expected).
222        // TODO Should we also check for normal Variables in the global scope? See test setafterexec
223        $override = !( $variableObj instanceof Property ) && !$globalVarObj;
224
225        $overrideTaint = $override;
226        if ( $this->dimDepth > 0 ) {
227            $curTaint = self::getTaintednessRaw( $variableObj );
228            if ( $curTaint ) {
229                $newTaint = $override
230                    ? $curTaint->asMergedForAssignment( $this->rightTaint, $this->dimDepth )
231                    : $curTaint->asMergedWith( $this->rightTaint );
232            } else {
233                $newTaint = $this->rightTaint;
234            }
235            $overrideTaint = true;
236        } else {
237            $newTaint = $this->rightTaint;
238        }
239        $this->setTaintedness( $variableObj, $newTaint, $overrideTaint );
240
241        if ( $globalVarObj ) {
242            // Merge the taint on the "true" global object, too
243            if ( $this->dimDepth > 0 ) {
244                $curGlobalTaint = self::getTaintednessRaw( $globalVarObj );
245                if ( $curGlobalTaint ) {
246                    $newGlobalTaint = clone $curGlobalTaint;
247                    $newGlobalTaint->mergeWith( $this->rightTaint );
248                } else {
249                    $newGlobalTaint = $this->rightTaint;
250                }
251                $overrideGlobalTaint = true;
252            } else {
253                $newGlobalTaint = $this->rightTaint;
254                $overrideGlobalTaint = false;
255            }
256            $this->setTaintedness( $globalVarObj, $newGlobalTaint, $overrideGlobalTaint );
257        }
258
259        if ( $this->dimDepth > 0 ) {
260            $curLinks = self::getMethodLinksCloneOrEmpty( $variableObj );
261            $newLinks = $override
262                ? $curLinks->asMergedForAssignment( $this->rightLinks, $this->dimDepth )
263                : $curLinks->asMergedWith( $this->rightLinks );
264            $overrideLinks = true;
265        } else {
266            $newLinks = $this->rightLinks;
267            $overrideLinks = $override;
268        }
269        $this->mergeTaintDependencies( $variableObj, $newLinks, $overrideLinks );
270        if ( $globalVarObj ) {
271            // Merge dependencies on the global copy as well
272            if ( $this->dimDepth > 0 ) {
273                $curGlobalLinks = self::getMethodLinksCloneOrEmpty( $globalVarObj );
274                $newGlobalLinks = $curGlobalLinks->asMergedWith( $this->rightLinks );
275                $overrideGlobalLinks = true;
276            } else {
277                $newGlobalLinks = $this->rightLinks;
278                $overrideGlobalLinks = false;
279            }
280            $this->mergeTaintDependencies( $globalVarObj, $newGlobalLinks, $overrideGlobalLinks );
281        }
282
283        if ( $this->dimDepth > 0 ) {
284            $curError = self::getCausedByRaw( $variableObj );
285            $newError = $curError ? clone $curError : new CausedByLines();
286            $newError->mergeWith( $this->rightError );
287            $overrideError = true;
288        } else {
289            $newError = $this->rightError;
290            $overrideError = $override;
291        }
292        if ( $overrideError ) {
293            self::clearTaintError( $variableObj );
294        }
295        $this->addTaintError( $variableObj, $this->errorTaint, $this->errorLinks );
296        $this->mergeTaintError( $variableObj, $newError );
297        if ( $globalVarObj ) {
298            $this->addTaintError( $globalVarObj, $this->errorTaint, $this->errorLinks );
299            $this->mergeTaintError( $globalVarObj, $newError );
300        }
301    }
302}