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