Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.94% covered (success)
96.94%
507 / 523
88.71% covered (warning)
88.71%
55 / 62
CRAP
0.00% covered (danger)
0.00%
0 / 1
TaintednessVisitor
96.94% covered (success)
96.94%
507 / 523
88.71% covered (warning)
88.71%
55 / 62
154
0.00% covered (danger)
0.00%
0 / 1
 analyzeNodeAndGetTaintedness
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setCachedData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCurTaintUnknown
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCurTaintSafe
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visit
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 visitClosure
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 visitFuncDecl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitMethod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitArrowFunc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 analyzeFunctionLike
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 visitClassName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitThrow
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitUnset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 handleUnsetDim
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
9.37
 visitClone
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitAssignOp
94.12% covered (success)
94.12%
32 / 34
0.00% covered (danger)
0.00%
0 / 1
3.00
 visitStatic
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitAssignRef
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitAssign
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
 doVisitAssign
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 visitBinaryOp
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
 getBinOpTaint
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 visitDim
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 visitPrint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitExit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitShellExec
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 visitIncludeOrEval
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 visitEcho
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 visitSimpleSinkAndPropagate
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 visitStaticCall
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitNew
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
8
 visitMethodCall
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 analyzeCallNode
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 visitNullsafeMethodCall
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitCall
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 visitVar
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
4.01
 getHardcodedTaintednessForVar
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
14
 visitGlobal
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 visitReturn
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
 setFuncTaintFromReturn
97.22% covered (success)
97.22%
35 / 36
0.00% covered (danger)
0.00%
0 / 1
11
 visitArray
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
9
 visitClassConst
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitConst
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitStaticProp
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
2.03
 visitProp
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
7
 visitNullsafeProp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitConditional
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 visitName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitNameList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitUnaryOp
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 visitPostInc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitPreInc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitPostDec
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitPreDec
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 analyzeIncOrDec
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 visitCast
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 visitEncapsList
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 visitIsset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitMagicConst
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitInstanceOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitMatch
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php declare( strict_types=1 );
2/**
3 * Copyright (C) 2017  Brian Wolff <bawolff@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20namespace SecurityCheckPlugin;
21
22use ast\Node;
23use Phan\Analysis\BlockExitStatusChecker;
24use Phan\AST\ContextNode;
25use Phan\Debug;
26use Phan\Exception\CodeBaseException;
27use Phan\Exception\IssueException;
28use Phan\Exception\NodeException;
29use Phan\Language\Element\FunctionInterface;
30use Phan\Language\Element\GlobalVariable;
31use Phan\Language\FQSEN\FullyQualifiedClassName;
32use Phan\Language\FQSEN\FullyQualifiedFunctionName;
33use Phan\Language\Type\FunctionLikeDeclarationType;
34use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
35
36/**
37 * This class visits all the nodes in the ast. It has two jobs:
38 *
39 * 1) Return the taint value of the current node we are visiting.
40 * 2) In the event of an assignment (and similar things) propagate
41 *  the taint value from the left hand side to the right hand side.
42 *
43 * For the moment, the taint values are stored in a "taintedness"
44 * property of various phan TypedElement objects. This is probably
45 * not the best solution for where to store the data, but its what
46 * this does for now.
47 *
48 * This also maintains some other properties, such as where the error
49 * originates, and dependencies in certain cases.
50 *
51 * @phan-file-suppress PhanUnusedPublicMethodParameter Many methods don't use $node
52 */
53class TaintednessVisitor extends PluginAwarePostAnalysisVisitor {
54    use TaintednessBaseVisitor;
55
56    /**
57     * Node kinds whose taintedness is not well-defined and for which we don't need a visit* method.
58     */
59    public const INAPPLICABLE_NODES_WITHOUT_VISITOR = [
60        \ast\AST_ARG_LIST => true,
61        \ast\AST_TYPE => true,
62        \ast\AST_NULLABLE_TYPE => true,
63        \ast\AST_PARAM_LIST => true,
64        // Params are handled in PreTaintednessVisitor
65        \ast\AST_PARAM => true,
66        \ast\AST_CLASS => true,
67        \ast\AST_USE_ELEM => true,
68        \ast\AST_STMT_LIST => true,
69        \ast\AST_CLASS_CONST_DECL => true,
70        \ast\AST_CLASS_CONST_GROUP => true,
71        \ast\AST_CONST_DECL => true,
72        \ast\AST_IF => true,
73        \ast\AST_IF_ELEM => true,
74        \ast\AST_PROP_DECL => true,
75        \ast\AST_CONST_ELEM => true,
76        \ast\AST_USE => true,
77        \ast\AST_USE_TRAIT => true,
78        \ast\AST_BREAK => true,
79        \ast\AST_CONTINUE => true,
80        \ast\AST_GOTO => true,
81        \ast\AST_CATCH => true,
82        \ast\AST_NAMESPACE => true,
83        \ast\AST_SWITCH => true,
84        \ast\AST_SWITCH_CASE => true,
85        \ast\AST_SWITCH_LIST => true,
86        \ast\AST_WHILE => true,
87        \ast\AST_DO_WHILE => true,
88        \ast\AST_FOR => true,
89        // Handled in TaintednessLoopVisitor
90        \ast\AST_FOREACH => true,
91        \ast\AST_EXPR_LIST => true,
92        \ast\AST_TRY => true,
93        // Array elems are handled directly in visitArray
94        \ast\AST_ARRAY_ELEM => true,
95        // Initializing the prop is done in preorder
96        \ast\AST_PROP_ELEM => true,
97        \ast\AST_PROP_GROUP => true,
98        // Variables are already handled in visitVar
99        \ast\AST_CLOSURE_VAR => true,
100        \ast\AST_CLOSURE_USES => true,
101        \ast\AST_LABEL => true,
102        \ast\AST_ATTRIBUTE => true,
103        \ast\AST_ATTRIBUTE_GROUP => true,
104        \ast\AST_ATTRIBUTE_LIST => true,
105    ];
106
107    /**
108     * Node kinds whose taintedness is not well-defined, but for which we still need a visit* method.
109     * Trying to get the taintedness of these nodes will still result in an error.
110     */
111    public const INAPPLICABLE_NODES_WITH_VISITOR = [
112        \ast\AST_GLOBAL => true,
113        \ast\AST_RETURN => true,
114        \ast\AST_STATIC => true,
115        \ast\AST_FUNC_DECL => true,
116        \ast\AST_METHOD => true,
117    ];
118
119    /**
120     * Map of node kinds whose taintedness is not well-defined, e.g. because that node
121     * cannot be used as an expression. Note that it's safe to use array plus here.
122     */
123    private const INAPPLICABLE_NODES = self::INAPPLICABLE_NODES_WITHOUT_VISITOR + self::INAPPLICABLE_NODES_WITH_VISITOR;
124
125    /** @var TaintednessWithError|null */
126    private $curTaintWithError;
127
128    /**
129     * @param Node $node
130     * @return TaintednessWithError
131     */
132    public function analyzeNodeAndGetTaintedness( Node $node ): TaintednessWithError {
133        assert(
134            !isset( self::INAPPLICABLE_NODES[$node->kind] ),
135            'Should not try to get taintedness of inapplicable nodes (got ' . Debug::nodeName( $node ) . ')'
136        );
137        $this->__invoke( $node );
138        $this->setCachedData( $node );
139        return $this->curTaintWithError;
140    }
141
142    /**
143     * Cache taintedness data in an AST node. Ideally we'd want this to happen at the end of __invoke, but phan
144     * calls visit* methods by name, so that doesn't work.
145     * Caching a node *may* improve the speed, but *will* increase the memory usage, so only do that for nodes
146     * whose taintedness:
147     *  - Is not trivial to compute, and
148     *  - Might be needed from another node (via getTaintednessNode)
149     * @param Node $node
150     */
151    private function setCachedData( Node $node ): void {
152        // @phan-suppress-next-line PhanUndeclaredProperty
153        $node->taint = $this->curTaintWithError;
154    }
155
156    /**
157     * Sets $this->curTaint to UNKNOWN. Shorthand to filter the usages of curTaint.
158     */
159    private function setCurTaintUnknown(): void {
160        $this->curTaintWithError = TaintednessWithError::unknownSingleton();
161    }
162
163    private function setCurTaintSafe(): void {
164        $this->curTaintWithError = TaintednessWithError::emptySingleton();
165    }
166
167    /**
168     * Generic visitor when we haven't defined a more specific one.
169     *
170     * @param Node $node
171     */
172    public function visit( Node $node ): void {
173        // This method will be called on all nodes for which
174        // there is no implementation of its kind visitor.
175
176        if ( isset( self::INAPPLICABLE_NODES_WITHOUT_VISITOR[$node->kind] ) ) {
177            return;
178        }
179
180        // To see what kinds of nodes are passing through here,
181        // you can run `Debug::printNode($node)`.
182        # Debug::printNode( $node );
183        $this->debug( __METHOD__, "unhandled case " . Debug::nodeName( $node ) );
184        $this->setCurTaintUnknown();
185    }
186
187    /**
188     * @param Node $node
189     */
190    public function visitClosure( Node $node ): void {
191        // We cannot use getFunctionLikeInScope for closures
192        $closureFQSEN = FullyQualifiedFunctionName::fromClosureInContext( $this->context, $node );
193
194        if ( $this->code_base->hasFunctionWithFQSEN( $closureFQSEN ) ) {
195            $func = $this->code_base->getFunctionByFQSEN( $closureFQSEN );
196            $this->analyzeFunctionLike( $func );
197        } else {
198            // @codeCoverageIgnoreStart
199            $this->debug( __METHOD__, 'closure doesn\'t exist' );
200            // @codeCoverageIgnoreEnd
201        }
202        $this->setCurTaintSafe();
203        $this->setCachedData( $node );
204    }
205
206    /**
207     * @param Node $node
208     */
209    public function visitFuncDecl( Node $node ): void {
210        $func = $this->context->getFunctionLikeInScope( $this->code_base );
211        $this->analyzeFunctionLike( $func );
212    }
213
214    /**
215     * Visit a method declaration
216     *
217     * @param Node $node
218     */
219    public function visitMethod( Node $node ): void {
220        $method = $this->context->getFunctionLikeInScope( $this->code_base );
221        $this->analyzeFunctionLike( $method );
222    }
223
224    /**
225     * @param Node $node
226     */
227    public function visitArrowFunc( Node $node ): void {
228        $this->visitClosure( $node );
229    }
230
231    /**
232     * Handles methods, functions and closures.
233     *
234     * @param FunctionInterface $func The func to analyze
235     */
236    private function analyzeFunctionLike( FunctionInterface $func ): void {
237        if ( self::getFuncTaint( $func ) === null ) {
238            // If we still have no data, presumably the function doesn't return anything, so mark as safe.
239            if ( $func->hasReturn() || $func->hasYield() ) {
240                $this->debug( __METHOD__, "TODO: $func returns something but has no taint after analysis" );
241            }
242
243            // NOTE: If the method stores its arg to a class prop, and that class prop gets output later,
244            // the exec status of this won't be detected until the output is analyzed, we might miss some issues
245            // in the inbetween period.
246            self::doSetFuncTaint( $func, FunctionTaintedness::emptySingleton() );
247        }
248    }
249
250    /**
251     * FooBar::class, presumably safe since class names cannot have special chars.
252     *
253     * @param Node $node
254     */
255    public function visitClassName( Node $node ): void {
256        $this->setCurTaintSafe();
257        $this->setCachedData( $node );
258    }
259
260    /**
261     * @note AST_THROW is an expression since PHP 8
262     * @param Node $node
263     */
264    public function visitThrow( Node $node ): void {
265        $this->setCurTaintSafe();
266        $this->setCachedData( $node );
267    }
268
269    /**
270     * @param Node $node
271     */
272    public function visitUnset( Node $node ): void {
273        $varNode = $node->children['var'];
274        if ( $varNode instanceof Node && $varNode->kind === \ast\AST_DIM ) {
275            $this->handleUnsetDim( $varNode );
276        }
277        $this->setCurTaintSafe();
278    }
279
280    /**
281     * Analyzes expressions like unset( $arr['foo'] ) to infer shape mutations.
282     * @param Node $node
283     * @return void
284     */
285    private function handleUnsetDim( Node $node ): void {
286        $expr = $node->children['expr'];
287        if ( !$expr instanceof Node ) {
288            // Syntax error.
289            return;
290        }
291        if ( $expr->kind !== \ast\AST_VAR ) {
292            // For now, we only handle a single offset.
293            // TODO actually recurse.
294            return;
295        }
296
297        $keyNode = $node->children['dim'];
298        $key = $keyNode !== null ? $this->resolveOffset( $keyNode ) : null;
299        if ( $key instanceof Node ) {
300            // We can't tell what gets removed.
301            return;
302        }
303
304        try {
305            $var = $this->getCtxN( $expr )->getVariable();
306        } catch ( NodeException | IssueException $_ ) {
307            return;
308        }
309
310        if ( $var instanceof GlobalVariable ) {
311            // Don't handle for now.
312            return;
313        }
314
315        $curTaint = self::getTaintednessRaw( $var );
316        if ( !$curTaint ) {
317            // Is this even possible? Don't do anything, just in case.
318            return;
319        }
320        self::setTaintednessRaw( $var, $curTaint->withoutKey( $key ) );
321    }
322
323    /**
324     * @param Node $node
325     */
326    public function visitClone( Node $node ): void {
327        $this->curTaintWithError = $this->getTaintedness( $node->children['expr'] );
328        $this->setCachedData( $node );
329    }
330
331    /**
332     * Assignment operators are: .=, +=, -=, /=, %=, *=, **=, ??=, |=, &=, ^=, <<=, >>=
333     * @param Node $node
334     */
335    public function visitAssignOp( Node $node ): void {
336        $lhs = $node->children['var'];
337        if ( !$lhs instanceof Node ) {
338            // Syntax error, don't crash
339            $this->setCurTaintSafe();
340            return;
341        }
342        $rhs = $node->children['expr'];
343        $lhsTaintedness = $this->getTaintedness( $lhs );
344        $rhsTaintedness = $this->getTaintedness( $rhs );
345
346        if ( property_exists( $node, 'assignTaintMask' ) ) {
347            // @phan-suppress-next-line PhanUndeclaredProperty
348            $mask = $node->assignTaintMask;
349            // TODO Should we consume the value, since it depends on the union types?
350        } else {
351            $this->debug( __METHOD__, 'FIXME no preorder visit?' );
352            $mask = SecurityCheckPlugin::ALL_TAINT_FLAGS;
353        }
354
355        // Expand rhs to include implicit lhs ophand.
356        $allRHSTaint = $this->getBinOpTaint(
357            $lhsTaintedness->getTaintedness(),
358            $rhsTaintedness->getTaintedness(),
359            $node->flags,
360            $mask
361        );
362        $allError = $lhsTaintedness->getError()->asMergedWith( $rhsTaintedness->getError() );
363        $allLinks = $lhsTaintedness->getMethodLinks()->asMergedWith( $rhsTaintedness->getMethodLinks() );
364
365        $curTaint = $this->doVisitAssign(
366            $lhs,
367            $rhs,
368            $allRHSTaint,
369            $allError,
370            $allLinks,
371            $rhsTaintedness->getTaintedness(),
372            $rhsTaintedness->getMethodLinks()
373        );
374        // TODO Links and error?
375        $this->curTaintWithError = new TaintednessWithError(
376            $curTaint,
377            CausedByLines::emptySingleton(),
378            MethodLinks::emptySingleton()
379        );
380        $this->setCachedData( $node );
381    }
382
383    /**
384     * `static $var = 'foo'` Handle it as an assignment of a safe value, to initialize the taintedness
385     * on $var. Ideally, we'd want to retain any taintedness on this object, but it's currently impossible
386     * (upstream has the same limitation with union types).
387     *
388     * @param Node $node
389     */
390    public function visitStatic( Node $node ): void {
391        $var = $this->getCtxN( $node->children['var'] )->getVariable();
392        $this->ensureTaintednessIsSet( $var );
393    }
394
395    /**
396     * @param Node $node
397     */
398    public function visitAssignRef( Node $node ): void {
399        $this->visitAssign( $node );
400    }
401
402    /**
403     * @param Node $node
404     */
405    public function visitAssign( Node $node ): void {
406        $lhs = $node->children['var'];
407        if ( !$lhs instanceof Node ) {
408            // Syntax error, don't crash
409            $this->setCurTaintSafe();
410            return;
411        }
412        $rhs = $node->children['expr'];
413
414        $rhsTaintedness = $this->getTaintedness( $rhs );
415        $curTaint = $this->doVisitAssign(
416            $lhs,
417            $rhs,
418            $rhsTaintedness->getTaintedness(),
419            $rhsTaintedness->getError(),
420            $rhsTaintedness->getMethodLinks(),
421            $rhsTaintedness->getTaintedness(),
422            $rhsTaintedness->getMethodLinks()
423        );
424        // TODO Links and error?
425        $this->curTaintWithError = new TaintednessWithError(
426            $curTaint,
427            CausedByLines::emptySingleton(),
428            MethodLinks::emptySingleton()
429        );
430        $this->setCachedData( $node );
431    }
432
433    /**
434     * @param Node $lhs
435     * @param Node|mixed $rhs
436     * @param Taintedness $rhsTaint
437     * @param CausedByLines $rhsError
438     * @param MethodLinks $rhsLinks
439     * @param Taintedness $errorTaint
440     * @param MethodLinks $errorLinks
441     * @return Taintedness
442     */
443    private function doVisitAssign(
444        Node $lhs,
445        $rhs,
446        Taintedness $rhsTaint,
447        CausedByLines $rhsError,
448        MethodLinks $rhsLinks,
449        Taintedness $errorTaint,
450        MethodLinks $errorLinks
451    ): Taintedness {
452        $vis = new TaintednessAssignVisitor(
453            $this->code_base,
454            $this->context,
455            $rhsTaint,
456            $rhsError,
457            $rhsLinks,
458            $errorTaint,
459            $errorLinks,
460            function () use ( $rhs ): bool {
461                return $this->nodeIsArray( $rhs );
462            }
463        );
464        $vis( $lhs );
465        return $rhsTaint;
466    }
467
468    /**
469     * @param Node $node
470     */
471    public function visitBinaryOp( Node $node ): void {
472        $lhs = $node->children['left'];
473        $rhs = $node->children['right'];
474        $mask = $this->getBinOpTaintMask( $node, $lhs, $rhs );
475        if ( $mask === SecurityCheckPlugin::NO_TAINT ) {
476            // If the operation is safe, don't waste time analyzing children.This might also create bugs
477            // like the test undeclaredvar2
478            $this->setCurTaintSafe();
479            $this->setCachedData( $node );
480            return;
481        }
482        $leftTaint = $this->getTaintedness( $lhs );
483        $rightTaint = $this->getTaintedness( $rhs );
484        $curTaint = $this->getBinOpTaint(
485            $leftTaint->getTaintedness(),
486            $rightTaint->getTaintedness(),
487            $node->flags,
488            $mask
489        );
490        $this->curTaintWithError = new TaintednessWithError(
491            $curTaint,
492            $leftTaint->getError()->asMergedWith( $rightTaint->getError() ),
493            $leftTaint->getMethodLinks()->asMergedWith( $rightTaint->getMethodLinks() )
494        );
495        $this->setCachedData( $node );
496    }
497
498    /**
499     * Get the taintedness of a binop, depending on the op type, applying the given flags
500     * @param Taintedness $leftTaint
501     * @param Taintedness $rightTaint
502     * @param int $op Represented by a flags in \ast\flags
503     * @param int $mask
504     * @return Taintedness
505     */
506    private function getBinOpTaint(
507        Taintedness $leftTaint,
508        Taintedness $rightTaint,
509        int $op,
510        int $mask
511    ): Taintedness {
512        if ( $mask === SecurityCheckPlugin::NO_TAINT ) {
513            return Taintedness::safeSingleton();
514        }
515        if ( $op === \ast\flags\BINARY_ADD ) {
516            // HACK: This means that a node can be array, so assume array plus
517            return $leftTaint->asArrayPlusWith( $rightTaint );
518        }
519        return $leftTaint->asMergedWith( $rightTaint )->asCollapsed()->withOnly( $mask );
520    }
521
522    /**
523     * @param Node $node
524     */
525    public function visitDim( Node $node ): void {
526        $varNode = $node->children['expr'];
527        if ( !$varNode instanceof Node ) {
528            // Accessing offset of a string literal
529            $this->setCurTaintSafe();
530            $this->setCachedData( $node );
531            return;
532        }
533        $nodeTaint = $this->getTaintednessNode( $varNode );
534        if ( $node->children['dim'] === null ) {
535            // This should only happen in assignments: $x[] = 'foo'. Just return
536            // the taint of the whole object.
537            $this->curTaintWithError = $nodeTaint;
538            $this->setCachedData( $node );
539            return;
540        }
541        $offset = $this->resolveOffset( $node->children['dim'] );
542        $this->curTaintWithError = new TaintednessWithError(
543            $nodeTaint->getTaintedness()->getTaintednessForOffsetOrWhole( $offset ),
544            $nodeTaint->getError()->getForDim( $offset ),
545            $nodeTaint->getMethodLinks()->getForDim( $offset )
546        );
547        $this->setCachedData( $node );
548    }
549
550    /**
551     * @param Node $node
552     */
553    public function visitPrint( Node $node ): void {
554        $this->visitEcho( $node );
555    }
556
557    /**
558     * This is for exit() and die(). If they're passed an argument, they behave the
559     * same as print.
560     * @param Node $node
561     */
562    public function visitExit( Node $node ): void {
563        $this->visitEcho( $node );
564    }
565
566    /**
567     * Visits the backtick operator. Note that shell_exec() has a simple AST_CALL node.
568     * @param Node $node
569     */
570    public function visitShellExec( Node $node ): void {
571        $this->visitSimpleSinkAndPropagate(
572            $node,
573            SecurityCheckPlugin::SHELL_EXEC_TAINT,
574            'Backtick shell execution operator contains user controlled arg'
575        );
576        // Its unclear if we should consider this tainted or not
577        $this->curTaintWithError = new TaintednessWithError(
578            Taintedness::newTainted(),
579            CausedByLines::emptySingleton(),
580            MethodLinks::emptySingleton()
581        );
582        $this->setCachedData( $node );
583    }
584
585    /**
586     * @param Node $node
587     */
588    public function visitIncludeOrEval( Node $node ): void {
589        if ( $node->flags === \ast\flags\EXEC_EVAL ) {
590            $taintValue = SecurityCheckPlugin::CODE_EXEC_TAINT;
591            $msg = 'The code supplied to `eval` is user controlled';
592        } else {
593            $taintValue = SecurityCheckPlugin::PATH_EXEC_TAINT;
594            $msg = 'The included path is user controlled';
595        }
596        $this->visitSimpleSinkAndPropagate( $node, $taintValue, $msg );
597        // Strictly speaking we have no idea if the result
598        // of an eval() or require() is safe. But given that we
599        // don't know, and at least in the require() case its
600        // fairly likely to be safe, no point in complaining.
601        $this->setCurTaintSafe();
602        $this->setCachedData( $node );
603    }
604
605    /**
606     * Also handles exit() and print
607     *
608     * We assume a web based system, where outputting HTML via echo
609     * is bad. This will have false positives in a CLI environment.
610     *
611     * @param Node $node
612     */
613    public function visitEcho( Node $node ): void {
614        $this->visitSimpleSinkAndPropagate(
615            $node,
616            SecurityCheckPlugin::HTML_EXEC_TAINT,
617            'Echoing expression that was not html escaped'
618        );
619        $this->setCurTaintSafe();
620        $this->setCachedData( $node );
621    }
622
623    /**
624     * @param Node $node
625     * @param int $sinkTaintInt
626     * @param string $issueMsg
627     */
628    private function visitSimpleSinkAndPropagate( Node $node, int $sinkTaintInt, string $issueMsg ): void {
629        if ( !isset( $node->children['expr'] ) ) {
630            return;
631        }
632        $expr = $node->children['expr'];
633        $exprTaint = $this->getTaintedness( $expr );
634
635        $sinkTaint = new Taintedness( $sinkTaintInt );
636        $rhsTaint = $exprTaint->getTaintedness();
637        $this->maybeEmitIssue(
638            $sinkTaint,
639            $rhsTaint,
640            "$issueMsg{DETAILS}",
641            [ [ 'lines' => $exprTaint->getError(), 'sink' => false ] ]
642        );
643
644        if ( $expr instanceof Node && !$rhsTaint->has( Taintedness::flagsAsExecToYesTaint( $sinkTaintInt ) ) ) {
645            $this->backpropagateArgTaint( $expr, $sinkTaint );
646        }
647    }
648
649    /**
650     * @param Node $node
651     */
652    public function visitStaticCall( Node $node ): void {
653        $this->visitMethodCall( $node );
654    }
655
656    /**
657     * @param Node $node
658     */
659    public function visitNew( Node $node ): void {
660        $ctxNode = $this->getCtxN( $node );
661        if ( !$node->children['class'] instanceof Node ) {
662            // Syntax error, don't crash
663            $this->setCurTaintSafe();
664            return;
665        }
666
667        // We check the __construct() method first, but the
668        // final resulting taint is from the __toString()
669        // method. This is a little hacky.
670        try {
671            // First do __construct()
672            $constructor = $ctxNode->getMethod(
673                '__construct',
674                false,
675                false,
676                true
677            );
678        } catch ( NodeException | CodeBaseException | IssueException $_ ) {
679            $constructor = null;
680        }
681
682        if ( $constructor ) {
683            $this->handleMethodCall(
684                $constructor,
685                $constructor->getFQSEN(),
686                $node->children['args']->children,
687                false
688            );
689        }
690
691        // Now return __toString()
692        try {
693            $clazzes = $ctxNode->getClassList(
694                false,
695                ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME,
696                null,
697                false
698            );
699        } catch ( CodeBaseException | IssueException $e ) {
700            $this->debug( __METHOD__, 'Cannot get class: ' . $this->getDebugInfo( $e ) );
701            $this->setCurTaintUnknown();
702            $this->setCachedData( $node );
703            return;
704        }
705
706        foreach ( $clazzes as $clazz ) {
707            try {
708                $toString = $clazz->getMethodByName( $this->code_base, '__toString' );
709            } catch ( CodeBaseException $_ ) {
710                // No __toString() in this class
711                continue;
712            }
713
714            $toStringTaint = $this->handleMethodCall( $toString, $toString->getFQSEN(), [] );
715            if ( !$this->curTaintWithError ) {
716                $this->curTaintWithError = $toStringTaint;
717            } else {
718                $this->curTaintWithError = $this->curTaintWithError->asMergedWith( $toStringTaint );
719            }
720        }
721
722        // If we find no __toString(), then presumably the object can't be outputted, so should be safe.
723        $this->curTaintWithError ??= TaintednessWithError::emptySingleton();
724
725        $this->setCachedData( $node );
726    }
727
728    /**
729     * @param Node $node
730     */
731    public function visitMethodCall( Node $node ): void {
732        $methodName = $node->children['method'];
733        $isStatic = $node->kind === \ast\AST_STATIC_CALL;
734        try {
735            $method = $this->getCtxN( $node )->getMethod( $methodName, $isStatic, true );
736        } catch ( NodeException | CodeBaseException | IssueException $e ) {
737            $this->debug( __METHOD__, "Cannot find method in node. " . $this->getDebugInfo( $e ) );
738            $this->setCurTaintUnknown();
739            $this->setCachedData( $node );
740            return;
741        }
742
743        $this->analyzeCallNode( $node, [ $method ] );
744    }
745
746    /**
747     * @param Node $node
748     * @param iterable<FunctionInterface> $funcs
749     */
750    protected function analyzeCallNode( Node $node, iterable $funcs ): void {
751        $args = $node->children['args']->children;
752        foreach ( $funcs as $func ) {
753            // No point in analyzing abstract function declarations
754            if ( !$func instanceof FunctionLikeDeclarationType ) {
755                $callTaint = $this->handleMethodCall( $func, $func->getFQSEN(), $args );
756                if ( !$this->curTaintWithError ) {
757                    $this->curTaintWithError = $callTaint;
758                } else {
759                    $this->curTaintWithError = $this->curTaintWithError->asMergedWith( $callTaint );
760                }
761            }
762        }
763        $this->curTaintWithError ??= TaintednessWithError::emptySingleton();
764        $this->setCachedData( $node );
765    }
766
767    /**
768     * @param Node $node
769     */
770    public function visitNullsafeMethodCall( Node $node ): void {
771        $this->visitMethodCall( $node );
772    }
773
774    /**
775     * A function call
776     *
777     * @param Node $node
778     */
779    public function visitCall( Node $node ): void {
780        $funcs = $this->getCtxN( $node->children['expr'] )->getFunctionFromNode();
781
782        if ( !$funcs ) {
783            $this->setCurTaintUnknown();
784            $this->setCachedData( $node );
785            return;
786        }
787
788        $this->analyzeCallNode( $node, $funcs );
789    }
790
791    /**
792     * A variable (e.g. $foo)
793     *
794     * This always considers superglobals as tainted
795     *
796     * @param Node $node
797     */
798    public function visitVar( Node $node ): void {
799        $varName = $this->getCtxN( $node )->getVariableName();
800        if ( $varName === '' ) {
801            // Something that phan can't understand, e.g. `$$foo` with unknown `$foo`.
802            $this->setCurTaintUnknown();
803            return;
804        }
805
806        $hardcodedTaint = $this->getHardcodedTaintednessForVar( $varName );
807        if ( $hardcodedTaint ) {
808            $this->curTaintWithError = new TaintednessWithError(
809                $hardcodedTaint,
810                CausedByLines::emptySingleton(),
811                MethodLinks::emptySingleton()
812            );
813            $this->setCachedData( $node );
814            return;
815        }
816        if ( !$this->context->getScope()->hasVariableWithName( $varName ) ) {
817            // Probably the var just isn't in scope yet.
818            // $this->debug( __METHOD__, "No var with name \$$varName in scope (Setting Unknown taint)" );
819            $this->setCurTaintUnknown();
820            $this->setCachedData( $node );
821            return;
822        }
823        $variableObj = $this->context->getScope()->getVariableByName( $varName );
824        $this->curTaintWithError = new TaintednessWithError(
825            $this->getTaintednessPhanObj( $variableObj ),
826            self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton(),
827            self::getMethodLinks( $variableObj ) ?? MethodLinks::emptySingleton()
828        );
829        $this->setCachedData( $node );
830    }
831
832    /**
833     * If we hardcode taintedness for the given var name, return that taintedness; return null otherwise.
834     * This is currently used for superglobals, since they're always tainted, regardless of whether they're in
835     * the current scope: `function foo() use ($argv)` puts $argv in the local scope, but it retains its
836     * taintedness (see test closure2).
837     *
838     * @param string $varName
839     * @return Taintedness|null
840     */
841    private function getHardcodedTaintednessForVar( string $varName ): ?Taintedness {
842        switch ( $varName ) {
843            case '_GET':
844            case '_POST':
845            case 'argc':
846            case 'argv':
847            case 'http_response_header':
848            case '_COOKIE':
849            case '_REQUEST':
850            // It's not entirely clear what $_ENV and $_SESSION should be considered
851            case '_ENV':
852            case '_SESSION':
853            // Hopefully we don't need to specify all keys for $_SERVER...
854            case '_SERVER':
855                return Taintedness::newTainted();
856            case '_FILES':
857                $elTaint = Taintedness::newFromArray( [
858                    'name' => Taintedness::newTainted(),
859                    'type' => Taintedness::newTainted(),
860                    'tmp_name' => Taintedness::safeSingleton(),
861                    'error' => Taintedness::safeSingleton(),
862                    'size' => Taintedness::safeSingleton(),
863                ] );
864                return Taintedness::safeSingleton()->withAddedKeysTaintedness( SecurityCheckPlugin::YES_TAINT )
865                    // Use 'null' as fake offset to set unknownDims
866                    ->withAddedOffsetTaintedness( null, $elTaint );
867            case 'GLOBALS':
868                // Ideally this would recurse properly, but hopefully nobody is using $GLOBALS in complex ways
869                // that wouldn't be covered by this approximation.
870
871                $filesTaintedness = $this->getHardcodedTaintednessForVar( '_FILES' );
872                assert( $filesTaintedness !== null );
873                return Taintedness::newFromArray( [
874                    '_GET' => Taintedness::newTainted(),
875                    '_POST' => Taintedness::newTainted(),
876                    '_SERVER' => Taintedness::newTainted(),
877                    '_COOKIE' => Taintedness::newTainted(),
878                    '_SESSION' => Taintedness::newTainted(),
879                    '_REQUEST' => Taintedness::newTainted(),
880                    '_ENV' => Taintedness::newTainted(),
881                    '_FILES' => $filesTaintedness,
882                    'GLOBALS' => Taintedness::newTainted()
883                ] );
884            default:
885                return null;
886        }
887    }
888
889    /**
890     * A global declaration. Assume most globals are untainted.
891     *
892     * @param Node $node
893     */
894    public function visitGlobal( Node $node ): void {
895        assert( isset( $node->children['var'] ) && $node->children['var']->kind === \ast\AST_VAR );
896
897        $varName = $node->children['var']->children['name'];
898        if ( !is_string( $varName ) || !$this->context->getScope()->hasVariableWithName( $varName ) ) {
899            // Something like global $$indirectReference; or the variable wasn't created somehow
900            return;
901        }
902        // Copy taintedness data from the actual global into the scoped clone
903        $gvar = $this->context->getScope()->getVariableByName( $varName );
904        if ( !$gvar instanceof GlobalVariable ) {
905            // Likely a superglobal, nothing to do.
906            return;
907        }
908        $actualGlobal = $gvar->getElement();
909        self::setTaintednessRaw( $gvar, self::getTaintednessRaw( $actualGlobal ) ?: Taintedness::safeSingleton() );
910        self::setCausedByRaw( $gvar, self::getCausedByRaw( $actualGlobal ) ?? CausedByLines::emptySingleton() );
911        self::setMethodLinks( $gvar, self::getMethodLinks( $actualGlobal ) ?? MethodLinks::emptySingleton() );
912    }
913
914    /**
915     * Set the taint of the function based on what's returned
916     *
917     * This attempts to match the return value up to the argument
918     * to figure out which argument might taint the function. This won't
919     * work in complex cases though.
920     *
921     * @param Node $node
922     */
923    public function visitReturn( Node $node ): void {
924        if ( !$this->context->isInFunctionLikeScope() ) {
925            // E.g. a file that can be included.
926            $this->setCurTaintUnknown();
927            $this->setCachedData( $node );
928            return;
929        }
930
931        $curFunc = $this->context->getFunctionLikeInScope( $this->code_base );
932
933        $this->setFuncTaintFromReturn( $node, $curFunc );
934
935        if ( $node->children['expr'] instanceof Node ) {
936            $collector = new ReturnObjectsCollectVisitor( $this->code_base, $this->context );
937            self::addRetObjs( $curFunc, $collector->collectFromNode( $node ) );
938        }
939    }
940
941    /**
942     * @param Node $node
943     * @param FunctionInterface $func
944     */
945    private function setFuncTaintFromReturn( Node $node, FunctionInterface $func ): void {
946        assert( $node->kind === \ast\AST_RETURN );
947        $retExpr = $node->children['expr'];
948        $retTaintednessWithError = $this->getTaintedness( $retExpr );
949        // Ensure we don't transmit any EXEC flag.
950        $retTaintedness = $retTaintednessWithError->getTaintedness()->withOnly( SecurityCheckPlugin::ALL_TAINT );
951        if ( !$retExpr instanceof Node ) {
952            assert( $retTaintedness->isSafe() );
953            $this->ensureFuncTaintIsSet( $func );
954            return;
955        }
956
957        $overallFuncTaint = $retTaintedness;
958        // Note, it's important that we only use the real type here (e.g. from typehints) and NOT
959        // the PHPDoc type, as it may be wrong.
960        $retTaintMask = $this->getTaintMaskForType( $func->getRealReturnType() );
961        if ( $retTaintMask !== null ) {
962            $overallFuncTaint = $overallFuncTaint->withOnly( $retTaintMask->get() );
963        }
964
965        $paramTaint = new FunctionTaintedness( $overallFuncTaint );
966
967        if ( $retTaintMask === null || !$retTaintMask->isSafe() ) {
968            $funcError = FunctionCausedByLines::emptySingleton();
969            $links = $retTaintednessWithError->getMethodLinks();
970            $retError = $retTaintednessWithError->getError();
971            // Note, not forCaller, as that doesn't see variadic parameters
972            $calleeParamList = $func->getParameterList();
973            foreach ( $calleeParamList as $i => $param ) {
974                $presTaint = $links->asPreservedTaintednessForFuncParam( $func, $i );
975                $paramError = $retError->asFilteredForFuncAndParam( $func, $i );
976                if ( !$presTaint->isEmpty() ) {
977                    $paramTaint = $param->isVariadic()
978                        ? $paramTaint->withVariadicParamPreservedTaint( $i, $presTaint )
979                        : $paramTaint->withParamPreservedTaint( $i, $presTaint );
980                }
981                if ( !$paramError->isEmpty() ) {
982                    if ( $param->isVariadic() ) {
983                        $funcError = $funcError->withVariadicParamPreservedLines( $i, $paramError );
984                    } else {
985                        $funcError = $funcError->withParamPreservedLines( $i, $paramError );
986                    }
987                }
988            }
989            $genericError = $retError->getLinesForGenericReturn();
990            if ( !$genericError->isEmpty() ) {
991                $funcError = $funcError->withGenericLines( $genericError );
992            }
993
994            $this->addFuncTaint( $func, $paramTaint );
995            $newFuncTaint = self::getFuncTaint( $func );
996            assert( $newFuncTaint !== null );
997
998            $this->maybeAddFuncError( $func, null, $paramTaint->withoutPreserved(), $newFuncTaint );
999            $this->mergeFuncError( $func, $funcError, $newFuncTaint );
1000            $this->maybeAddFuncError( $func, null, $paramTaint->asOnlyPreserved(), $newFuncTaint, $links );
1001        } else {
1002            $this->addFuncTaint( $func, $paramTaint );
1003        }
1004    }
1005
1006    /**
1007     * @param Node $node
1008     */
1009    public function visitArray( Node $node ): void {
1010        $curTaint = Taintedness::safeSingleton();
1011        $curError = CausedByLines::emptySingleton();
1012        $links = MethodLinks::emptySingleton();
1013        // Current numeric key in the array
1014        $curNumKey = 0;
1015        foreach ( $node->children as $child ) {
1016            if ( $child === null ) {
1017                // Happens for list( , $x ) = foo()
1018                continue;
1019            }
1020            if ( $child->kind === \ast\AST_UNPACK ) {
1021                // PHP 7.4's in-place unpacking.
1022                // TODO Do something?
1023                continue;
1024            }
1025            assert( $child->kind === \ast\AST_ARRAY_ELEM );
1026            $key = $child->children['key'];
1027            $keyTaintAll = $this->getTaintedness( $key );
1028            $keyTaint = $keyTaintAll->getTaintedness();
1029            $value = $child->children['value'];
1030            $valTaintAll = $this->getTaintedness( $value );
1031            $valTaint = $valTaintAll->getTaintedness();
1032            $sqlTaint = SecurityCheckPlugin::SQL_TAINT;
1033
1034            if (
1035                ( $keyTaint->has( $sqlTaint ) ) || (
1036                    ( $key === null || $this->nodeCanBeIntKey( $key ) )
1037                    && $valTaint->has( $sqlTaint )
1038                    && $this->nodeCanBeString( $value )
1039                )
1040            ) {
1041                $curTaint = $curTaint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT );
1042            }
1043            // FIXME This will fail with in-place spread and when some numeric keys are specified
1044            //  explicitly (at least).
1045            $offset = $key ?? $curNumKey++;
1046            $offset = $this->resolveOffset( $offset );
1047            $curTaint = $curTaint->withAddedOffsetTaintedness( $offset, $valTaint )
1048                ->withAddedKeysTaintedness( $keyTaint->get() );
1049            $valError = $valTaintAll->getError()->asAllMaybeMovedAtOffset( $offset );
1050            $keyError = $keyTaintAll->getError()->asAllMovedToKeys();
1051            $curError = $curError->asMergedWith( $keyError )
1052                ->asMergedWith( $valError );
1053            $links = $links->withKeysLinks( $keyTaintAll->getMethodLinks()->getLinksCollapsing() )
1054                ->withLinksAtDim( $offset, $valTaintAll->getMethodLinks() );
1055        }
1056        $this->curTaintWithError = new TaintednessWithError( $curTaint, $curError, $links );
1057        $this->setCachedData( $node );
1058    }
1059
1060    /**
1061     * @param Node $node
1062     */
1063    public function visitClassConst( Node $node ): void {
1064        $this->setCurTaintSafe();
1065        $this->setCachedData( $node );
1066    }
1067
1068    /**
1069     * @param Node $node
1070     */
1071    public function visitConst( Node $node ): void {
1072        // We are going to assume nobody is doing stupid stuff like
1073        // define( "foo", $_GET['bar'] );
1074        $this->setCurTaintSafe();
1075        $this->setCachedData( $node );
1076    }
1077
1078    /**
1079     * The :: operator (for props)
1080     * @param Node $node
1081     */
1082    public function visitStaticProp( Node $node ): void {
1083        $prop = $this->getPropFromNode( $node );
1084        if ( !$prop ) {
1085            $this->setCurTaintUnknown();
1086            return;
1087        }
1088        $this->curTaintWithError = new TaintednessWithError(
1089            $this->getTaintednessPhanObj( $prop ),
1090            self::getCausedByRaw( $prop ) ?? CausedByLines::emptySingleton(),
1091            self::getMethodLinks( $prop ) ?? MethodLinks::emptySingleton()
1092        );
1093        $this->setCachedData( $node );
1094    }
1095
1096    /**
1097     * The -> operator (when not a method call)
1098     * @param Node $node
1099     */
1100    public function visitProp( Node $node ): void {
1101        $nodeExpr = $node->children['expr'];
1102        if ( !$nodeExpr instanceof Node ) {
1103            // Syntax error.
1104            $this->setCurTaintSafe();
1105            return;
1106        }
1107
1108        // If the LHS expr can potentially be a stdClass, merge in its taintedness as well.
1109        // TODO Improve this (should similar to array offsets)
1110        $foundStdClass = false;
1111        $exprType = $this->getNodeType( $nodeExpr );
1112        $stdClassType = FullyQualifiedClassName::getStdClassFQSEN()->asType();
1113        if ( $exprType && $exprType->hasType( $stdClassType ) ) {
1114            $exprTaint = $this->getTaintedness( $nodeExpr );
1115            $this->curTaintWithError = new TaintednessWithError(
1116                $exprTaint->getTaintedness(),
1117                $exprTaint->getError(),
1118                // TODO Links?
1119                MethodLinks::emptySingleton()
1120            );
1121            $foundStdClass = true;
1122        }
1123
1124        $prop = $this->getPropFromNode( $node );
1125        if ( !$prop ) {
1126            if ( !$foundStdClass ) {
1127                $this->setCurTaintUnknown();
1128            }
1129            $this->setCachedData( $node );
1130            return;
1131        }
1132
1133        $objTaint = $this->getTaintednessPhanObj( $prop );
1134        $objError = self::getCausedByRaw( $prop ) ?? CausedByLines::emptySingleton();
1135        $objLinks = self::getMethodLinks( $prop ) ?? MethodLinks::emptySingleton();
1136
1137        if ( $foundStdClass ) {
1138            $newTaint = $this->curTaintWithError->getTaintedness()
1139                ->asMergedWith( $objTaint->without( SecurityCheckPlugin::UNKNOWN_TAINT ) );
1140            $this->curTaintWithError = new TaintednessWithError(
1141                $newTaint,
1142                $this->curTaintWithError->getError()->asMergedWith( $objError ),
1143                $this->curTaintWithError->getMethodLinks()->asMergedWith( $objLinks )
1144            );
1145        } else {
1146            $this->curTaintWithError = new TaintednessWithError( $objTaint, $objError, $objLinks );
1147        }
1148
1149        $this->setCachedData( $node );
1150    }
1151
1152    /**
1153     * @param Node $node
1154     */
1155    public function visitNullsafeProp( Node $node ): void {
1156        $this->visitProp( $node );
1157    }
1158
1159    /**
1160     * Ternary operator.
1161     * @param Node $node
1162     */
1163    public function visitConditional( Node $node ): void {
1164        if ( $node->children['true'] === null ) {
1165            // $foo ?: $bar;
1166            $trueTaint = $this->getTaintedness( $node->children['cond'] );
1167        } else {
1168            $trueTaint = $this->getTaintedness( $node->children['true'] );
1169        }
1170        $falseTaint = $this->getTaintedness( $node->children['false'] );
1171        $this->curTaintWithError = $trueTaint->asMergedWith( $falseTaint );
1172        $this->setCachedData( $node );
1173    }
1174
1175    /**
1176     * @param Node $node
1177     */
1178    public function visitName( Node $node ): void {
1179        $this->setCurTaintSafe();
1180    }
1181
1182    /**
1183     * This is e.g. for class X implements Name,List
1184     *
1185     * @param Node $node
1186     */
1187    public function visitNameList( Node $node ): void {
1188        $this->setCurTaintSafe();
1189    }
1190
1191    /**
1192     * @param Node $node
1193     */
1194    public function visitUnaryOp( Node $node ): void {
1195        // ~ and @ are the only two unary ops
1196        // that can preserve taint (others cast bool or int)
1197        $unsafe = [
1198            \ast\flags\UNARY_BITWISE_NOT,
1199            \ast\flags\UNARY_SILENCE
1200        ];
1201        if ( in_array( $node->flags, $unsafe, true ) ) {
1202            $this->curTaintWithError = $this->getTaintedness( $node->children['expr'] );
1203        } else {
1204            $this->setCurTaintSafe();
1205        }
1206        $this->setCachedData( $node );
1207    }
1208
1209    /**
1210     * @param Node $node
1211     */
1212    public function visitPostInc( Node $node ): void {
1213        $this->analyzeIncOrDec( $node );
1214    }
1215
1216    /**
1217     * @param Node $node
1218     */
1219    public function visitPreInc( Node $node ): void {
1220        $this->analyzeIncOrDec( $node );
1221    }
1222
1223    /**
1224     * @param Node $node
1225     */
1226    public function visitPostDec( Node $node ): void {
1227        $this->analyzeIncOrDec( $node );
1228    }
1229
1230    /**
1231     * @param Node $node
1232     */
1233    public function visitPreDec( Node $node ): void {
1234        $this->analyzeIncOrDec( $node );
1235    }
1236
1237    /**
1238     * Handles all post/pre-increment/decrement operators. They have no effect on the
1239     * taintedness of a variable.
1240     *
1241     * @param Node $node
1242     */
1243    private function analyzeIncOrDec( Node $node ): void {
1244        $this->curTaintWithError = $this->getTaintedness( $node->children['var'] );
1245        $this->setCachedData( $node );
1246    }
1247
1248    /**
1249     * @param Node $node
1250     */
1251    public function visitCast( Node $node ): void {
1252        // Casting between an array and object maintains
1253        // taint. Casting an object to a string calls __toString().
1254        // Future TODO: handle the string case properly.
1255        $dangerousCasts = [
1256            \ast\flags\TYPE_STRING,
1257            \ast\flags\TYPE_ARRAY,
1258            \ast\flags\TYPE_OBJECT
1259        ];
1260
1261        if ( !in_array( $node->flags, $dangerousCasts, true ) ) {
1262            $this->setCurTaintSafe();
1263        } else {
1264            $exprTaint = $this->getTaintedness( $node->children['expr'] );
1265            // Note, casting deletes shapes.
1266            $this->curTaintWithError = new TaintednessWithError(
1267                $exprTaint->getTaintedness()->asCollapsed(),
1268                $exprTaint->getError(),
1269                $exprTaint->getMethodLinks()->asCollapsed()
1270            );
1271        }
1272        $this->setCachedData( $node );
1273    }
1274
1275    /**
1276     * The taint is the taint of all the child elements
1277     *
1278     * @param Node $node
1279     */
1280    public function visitEncapsList( Node $node ): void {
1281        $this->curTaintWithError = TaintednessWithError::emptySingleton();
1282        foreach ( $node->children as $child ) {
1283            $this->curTaintWithError = $this->curTaintWithError->asMergedWith( $this->getTaintedness( $child ) );
1284        }
1285        $this->setCachedData( $node );
1286    }
1287
1288    /**
1289     * Visit a node that is always safe
1290     *
1291     * @param Node $node
1292     */
1293    public function visitIsset( Node $node ): void {
1294        $this->setCurTaintSafe();
1295    }
1296
1297    /**
1298     * Visits calls to empty(), which is always safe
1299     *
1300     * @param Node $node
1301     */
1302    public function visitEmpty( Node $node ): void {
1303        $this->setCurTaintSafe();
1304    }
1305
1306    /**
1307     * Visit a node that is always safe
1308     *
1309     * @param Node $node
1310     */
1311    public function visitMagicConst( Node $node ): void {
1312        $this->setCurTaintSafe();
1313    }
1314
1315    /**
1316     * Visit a node that is always safe
1317     *
1318     * @param Node $node
1319     */
1320    public function visitInstanceOf( Node $node ): void {
1321        $this->setCurTaintSafe();
1322    }
1323
1324    /**
1325     * @param Node $node
1326     */
1327    public function visitMatch( Node $node ): void {
1328        $this->curTaintWithError = TaintednessWithError::emptySingleton();
1329        // Based on UnionTypeVisitor
1330        foreach ( $node->children['stmts']->children as $armNode ) {
1331            // It sounds a bit weird to have to call this ourselves, but aight.
1332            if ( !BlockExitStatusChecker::willUnconditionallyThrowOrReturn( $armNode ) ) {
1333                // Note, we're straight using the expr to avoid implementing visitMatchArm
1334                $this->curTaintWithError = $this->curTaintWithError
1335                    ->asMergedWith( $this->getTaintedness( $armNode->children['expr'] ) );
1336            }
1337        }
1338
1339        $this->setCachedData( $node );
1340    }
1341}