Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.95% covered (success)
95.95%
142 / 148
55.56% covered (warning)
55.56%
5 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TaintednessAssignVisitor
95.95% covered (success)
95.95%
142 / 148
55.56% covered (warning)
55.56%
5 / 9
47
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
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
3.00
 doAssignmentSingleElement
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
1 / 1
21
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->getForDim( $key ),
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 = $this->rightTaint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT );
182            $this->errorTaint = $this->errorTaint->with( 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        $dimError = $dimTaintWithErr->getError()->asAllMovedToKeys();
207        $this->rightError = $this->rightError->asAllMaybeMovedAtOffset( $curOff )->asMergedWith( $dimError );
208        $this->errorTaint = $this->errorTaint->asMaybeMovedAtOffset( $curOff, $dimTaintInt );
209        $this->errorLinks = $this->errorLinks->asMaybeMovedAtOffset( $curOff, $dimLinks );
210        $this->maybeAddNumkeyOnAssignmentLHS( $node );
211        $this( $node->children['expr'] );
212    }
213
214    /**
215     * @param TypedElementInterface $variableObj
216     */
217    private function doAssignmentSingleElement(
218        TypedElementInterface $variableObj
219    ): void {
220        $globalVarObj = $variableObj instanceof GlobalVariable ? $variableObj->getElement() : null;
221
222        // Make sure assigning to $this->bar doesn't kill the whole prop taint.
223        // Note: If there is a local variable that is a reference to another non-local variable, this will not
224        // affect the non-local one (Pass by reference arguments are handled separately and work as expected).
225        // TODO Should we also check for normal Variables in the global scope? See test setafterexec
226        $override = !( $variableObj instanceof Property ) && !$globalVarObj;
227
228        $overrideTaint = $override;
229        if ( $this->dimDepth > 0 ) {
230            $curTaint = self::getTaintednessRaw( $variableObj );
231            if ( $curTaint ) {
232                $newTaint = $override
233                    ? $curTaint->asMergedForAssignment( $this->rightTaint, $this->dimDepth )
234                    : $curTaint->asMergedWith( $this->rightTaint );
235            } else {
236                $newTaint = $this->rightTaint;
237            }
238            $overrideTaint = true;
239        } else {
240            $newTaint = $this->rightTaint;
241        }
242        $this->setTaintedness( $variableObj, $newTaint, $overrideTaint );
243
244        if ( $globalVarObj ) {
245            // Merge the taint on the "true" global object, too
246            if ( $this->dimDepth > 0 ) {
247                $curGlobalTaint = self::getTaintednessRaw( $globalVarObj );
248                if ( $curGlobalTaint ) {
249                    $newGlobalTaint = $curGlobalTaint->asMergedWith( $this->rightTaint );
250                } else {
251                    $newGlobalTaint = $this->rightTaint;
252                }
253                $overrideGlobalTaint = true;
254            } else {
255                $newGlobalTaint = $this->rightTaint;
256                $overrideGlobalTaint = false;
257            }
258            $this->setTaintedness( $globalVarObj, $newGlobalTaint, $overrideGlobalTaint );
259        }
260
261        if ( $this->dimDepth > 0 ) {
262            $curLinks = self::getMethodLinks( $variableObj ) ?? MethodLinks::emptySingleton();
263            $newLinks = $override
264                ? $curLinks->asMergedForAssignment( $this->rightLinks, $this->dimDepth )
265                : $curLinks->asMergedWith( $this->rightLinks );
266            $overrideLinks = true;
267        } else {
268            $newLinks = $this->rightLinks;
269            $overrideLinks = $override;
270        }
271        $this->mergeTaintDependencies( $variableObj, $newLinks, $overrideLinks );
272        if ( $globalVarObj ) {
273            // Merge dependencies on the global copy as well
274            if ( $this->dimDepth > 0 ) {
275                $curGlobalLinks = self::getMethodLinks( $globalVarObj );
276                $newGlobalLinks = $curGlobalLinks
277                    ? $curGlobalLinks->asMergedWith( $this->rightLinks )
278                    : $this->rightLinks;
279                $overrideGlobalLinks = true;
280            } else {
281                $newGlobalLinks = $this->rightLinks;
282                $overrideGlobalLinks = false;
283            }
284            $this->mergeTaintDependencies( $globalVarObj, $newGlobalLinks, $overrideGlobalLinks );
285        }
286
287        $curLineCausedBy = $this->getCausedByLinesToAdd( $this->errorTaint, $this->errorLinks );
288        if ( $this->dimDepth > 0 ) {
289            $curError = self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton();
290            $newError = $override
291                ? $curError->asMergedWith( $this->rightError, $this->dimDepth )
292                : $curError->asMergedWith( $this->rightError );
293            $overrideError = true;
294        } else {
295            $newError = $this->rightError;
296            $overrideError = $override;
297        }
298        $curError = $overrideError
299            ? CausedByLines::emptySingleton()
300            : self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton();
301        $newOverallError = $curError->withAddedLines( $curLineCausedBy, $this->errorTaint, $this->errorLinks )
302            ->asMergedWith( $newError );
303        self::setCausedByRaw( $variableObj, $newOverallError );
304
305        if ( $globalVarObj ) {
306            if ( $this->dimDepth > 0 ) {
307                $curGlobalError = self::getCausedByRaw( $globalVarObj );
308                $newGlobalError = $curGlobalError
309                    ? $curGlobalError->asMergedWith( $this->rightError )
310                    : $this->rightError;
311                $overrideGlobalError = true;
312            } else {
313                $newGlobalError = $this->rightError;
314                $overrideGlobalError = false;
315            }
316            $curGlobalError = $overrideGlobalError
317                ? CausedByLines::emptySingleton()
318                : self::getCausedByRaw( $globalVarObj ) ?? CausedByLines::emptySingleton();
319            $newOverallGlobalError = $curGlobalError
320                ->withAddedLines( $curLineCausedBy, $this->errorTaint, $this->errorLinks )
321                ->asMergedWith( $newGlobalError );
322            self::setCausedByRaw( $globalVarObj, $newOverallGlobalError );
323        }
324    }
325}