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