Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 1440
0.00% covered (danger)
0.00%
0 / 60
CRAP
0.00% covered (danger)
0.00%
0 / 1
TaintednessBaseVisitor
0.00% covered (danger)
0.00%
0 / 1440
0.00% covered (danger)
0.00%
0 / 60
261632
0.00% covered (danger)
0.00%
0 / 1
 addFuncTaint
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 ensureFuncTaintIsSet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 maybeAddFuncError
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
506
 mergeFuncError
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addTaintError
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getCausedByLinesToAdd
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
56
 ensureTaintednessIsSet
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 setTaintedness
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getDefiningFuncIfDifferent
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getPossibleFuncDefinitions
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 getTaintOfFunction
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
132
 getSetKnownTaintOfFunctionWithoutAnalysis
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 analyzeFunc
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
110
 getDocBlockTaintOfFunc
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 1
506
 getTaintByType
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
1122
 getTaintMaskForTypedElement
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTaintMaskForType
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getPossibleFutureTaintOfElement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCurrentMethod
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getTaintedness
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getTaintednessNode
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getTaintednessPhanObj
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 resolveOffset
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 resolveValue
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getPropInCurrentScopeByName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getCtxN
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getObjsForNodeForNumkeyBackprop
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 1
2970
 getPropFromNode
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getDebugInfo
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 dbgInfo
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 linkParamAndFunc
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 mergeTaintDependencies
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 markAllDependentMethodsExec
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
182
 markAllDependentMethodsExecForNode
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 markAllDependentVarsYes
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 getCausedByLinesForFunc
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getActualFuncWithCausedBy
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 debug
n/a
0 / 0
n/a
0 / 0
8
 getCallableFromNode
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
272
 getFirstElmFromArrayOrGenerator
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 taintToIssuesAndSeverities
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
132
 maybeEmitIssueSimplified
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 maybeEmitIssue
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
210
 isIssueSuppressedOrFalsePositive
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 handleMethodCall
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 1
600
 maybeHandleSpecialCall
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
306
 extractArrayArgs
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 translateNamedArg
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 backpropagateArgTaint
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 handlePassByRef
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
210
 getPassByRefObjFromNode
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 getHardcodedPreservedTaintForFunc
0.00% covered (danger)
0.00%
0 / 312
0.00% covered (danger)
0.00%
0 / 1
7140
 getBinOpTaintMask
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
42
 getNodeType
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 nodeIsArray
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 nodeCanBeArray
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 nodeCanBeString
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 elementCanBeNumkey
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 nodeCanBeIntKey
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 getReturnObjsOfFunc
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 isSubclassOf
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php declare( strict_types=1 );
2
3/**
4 * Copyright (C) 2017  Brian Wolff <bawolff@gmail.com>
5 *
6 * @license GPL-2.0-or-later
7 */
8
9namespace SecurityCheckPlugin;
10
11use ast\Node;
12use Closure;
13use Exception;
14use Generator;
15use Phan\AST\ASTReverter;
16use Phan\AST\ContextNode;
17use Phan\AST\UnionTypeVisitor;
18use Phan\BlockAnalysisVisitor;
19use Phan\CodeBase;
20use Phan\Debug;
21use Phan\Exception\CodeBaseException;
22use Phan\Exception\FQSENException;
23use Phan\Exception\IssueException;
24use Phan\Exception\NodeException;
25use Phan\Exception\UnanalyzableException;
26use Phan\Issue;
27use Phan\Language\Context;
28use Phan\Language\Element\FunctionInterface;
29use Phan\Language\Element\GlobalVariable;
30use Phan\Language\Element\Method;
31use Phan\Language\Element\Property;
32use Phan\Language\Element\TypedElementInterface;
33use Phan\Language\Element\Variable;
34use Phan\Language\FQSEN\FullyQualifiedClassName;
35use Phan\Language\FQSEN\FullyQualifiedFunctionLikeName;
36use Phan\Language\FQSEN\FullyQualifiedFunctionName;
37use Phan\Language\FQSEN\FullyQualifiedMethodName;
38use Phan\Language\Type\FunctionLikeDeclarationType;
39use Phan\Language\Type\GenericArrayType;
40use Phan\Language\Type\LiteralTypeInterface;
41use Phan\Language\UnionType;
42use RuntimeException;
43use SplObjectStorage;
44use const ast\AST_CALL;
45use const ast\AST_CALLABLE_CONVERT;
46use const ast\AST_METHOD_CALL;
47use const ast\AST_STATIC_CALL;
48
49/**
50 * Trait for the Tainedness visitor subclasses. Mostly contains
51 * utility methods.
52 *
53 * @property-read Context $context
54 * @property-read \Phan\CodeBase $code_base
55 */
56trait TaintednessBaseVisitor {
57    use TaintednessAccessorsTrait;
58
59    /** @var null|string|bool|resource filehandle to output debug messages */
60    private $debugOutput;
61
62    /** @var Context|null Override the file/line number to emit issues */
63    protected $overrideContext;
64
65    /**
66     * @var bool[] FQSENs of classes without __toString, map of [ (string)FQSEN => true ]
67     */
68    public static $fqsensWithoutToStringCache = [];
69
70    /**
71     * @var array<string,bool> FQSENs of functions currently being analyzed by us, map of [ (string)FQSEN => true ]
72     */
73    private static $funcAnalysisStack = [];
74
75    /**
76     * Merge taintedness of a function/method
77     */
78    protected function addFuncTaint( FunctionInterface $func, FunctionTaintedness $taint ): void {
79        $curTaint = self::getFuncTaint( $func );
80        if ( $curTaint ) {
81            $newTaint = $curTaint->asMergedWith( $taint );
82        } else {
83            $newTaint = $taint;
84        }
85        self::doSetFuncTaint( $func, $newTaint );
86    }
87
88    /**
89     * Ensure a function-like has its taintedness set and not unknown
90     */
91    protected function ensureFuncTaintIsSet( FunctionInterface $func ): void {
92        if ( !self::getFuncTaint( $func ) ) {
93            self::doSetFuncTaint( $func, FunctionTaintedness::emptySingleton() );
94        }
95    }
96
97    /**
98     * @param FunctionInterface $func
99     * @param Context|string|null $reason To override the caused-by line
100     * @param FunctionTaintedness $addedTaint
101     * @param FunctionTaintedness $allNewTaint
102     * @param MethodLinks|null $returnLinks NOTE: These are only used for preserved params, since for sink params
103     * we're already adding a Taintedness with the expected EXEC bits.
104     */
105    private function maybeAddFuncError(
106        FunctionInterface $func,
107        Context|string|null $reason,
108        FunctionTaintedness $addedTaint,
109        FunctionTaintedness $allNewTaint,
110        ?MethodLinks $returnLinks = null
111    ): void {
112        if ( !is_string( $reason ) ) {
113            $newErrors = [ $this->dbgInfo( $reason ?? $this->context ) ];
114        } else {
115            $newErrors = [ $reason ];
116        }
117        if ( $this->overrideContext && !( $this->isHook ?? false ) ) {
118            // @phan-suppress-previous-line PhanUndeclaredProperty
119            $newErrors[] = $this->dbgInfo( $this->overrideContext );
120        }
121
122        $hasReturnLinks = $returnLinks && !$returnLinks->isEmpty();
123
124        // Future TODO: we might consider using PreservedTaintedness from the funcs instead of MethodLinks, but using
125        // links is more consistent with what we do for non-function causedby lines.
126
127        $newErr = self::getFuncCausedByRaw( $func ) ?? FunctionCausedByLines::emptySingleton();
128
129        foreach ( $addedTaint->getSinkParamKeysNoVariadic() as $key ) {
130            if ( $reason || $allNewTaint->canOverrideNonVariadicParam( $key ) ) {
131                $curTaint = $addedTaint->getParamSinkTaint( $key );
132                if ( $curTaint->has( SecurityCheckPlugin::ALL_EXEC_TAINT ) ) {
133                    $newErr = $newErr->withAddedParamSinkLines( $key, $newErrors, $curTaint->asExecToYesTaint() );
134                }
135            }
136        }
137        foreach ( $addedTaint->getPreserveParamKeysNoVariadic() as $key ) {
138            if ( $hasReturnLinks && ( $reason || $allNewTaint->canOverrideNonVariadicParam( $key ) ) ) {
139                $newErr = $newErr->withAddedParamPreservedLines(
140                    $key,
141                    $newErrors,
142                    Taintedness::safeSingleton(),
143                    $returnLinks->asFilteredForFuncAndParam( $func, $key )
144                );
145            }
146        }
147        $variadicIndex = $addedTaint->getVariadicParamIndex();
148        if ( $variadicIndex !== null && ( $reason || $allNewTaint->canOverrideVariadicParam() ) ) {
149            $sinkVariadic = $addedTaint->getVariadicParamSinkTaint();
150            if ( $sinkVariadic && $sinkVariadic->has( SecurityCheckPlugin::ALL_EXEC_TAINT ) ) {
151                $newErr = $newErr->withAddedVariadicParamSinkLines(
152                    $variadicIndex,
153                    $newErrors,
154                    $sinkVariadic->asExecToYesTaint()
155                );
156            }
157            if ( $hasReturnLinks ) {
158                $newErr = $newErr->withAddedVariadicParamPreservedLines(
159                    $variadicIndex,
160                    $newErrors,
161                    Taintedness::safeSingleton(),
162                    $returnLinks->asFilteredForFuncAndParam( $func, $variadicIndex )
163                );
164            }
165        }
166
167        $curTaint = $addedTaint->getOverall();
168        if ( ( $reason || $allNewTaint->canOverrideOverall() ) && $curTaint->has( SecurityCheckPlugin::ALL_TAINT ) ) {
169            // Note, the generic error shouldn't have any link
170            $newErr = $newErr->withAddedGenericLines( $newErrors, $curTaint );
171        }
172
173        self::setFuncCausedByRaw( $func, $newErr );
174    }
175
176    /**
177     * @param FunctionInterface $func
178     * @param FunctionCausedByLines $newError
179     * @param FunctionTaintedness $allFuncTaint Used to check NO_OVERRIDE
180     */
181    protected function mergeFuncError(
182        FunctionInterface $func,
183        FunctionCausedByLines $newError,
184        FunctionTaintedness $allFuncTaint
185    ): void {
186        $curError = self::getFuncCausedByRaw( $func ) ?? FunctionCausedByLines::emptySingleton();
187        self::setFuncCausedByRaw( $func, $curError->asMergedWith( $newError, $allFuncTaint ) );
188    }
189
190    /**
191     * Shortcut to add all possible caused-by lines for the current context to the given variable-like element.
192     *
193     * @param TypedElementInterface $elem Where to put it
194     * @param Taintedness $taintedness
195     * @param MethodLinks|null $links
196     * @param string|null $reason To override the caused by line
197     */
198    protected function addTaintError(
199        TypedElementInterface $elem,
200        Taintedness $taintedness,
201        ?MethodLinks $links,
202        ?string $reason = null
203    ): void {
204        assert( !$elem instanceof FunctionInterface, 'Should use addFuncTaintError' );
205        $newErrors = $this->getCausedByLinesToAdd( $taintedness, $links, $reason );
206        $curErr = self::getCausedByRaw( $elem ) ?? CausedByLines::emptySingleton();
207        self::setCausedByRaw( $elem, $curErr->withAddedLines( $newErrors, $taintedness, $links ) );
208    }
209
210    /**
211     * @param Taintedness $taintedness
212     * @param MethodLinks|null $links
213     * @param string|null $reason To override the caused by line
214     * @return string[]
215     */
216    private function getCausedByLinesToAdd(
217        Taintedness $taintedness,
218        ?MethodLinks $links,
219        ?string $reason = null
220    ): array {
221        if ( !$taintedness->has( SecurityCheckPlugin::ALL_TAINT ) && ( !$links || $links->isEmpty() ) ) {
222            // Don't add book-keeping if no actual taint was added.
223            return [];
224        }
225
226        $newErrors = $reason !== null ? [ $reason ] : [ $this->dbgInfo() ];
227        if ( $this->overrideContext && !( $this->isHook ?? false ) ) {
228            // @phan-suppress-previous-line PhanUndeclaredProperty
229            $newErrors[] = $this->dbgInfo( $this->overrideContext );
230        }
231        return $newErrors;
232    }
233
234    /**
235     * Ensures that the given variable obj has some taintedness set, initializing to safe if it doesn't.
236     */
237    protected function ensureTaintednessIsSet( TypedElementInterface $varObj ): void {
238        if ( !self::getTaintednessRaw( $varObj ) ) {
239            self::setTaintednessRaw( $varObj, Taintedness::safeSingleton() );
240        }
241        if ( $varObj instanceof GlobalVariable ) {
242            $gVarObj = $varObj->getElement();
243            if ( !self::getTaintednessRaw( $gVarObj ) ) {
244                self::setTaintednessRaw( $gVarObj, Taintedness::safeSingleton() );
245            }
246        }
247    }
248
249    /**
250     * Change the taintedness of $variableObj.
251     */
252    private function setTaintedness(
253        TypedElementInterface $variableObj,
254        Taintedness $taintedness,
255        bool $override
256    ): void {
257        assert( !$variableObj instanceof FunctionInterface, 'Must use setFuncTaint for functions' );
258
259        if (
260            $variableObj instanceof Property &&
261            $variableObj->getClassFQSEN() === FullyQualifiedClassName::getStdClassFQSEN()
262        ) {
263            // Phan conflates all stdClass props, see https://github.com/phan/phan/issues/3869
264            // Avoid doing the same with taintedness, as that would cause weird issues (see
265            // 'stdclassconflation' test).
266            // TODO Is it possible to store prop taintedness in the Variable object?
267            // that would be similar to a fine-grained handling of arrays.
268            return;
269        }
270
271        if ( $override ) {
272            $newTaint = $taintedness;
273        } else {
274            $curTaint = self::getTaintednessRaw( $variableObj );
275            if ( !$curTaint ) {
276                $newTaint = $taintedness;
277            } else {
278                // NOTE: Do NOT merge in place here, as that would change the taintedness for all variable
279                // objects of which $variableObj is a clone!
280                $newTaint = $curTaint->asMergedWith( $taintedness );
281            }
282        }
283        self::setTaintednessRaw( $variableObj, $newTaint );
284    }
285
286    /**
287     * Given a func, if it has a defining func different from itself, return that defining func. Returns null otherwise.
288     */
289    private function getDefiningFuncIfDifferent( FunctionInterface $func ): ?FunctionInterface {
290        if ( $func instanceof Method && $func->hasDefiningFQSEN() ) {
291            $definingFQSEN = $func->getDefiningFQSEN();
292            if ( $definingFQSEN !== $func->getFQSEN() ) {
293                return $this->code_base->getMethodByFQSEN( $definingFQSEN );
294            }
295        }
296        return null;
297    }
298
299    /**
300     * Get a list of places to look for function taint info
301     *
302     * @todo How to handle multiple function definitions (phan "alternates")
303     * @return Generator<FunctionInterface>
304     */
305    private function getPossibleFuncDefinitions( FunctionInterface $func ): Generator {
306        yield $func;
307
308        // If we don't have a defining func, stay with the same func.
309        // definingFunc is used later on during fallback processing.
310        $definingFunc = $this->getDefiningFuncIfDifferent( $func );
311        if ( $definingFunc ) {
312            yield $definingFunc;
313        }
314        if ( $func instanceof Method ) {
315            try {
316                $class = $func->getClass( $this->code_base );
317            } catch ( CodeBaseException $e ) {
318                $this->debug( __METHOD__, "Class not found for func $func" . $this->getDebugInfo( $e ) );
319                return;
320            }
321
322            // Iterate through the whole hierarchy to see if the method was defined in an interface or trait. A few
323            // notes on this:
324            // - getNonParentAncestorFQSENList (and similar methods in Class and Method) only go one level up, and
325            //   would not give us e.g. the interfaces implemented by the parent class.
326            // - asExpandedTypes would work, but it has a non-zero overhead, and most importantly, we would cause phan
327            //   to emit issues like RedefinedClass in places where phan wouldn't normally emit them.
328            // - It's unclear whether this code should also look for method definitions in classes (and not just
329            //   interfaces/traits). And more generally, what would the expectations for *-taint annotations be.
330            $curClass = $class;
331            // Use a safeguard in case this goes out of control (e.g., broken code with circular inheritance).
332            $depth = 0;
333            do {
334                $depth++;
335                $nonParents = $curClass->getNonParentAncestorFQSENList();
336
337                foreach ( $nonParents as $nonParentFQSEN ) {
338                    if ( $this->code_base->hasClassWithFQSEN( $nonParentFQSEN ) ) {
339                        $nonParent = $this->code_base->getClassByFQSEN( $nonParentFQSEN );
340                        // TODO Assuming this is a direct invocation, but it doesn't always make sense
341                        $directInvocation = true;
342                        if ( $nonParent->hasMethodWithName( $this->code_base, $func->getName(), $directInvocation ) ) {
343                            try {
344                                yield $nonParent->getMethodByName( $this->code_base, $func->getName() );
345                            } catch ( CodeBaseException $e ) {
346                                $this->debug(
347                                    __METHOD__,
348                                    "Can't find func definition for $func in $nonParent" . $this->getDebugInfo( $e )
349                                );
350                            }
351                        }
352                    }
353                }
354                if (
355                    !$curClass->hasParentType() ||
356                    !$this->code_base->hasClassWithFQSEN( $curClass->getParentClassFQSEN() )
357                ) {
358                    break;
359                }
360                $curClass = $curClass->getParentClass( $this->code_base );
361            } while ( $depth < 20 );
362        }
363    }
364
365    /**
366     * This is also for methods and other function like things
367     * @note This is not guaranteed to return a clone
368     *
369     * @param FunctionInterface $func What function/method to look up
370     * @return FunctionTaintedness Always a clone
371     */
372    protected function getTaintOfFunction( FunctionInterface $func ): FunctionTaintedness {
373        $funcTaint = self::getFuncTaint( $func );
374        if ( $funcTaint !== null ) {
375            return $funcTaint;
376        }
377
378        $annotatedTaint = $this->getSetKnownTaintOfFunctionWithoutAnalysis( $func );
379        if ( $annotatedTaint ) {
380            return $annotatedTaint;
381        }
382
383        $fqsen = $func->getFQSEN();
384        if (
385            isset( self::$funcAnalysisStack[$fqsen->__toString()] ) ||
386            ( $this->context->isInFunctionLikeScope() && $fqsen === $this->context->getFunctionLikeFQSEN() )
387        ) {
388            // Recursive function(s). Analyzing it again isn't useful. Provisionally mark the function as safe, the idea
389            // being that anything dangerous will be added as it's found. Failing to do this would mark the function as
390            // inconditionally preserving all taintedness, as we'd look at its return type only. `--analyze-twice` gives
391            // more accurate results here, since it will analyze the function again once its taintedness (except that
392            // coming from the recursive call) is known.
393            $taint = FunctionTaintedness::emptySingleton();
394            self::doSetFuncTaint( $func, $taint );
395            return $taint;
396        }
397
398        $isPHPInternalFunc = $func->isPHPInternal();
399        if ( !$isPHPInternalFunc ) {
400            // PHP internal functions cannot be analyzed because they don't have a body.
401            $funcToAnalyze = $this->getDefiningFuncIfDifferent( $func ) ?: $func;
402            $this->analyzeFunc( $funcToAnalyze );
403            $analyzedFuncTaint = self::getFuncTaint( $funcToAnalyze );
404            if ( $analyzedFuncTaint !== null ) {
405                return $analyzedFuncTaint;
406            }
407        }
408
409        $taintFromReturnType = $this->getTaintByType( $func->getUnionType() );
410        if ( !$isPHPInternalFunc ) {
411            // If we haven't seen this function before, first of all check the return type. If it
412            // returns a safe type (like int), it's safe.
413            // TODO Should we remove UNKNOWN_TAINT here? Strictly speaking it *is* unknown, but that's treated as if
414            // it were unsafe. This can be seen, for example, when the maximum analysis depth is reached.
415            $taint = new FunctionTaintedness( $taintFromReturnType );
416            self::doSetFuncTaint( $func, $taint );
417            $this->maybeAddFuncError( $func, null, $taint, $taint );
418        } else {
419            // Assume that anything really dangerous we've already hardcoded. So just preserve taint.
420            $overall = $taintFromReturnType->isSafe()
421                ? $taintFromReturnType
422                : new Taintedness( SecurityCheckPlugin::PRESERVE_TAINT );
423            $taint = new FunctionTaintedness( $overall );
424            // We're not adding any error here, since it's presumably unnecessary for PHP internal stuff.
425            self::doSetFuncTaint( $func, $taint );
426        }
427        return $taint;
428    }
429
430    /**
431     * Given a function, find out if it has any hardcoded/annotated taint, or whether it should inherit its taint
432     * from an alternate definition. If anything was found, set that taintedness in the func object and return it.
433     * In particular, this does NOT cause $func to be analyzed.
434     */
435    private function getSetKnownTaintOfFunctionWithoutAnalysis( FunctionInterface $func ): ?FunctionTaintedness {
436        $funcsToTry = $this->getPossibleFuncDefinitions( $func );
437        foreach ( $funcsToTry as $trialFunc ) {
438            /** @var FunctionInterface $trialFunc */
439            if ( !$trialFunc->isPHPInternal() ) {
440                // PHP internal functions can't have a docblock.
441                $taintData = $this->getDocBlockTaintOfFunc( $trialFunc );
442                if ( $taintData !== null ) {
443                    [ $taint, $methodLinks ] = $taintData;
444                    self::doSetFuncTaint( $func, $taint );
445                    // TODO Make this more granular if possible
446                    $errorDesc = 'annotations in ' . $trialFunc->getFQSEN()->__toString();
447                    $this->maybeAddFuncError( $func, $errorDesc, $taint, $taint, $methodLinks );
448                    return $taint;
449                }
450            }
451
452            $trialFuncName = $trialFunc->getFQSEN();
453            $taint = SecurityCheckPlugin::$pluginInstance->getBuiltinFuncTaint( $trialFuncName );
454            if ( $taint !== null ) {
455                self::doSetFuncTaint( $func, $taint );
456                if ( !$func->isPHPInternal() ) {
457                    // Caused-by lines are presumably unnecessary for PHP internal stuff.
458                    $this->maybeAddFuncError( $func, "Builtin-$trialFuncName", $taint, $taint );
459                }
460                return $taint;
461            }
462        }
463
464        $definingFunc = $this->getDefiningFuncIfDifferent( $func );
465        if ( $definingFunc ) {
466            $definingFuncTaint = self::getFuncTaint( $definingFunc );
467            if ( $definingFuncTaint !== null ) {
468                return $definingFuncTaint;
469            }
470        }
471
472        return null;
473    }
474
475    /**
476     * Analyze a function. This is very similar to Analyzable::analyze, but avoids several checks
477     * used by phan for performance. Phan doesn't know about taintedness, so it may decide to skip
478     * a re-analysis which we need.
479     * @todo This is a bit hacky.
480     * @todo We should implement our own perf checks, e.g. if the method as already called with
481     * the same taintedness, taint links, etc. for all params.
482     * @see \Phan\Analysis\Analyzable::analyze()
483     */
484    public function analyzeFunc( FunctionInterface $func ): void {
485        $fqsenStr = $func->getFQSEN()->__toString();
486        if ( isset( self::$funcAnalysisStack[$fqsenStr] ) ) {
487            // Bail out immediately if this function is already being analyzed.
488            return;
489        }
490
491        $node = $func->getNode();
492        if ( !$node ) {
493            return;
494        }
495
496        if ( $this->context->isInFunctionLikeScope() && $func->getFQSEN() === $this->context->getFunctionLikeFQSEN() ) {
497            // Avoid pointless recursion
498            return;
499        }
500
501        // @todo Tune the max depth. Raw benchmarking shows very little difference between e.g.
502        // 5 and 10. However, while with higher values we can detect more issues and avoid more
503        // false positives, it becomes harder to tell where an issue is coming from.
504        // Thus, this value should be increased only when we'll have better error reporting.
505        $maxDepth = 5;
506        if ( count( self::$funcAnalysisStack ) > $maxDepth ) {
507            // $this->debug( __METHOD__, 'WARNING: aborting analysis earlier due to max depth' );
508            return;
509        }
510        if ( $node->kind === \ast\AST_CLOSURE && isset( $node->children['uses'] ) ) {
511            return;
512        }
513
514        self::$funcAnalysisStack[$fqsenStr] = true;
515        // Like Analyzable::analyze, clone the context to avoid overriding anything
516        $context = clone $func->getContext();
517        // @phan-suppress-next-line PhanUndeclaredMethod All implementations have it
518        if ( $func->getRecursionDepth() !== 0 ) {
519            // Add the arguments types to the internal scope of the function, see
520            // https://github.com/phan/phan/issues/3848
521            foreach ( $func->getParameterList() as $parameter ) {
522                $context->addScopeVariable( $parameter->cloneAsNonVariadic() );
523            }
524        }
525        try {
526            ( new BlockAnalysisVisitor( $this->code_base, $context ) )(
527                $node
528            );
529        } finally {
530            unset( self::$funcAnalysisStack[$fqsenStr] );
531        }
532    }
533
534    /**
535     * Obtain taint information from a docblock comment.
536     *
537     * @param FunctionInterface $func The function to check
538     * @return array<FunctionTaintedness|MethodLinks>|null null for no info
539     * @phan-return array{0:FunctionTaintedness,1:MethodLinks}|null
540     */
541    protected function getDocBlockTaintOfFunc( FunctionInterface $func ): ?array {
542        // Note that we're not using the hashed docblock for caching, because the same docblock
543        // may have different meanings in different contexts. E.g. @return self
544        $fqsen = (string)$func->getFQSEN();
545        if ( isset( SecurityCheckPlugin::$docblockCache[ $fqsen ] ) ) {
546            return SecurityCheckPlugin::$docblockCache[ $fqsen ];
547        }
548
549        $docBlock = $func->getDocComment();
550        if ( $docBlock === null ) {
551            return null;
552        }
553        if ( !str_contains( $docBlock, '-taint' ) ) {
554            // Lightweight check for methods that certainly aren't annotated
555            return null;
556        }
557        $lines = explode( "\n", $docBlock );
558        /** @param string[] $args */
559        $invalidLineIssueEmitter = function ( string $msg, array $args ) use ( $func ): void {
560            SecurityCheckPlugin::emitIssue(
561                $this->code_base,
562                // Emit issues at the line of the signature
563                $func->getContext(),
564                'SecurityCheckInvalidAnnotation',
565                $msg,
566                $args
567            );
568        };
569        // Note, not forCaller, as that doesn't see variadic parameters
570        $calleeParamList = $func->getParameterList();
571        $validTaintEncountered = false;
572        // Assume that if some of the taint is specified, then
573        // the person would specify all the dangerous taints, so
574        // don't set the unknown flag if not taint annotation on
575        // @return.
576        $funcTaint = FunctionTaintedness::emptySingleton();
577        // TODO $fakeMethodLinks here is a bit hacky...
578        $fakeMethodLinks = MethodLinks::emptySingleton();
579        foreach ( $lines as $line ) {
580            $m = [];
581            $trimmedLine = ltrim( rtrim( $line ), "* \t/" );
582            if ( str_starts_with( $trimmedLine, '@param-taint' ) ) {
583                $matched = preg_match( SecurityCheckPlugin::PARAM_ANNOTATION_REGEX, $trimmedLine, $m );
584                if ( !$matched ) {
585                    $invalidLineIssueEmitter( "Cannot parse taint line '{COMMENT}'", [ $trimmedLine ] );
586                    continue;
587                }
588
589                $paramNumber = null;
590                $isVariadic = null;
591                foreach ( $calleeParamList as $i => $param ) {
592                    if ( $m['paramname'] === $param->getName() ) {
593                        $paramNumber = $i;
594                        $isVariadic = $param->isVariadic();
595                        break;
596                    }
597                }
598                if ( $paramNumber === null || $isVariadic === null ) {
599                    $invalidLineIssueEmitter(
600                        'Annotated parameter ${PARAMETER} not found in the signature',
601                        [ $m['paramname'] ]
602                    );
603                    continue;
604                }
605
606                $annotatedAsVariadic = $m['variadic'] !== '';
607                if ( $isVariadic !== $annotatedAsVariadic ) {
608                    $msg = $isVariadic
609                        ? 'Variadic parameter ${PARAMETER} should be annotated as `...${PARAMETER}`'
610                        : 'Non-variadic parameter ${PARAMETER} should be annotated as `${PARAMETER}`';
611                    $invalidLineIssueEmitter( $msg, [ $m['paramname'], $m['paramname'] ] );
612                }
613                $taintData = SecurityCheckPlugin::parseTaintLine( $m['taint'] );
614                if ( $taintData === null ) {
615                    $invalidLineIssueEmitter( "Invalid param taintedness '{COMMENT}'", [ $m['taint'] ] );
616                    continue;
617                }
618                /** @var Taintedness $taint */
619                [ $taint, $flags ] = $taintData;
620                $sinkTaint = $taint->withOnly( SecurityCheckPlugin::ALL_EXEC_TAINT );
621                $allTaint = $taint->without( SecurityCheckPlugin::ALL_EXEC_TAINT );
622                $preserveTaint = $allTaint->asPreservedTaintedness();
623                if ( $isVariadic ) {
624                    $funcTaint = $funcTaint->withVariadicParamSinkTaint( $paramNumber, $sinkTaint )
625                        ->withVariadicParamPreservedTaint( $paramNumber, $preserveTaint, $flags );
626                } else {
627                    $funcTaint = $funcTaint->withParamSinkTaint( $paramNumber, $sinkTaint )
628                        ->withParamPreservedTaint( $paramNumber, $preserveTaint, $flags );
629                }
630                $preservedFlags = $allTaint->get();
631                if ( $preservedFlags !== SecurityCheckPlugin::NO_TAINT ) {
632                    $fakeMethodLinks = $fakeMethodLinks->withFuncAndParam(
633                        $func,
634                        $paramNumber,
635                        $isVariadic,
636                        $preservedFlags
637                    );
638                }
639                $validTaintEncountered = true;
640                if ( ( $taint->get() & SecurityCheckPlugin::ESCAPES_HTML ) === SecurityCheckPlugin::ESCAPES_HTML ) {
641                    // Special case to auto-set anything that escapes html to detect double escaping.
642                    $funcTaint = $funcTaint->withOverall(
643                        $funcTaint->getOverall()->with( SecurityCheckPlugin::ESCAPED_TAINT )
644                    );
645                }
646            } elseif ( str_starts_with( $trimmedLine, '@return-taint' ) ) {
647                $taintLine = substr( $trimmedLine, strlen( '@return-taint' ) + 1 );
648                $taintData = SecurityCheckPlugin::parseTaintLine( $taintLine );
649                if ( $taintData === null ) {
650                    $invalidLineIssueEmitter( "Invalid return taintedness '{COMMENT}'", [ $taintLine ] );
651                    continue;
652                }
653                /** @var Taintedness $taint */
654                [ $taint, $flags ] = $taintData;
655                if ( $taint->has( SecurityCheckPlugin::ALL_EXEC_TAINT ) ) {
656                    $invalidLineIssueEmitter( "Return taintedness cannot be exec", [] );
657                    continue;
658                }
659                $funcTaint = $funcTaint->withOverall( $taint, $flags );
660                $validTaintEncountered = true;
661            }
662        }
663
664        if ( !$validTaintEncountered ) {
665            $this->debug( __METHOD__, 'Possibly wrong taint annotation in docblock: ' . json_encode( $docBlock ) );
666        }
667
668        SecurityCheckPlugin::$docblockCache[ $fqsen ] = $validTaintEncountered
669            ? [ $funcTaint, $fakeMethodLinks ]
670            : null;
671        return SecurityCheckPlugin::$docblockCache[ $fqsen ];
672    }
673
674    /**
675     * Given a type, determine what type of taint
676     *
677     * e.g. Integers are probably untainted since its hard to do evil
678     * with them, but mark strings as unknown since we don't know.
679     *
680     * Only use as a fallback
681     * @param UnionType $types The types
682     */
683    protected function getTaintByType( UnionType $types ): Taintedness {
684        // NOTE: This flattens intersection types
685        $typelist = $types->getUniqueFlattenedTypeSet();
686        if ( !$typelist ) {
687            // $this->debug( __METHOD__, "Setting type unknown due to no type info." );
688            return Taintedness::unknownSingleton();
689        }
690
691        $taint = Taintedness::safeSingleton();
692        $isPossiblyUnknown = false;
693        foreach ( $typelist as $type ) {
694            if ( $type instanceof LiteralTypeInterface ) {
695                // We're going to assume that literals aren't tainted...
696                continue;
697            }
698            switch ( $type->getName() ) {
699                case 'int':
700                case 'non-zero-int':
701                case 'float':
702                case 'bool':
703                case 'false':
704                case 'true':
705                case 'null':
706                case 'void':
707                case 'class-string':
708                case 'callable-string':
709                case 'callable-object':
710                case 'callable-array':
711                    break;
712                case 'string':
713                case 'non-empty-string':
714                case 'Closure':
715                case 'callable':
716                case 'array':
717                case 'iterable':
718                case 'object':
719                case 'resource':
720                case 'mixed':
721                case 'non-empty-mixed':
722                case 'non-null-mixed':
723                    // $this->debug( __METHOD__, "Taint set unknown due to type '$type'." );
724                    $isPossiblyUnknown = true;
725                    break;
726                default:
727                    if ( $type->hasTemplateTypeRecursive() ) {
728                        // TODO Can we do better for template types?
729                        $isPossiblyUnknown = true;
730                        break;
731                    }
732
733                    if ( !$type->isObjectWithKnownFQSEN() ) {
734                        // Likely some phan-specific types not included above
735                        $this->debug( __METHOD__, " $type (" . get_class( $type ) . ') not a class?' );
736                        $isPossiblyUnknown = true;
737                        break;
738                    }
739
740                    $fqsenStr = $type->asFQSEN()->__toString();
741                    if ( isset( self::$fqsensWithoutToStringCache[$fqsenStr] ) ) {
742                        $isPossiblyUnknown = true;
743                        break;
744                    }
745
746                    // This means specific class, so look up __toString()
747                    $toStringFQSEN = FullyQualifiedMethodName::fromStringInContext(
748                        $fqsenStr . '::__toString',
749                        $this->context
750                    );
751                    if ( !$this->code_base->hasMethodWithFQSEN( $toStringFQSEN ) ) {
752                        // This is common in a void context.
753                        // e.g. code like $this->foo() will reach this
754                        // check.
755                        self::$fqsensWithoutToStringCache[$fqsenStr] = true;
756                        $isPossiblyUnknown = true;
757                        break;
758                    }
759                    // TODO: This should add caused-by lines also.
760                    $toString = $this->code_base->getMethodByFQSEN( $toStringFQSEN );
761                    $toStringTaint = $this->getTaintOfFunction( $toString );
762                    $taint = $taint->asMergedWith( $toStringTaint->getOverall()->without(
763                        SecurityCheckPlugin::PRESERVE_TAINT | SecurityCheckPlugin::ALL_EXEC_TAINT
764                    ) );
765            }
766        }
767        if ( $isPossiblyUnknown ) {
768            $taint = $taint->with( SecurityCheckPlugin::UNKNOWN_TAINT );
769        }
770        return $taint;
771    }
772
773    /**
774     * Get what taint types are allowed on a typed element (i.e. use its type to rule out
775     * impossible taint types).
776     *
777     * @return Taintedness|null Null means all taints, checking for null is faster than ORing
778     */
779    protected function getTaintMaskForTypedElement( TypedElementInterface $var ): ?Taintedness {
780        if ( $var instanceof GlobalVariable ) {
781            // TODO We wouldn't need to do this if phan didn't infer real types for global variables.
782            // See https://github.com/phan/phan/issues/4518
783            $var = $var->getElement();
784        }
785        // Note, we must use the real union type because:
786        // 1 - The non-real type might be wrong
787        // 2 - The non-real type might be incomplete (e.g. when analysing a func without docblock
788        // we still don't know all the possible types of the params).
789        return $this->getTaintMaskForType( $var->getUnionType()->getRealUnionType() );
790    }
791
792    /**
793     * Get what taint types are allowed on an element with the given type.
794     *
795     * @return Taintedness|null Null for all flags
796     */
797    protected function getTaintMaskForType( UnionType $type ): ?Taintedness {
798        $typeTaint = $this->getTaintByType( $type );
799
800        if ( $typeTaint->has( SecurityCheckPlugin::UNKNOWN_TAINT ) ) {
801            return null;
802        }
803        return $typeTaint;
804    }
805
806    /**
807     * Get what taint the element could have in the future. For instance, a func parameter may initially
808     * have no taint, but it may become tainted depending on the argument.
809     * @todo Ensure this won't miss any case (aside from when phan infers a wrong real type)
810     *
811     * @return Taintedness|null Null for all taints
812     */
813    protected function getPossibleFutureTaintOfElement( TypedElementInterface $el ): ?Taintedness {
814        return $this->getTaintMaskForTypedElement( $el );
815    }
816
817    /**
818     * Get name of current method (for debugging purposes)
819     *
820     * @return string Name of method or "[no method]"
821     */
822    protected function getCurrentMethod(): string {
823        return $this->context->isInFunctionLikeScope() ?
824            (string)$this->context->getFunctionLikeFQSEN() : '[no method]';
825    }
826
827    /**
828     * Get the taintedness of something from the AST tree.
829     *
830     * @param mixed $expr An expression from the AST tree.
831     */
832    protected function getTaintedness( mixed $expr ): TaintednessWithError {
833        if ( $expr instanceof Node ) {
834            return $this->getTaintednessNode( $expr );
835        }
836
837        assert( is_scalar( $expr ) || $expr === null );
838        return TaintednessWithError::emptySingleton();
839    }
840
841    /**
842     * Give an AST node, find its taint. This always returns a copy.
843     *
844     * @param Node $node
845     * @suppress PhanUndeclaredProperty
846     */
847    protected function getTaintednessNode( Node $node ): TaintednessWithError {
848        // Performance: use isset(), not property_exists()
849        if ( isset( $node->taint ) ) {
850            // Return cached result. Cache hit ratio should ideally be 100%, because we should never have to retrieve
851            // the taintedness of a node without having analyzed it first. For now the ratio is lower because
852            // we don't cache the result of cheap nodes.
853            return $node->taint;
854        }
855        // TODO This might just a return a default if no cached data.
856
857        // Debug::printNode( $node );
858        // Make sure to update the line number, or the same issue may be reported
859        // more than once on different lines (see test 'multilineissue').
860        $oldLine = $this->context->getLineNumberStart();
861        $this->context->setLineNumberStart( $node->lineno );
862
863        $visitor = new TaintednessVisitor( $this->code_base, $this->context );
864        try {
865            return $visitor->analyzeNodeAndGetTaintedness( $node );
866        } finally {
867            $this->context->setLineNumberStart( $oldLine );
868        }
869    }
870
871    /**
872     * Given a phan object (not method/function) find its taint. This always returns a copy
873     * for existing objects.
874     */
875    protected function getTaintednessPhanObj( TypedElementInterface $variableObj ): Taintedness {
876        assert( !$variableObj instanceof FunctionInterface, "This method cannot be used with methods" );
877        $taintOrNull = self::getTaintednessRaw( $variableObj );
878        if ( $taintOrNull !== null ) {
879            $mask = $this->getTaintMaskForTypedElement( $variableObj );
880            $taintedness = $mask !== null ? $taintOrNull->withOnly( $mask->get() ) : $taintOrNull;
881            // echo "$varName has taintedness $taintedness due to last time\n";
882        } else {
883            $type = $variableObj->getUnionType();
884            $taintedness = $this->getTaintByType( $type );
885            // $this->debug( " \$" . $variableObj->getName() . " first sight."
886            // . " taintedness set to $taintedness due to type $type\n";
887        }
888        return $taintedness;
889    }
890
891    /**
892     * Shortcut to resolve array offsets, which includes:
893     *  - Ensuring that the value is not null: null is used for implicit dims like in `$a[] = $b`; we can't say
894     *    for sure what the offset will be, and this method would return null (interpreted as offset 0), which is
895     *    most likely wrong.
896     *  - Casting floats to integers, since using a float as array key raises a warning (and crashes taint-check)
897     *    (T307504)
898     *  - Letting nodes that represent resources (e.g. `STDIN`) pass through, since they're not scalar and certainly
899     *    not valid offsets (see https://github.com/phan/phan/issues/4659).
900     *
901     * @param Node|mixed $rawOffset
902     * @return Node|mixed
903     */
904    protected function resolveOffset( mixed $rawOffset ): mixed {
905        assert( $rawOffset !== null );
906        $resolved = $this->resolveValue( $rawOffset );
907        // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
908        if ( is_resource( $resolved ) ) {
909            return $rawOffset;
910        }
911        return is_float( $resolved ) ? (int)$resolved : $resolved;
912    }
913
914    /**
915     * Shortcut to try and turn an AST element (Node or already literal) into an equivalent PHP
916     * scalar value.
917     *
918     * @param Node|mixed $value A Node or a scalar value from the AST
919     * @return Node|mixed An equivalent scalar PHP value, or $value if it cannot be resolved
920     */
921    protected function resolveValue( mixed $value ): mixed {
922        if ( !$value instanceof Node ) {
923            return $value;
924        }
925
926        try {
927            // Do not emit any issues here, see also upstream's 4629_define_global.php test.
928            return $this->getCtxN( $value )->getEquivalentPHPScalarValue( false );
929        } catch ( IssueException ) {
930            return $value;
931        }
932    }
933
934    /**
935     * Get a property by name in the current scope, failing hard if it cannot be found.
936     */
937    private function getPropInCurrentScopeByName( string $propName ): Property {
938        assert( $this->context->isInClassScope() );
939        try {
940            $clazz = $this->context->getClassInScope( $this->code_base );
941        } catch ( CodeBaseException $e ) {
942            throw new RuntimeException( 'Cannot find class in scope: ' . $this->getDebugInfo( $e ) );
943        }
944
945        assert( $clazz->hasPropertyWithName( $this->code_base, $propName ) );
946        return $clazz->getPropertyByName( $this->code_base, $propName );
947    }
948
949    /**
950     * Quick wrapper to get the ContextNode for a node
951     *
952     * @param Node|mixed $node
953     */
954    protected function getCtxN( mixed $node ): ContextNode {
955        return new ContextNode(
956            $this->code_base,
957            $this->context,
958            $node
959        );
960    }
961
962    /**
963     * Given a node, return the Phan variable objects that
964     * correspond to that node to which we can backpropagate a NUMKEY taintedness.
965     *
966     * @todo This should be handled together with the non-numkey case
967     *
968     * @param Node $node AST node in question
969     * @return TypedElementInterface[] Array of various phan objects corresponding to $node
970     */
971    protected function getObjsForNodeForNumkeyBackprop( Node $node ): array {
972        // TODO For now we only backprop in the simple case, to avoid tons of false positives, unless
973        // the env flag is set (chiefly for tests)
974        $definitelyNumkey = !getenv( 'SECCHECK_NUMKEY_SPERIMENTAL' );
975
976        switch ( $node->kind ) {
977            case \ast\AST_PROP:
978            case \ast\AST_NULLSAFE_PROP:
979            case \ast\AST_STATIC_PROP:
980                $prop = $this->getPropFromNode( $node );
981                return $prop && $this->elementCanBeNumkey( $prop, $definitelyNumkey ) ? [ $prop ] : [];
982            case \ast\AST_VAR:
983            case \ast\AST_CLOSURE_VAR:
984                $cn = $this->getCtxN( $node );
985                if ( Variable::isHardcodedGlobalVariableWithName( $cn->getVariableName() ) ) {
986                    return [];
987                }
988                try {
989                    $var = $cn->getVariable();
990                    return $this->elementCanBeNumkey( $var, $definitelyNumkey ) ? [ $var ] : [];
991                } catch ( NodeException | IssueException $e ) {
992                    $this->debug( __METHOD__, "variable not in scope?? " . $this->getDebugInfo( $e ) );
993                    return [];
994                }
995            case \ast\AST_ENCAPS_LIST:
996            case \ast\AST_ARRAY:
997                $results = [];
998                foreach ( $node->children as $child ) {
999                    if ( !$child instanceof Node ) {
1000                        continue;
1001                    }
1002
1003                    if (
1004                        $child->kind === \ast\AST_ARRAY_ELEM &&
1005                        $child->children['key'] !== null && !$this->nodeCanBeIntKey( $child->children['key'] )
1006                    ) {
1007                        continue;
1008                    }
1009                    $results = array_merge( $this->getObjsForNodeForNumkeyBackprop( $child ), $results );
1010                }
1011                return $results;
1012            case \ast\AST_ARRAY_ELEM:
1013                $results = [];
1014                if ( $node->children['key'] instanceof Node ) {
1015                    $results = array_merge(
1016                        $this->getObjsForNodeForNumkeyBackprop( $node->children['key'] ),
1017                        $results
1018                    );
1019                }
1020                if ( $node->children['value'] instanceof Node ) {
1021                    $results = array_merge(
1022                        $this->getObjsForNodeForNumkeyBackprop( $node->children['value'] ),
1023                        $results
1024                    );
1025                }
1026                return $results;
1027            case \ast\AST_CAST:
1028                // Future todo might be to ignore casts to ints, since
1029                // such things should be safe. Unclear if that makes
1030                // sense in all circumstances.
1031                if ( $node->children['expr'] instanceof Node ) {
1032                    return $this->getObjsForNodeForNumkeyBackprop( $node->children['expr'] );
1033                }
1034                return [];
1035            case \ast\AST_DIM:
1036                if ( $node->children['expr'] instanceof Node ) {
1037                    // For now just consider the outermost array.
1038                    // FIXME. doesn't handle tainted array keys!
1039                    return $this->getObjsForNodeForNumkeyBackprop( $node->children['expr'] );
1040                }
1041                return [];
1042            case \ast\AST_UNARY_OP:
1043                $var = $node->children['expr'];
1044                return $var instanceof Node ? $this->getObjsForNodeForNumkeyBackprop( $var ) : [];
1045            case \ast\AST_BINARY_OP:
1046                $left = $node->children['left'];
1047                $right = $node->children['right'];
1048                $leftObj = $left instanceof Node ? $this->getObjsForNodeForNumkeyBackprop( $left ) : [];
1049                $rightObj = $right instanceof Node ? $this->getObjsForNodeForNumkeyBackprop( $right ) : [];
1050                return array_merge( $leftObj, $rightObj );
1051            case \ast\AST_CONDITIONAL:
1052                $t = $node->children['true'];
1053                $f = $node->children['false'];
1054                $tObj = $t instanceof Node ? $this->getObjsForNodeForNumkeyBackprop( $t ) : [];
1055                $fObj = $f instanceof Node ? $this->getObjsForNodeForNumkeyBackprop( $f ) : [];
1056                return array_merge( $tObj, $fObj );
1057            case \ast\AST_CONST:
1058            case \ast\AST_CLASS_CONST:
1059            case \ast\AST_CLASS_NAME:
1060            case \ast\AST_MAGIC_CONST:
1061            case \ast\AST_ISSET:
1062            case \ast\AST_NEW:
1063            // For now we don't do methods, only variables
1064            // Also don't do args to function calls.
1065            // Unclear if this makes sense.
1066                return [];
1067            case \ast\AST_CALL:
1068            case \ast\AST_STATIC_CALL:
1069            case \ast\AST_METHOD_CALL:
1070            case \ast\AST_NULLSAFE_METHOD_CALL:
1071                if ( $definitelyNumkey ) {
1072                    // This case is too hard for now.
1073                    return [];
1074                }
1075                $ctxNode = $this->getCtxN( $node );
1076                // @todo Future todo might be to still return arguments when catching an exception.
1077                if ( $node->kind === \ast\AST_CALL ) {
1078                    if ( $node->children['expr']->kind !== \ast\AST_NAME ) {
1079                        // TODO Handle this case!
1080                        return [];
1081                    }
1082                    try {
1083                        $func = $ctxNode->getFunction( $node->children['expr']->children['name'] );
1084                    } catch ( IssueException | FQSENException $e ) {
1085                        $this->debug( __METHOD__, "FIXME func not found: " . $this->getDebugInfo( $e ) );
1086                        return [];
1087                    }
1088                } else {
1089                    $methodName = $node->children['method'];
1090                    try {
1091                        $func = $ctxNode->getMethod( $methodName, $node->kind === \ast\AST_STATIC_CALL, true );
1092                    } catch ( NodeException | CodeBaseException | IssueException $e ) {
1093                        $this->debug( __METHOD__, "FIXME method not found: " . $this->getDebugInfo( $e ) );
1094                        return [];
1095                    }
1096                }
1097                try {
1098                    return $this->getReturnObjsOfFunc( $func );
1099                } catch ( Exception $e ) {
1100                    $this->debug( __METHOD__, "FIXME: " . $this->getDebugInfo( $e ) );
1101                    return [];
1102                }
1103            case \ast\AST_PRE_INC:
1104            case \ast\AST_PRE_DEC:
1105            case \ast\AST_POST_INC:
1106            case \ast\AST_POST_DEC:
1107                $children = $node->children;
1108                assert( count( $children ) === 1 );
1109                return $this->getObjsForNodeForNumkeyBackprop( reset( $children ) );
1110            default:
1111                // TODO Should probably handle AST_MATCH & friends
1112                // Debug::printNode( $node );
1113                // This should really be a visitor that recurses into
1114                // things.
1115                $this->debug( __METHOD__, "FIXME unhandled case"
1116                    . Debug::nodeName( $node ) . "\n"
1117                );
1118                return [];
1119        }
1120    }
1121
1122    protected function getPropFromNode( Node $node ): ?Property {
1123        try {
1124            return $this->getCtxN( $node )->getProperty( $node->kind === \ast\AST_STATIC_PROP );
1125        } catch ( NodeException | IssueException | UnanalyzableException $e ) {
1126            $this->debug( __METHOD__, "Cannot determine " .
1127                "property (Maybe don't know what class) - " .
1128                $this->getDebugInfo( $e )
1129            );
1130            return null;
1131        }
1132    }
1133
1134    /**
1135     * Extract some useful debug data from an exception
1136     */
1137    protected function getDebugInfo( Exception $e ): string {
1138        return $e instanceof IssueException
1139            ? $e->getIssueInstance()->__toString()
1140            : ( get_class( $e ) . " {$e->getMessage()}" );
1141    }
1142
1143    /**
1144     * Get the current filename and line.
1145     *
1146     * @param Context|null $context Override the context to make debug info for
1147     * @return string path/to/file +linenumber
1148     */
1149    protected function dbgInfo( ?Context $context = null ): string {
1150        $ctx = $context ?: $this->context;
1151        // Using a + instead of : so that I can just copy and paste
1152        // into a vim command line.
1153        return $ctx->getFile() . ' +' . $ctx->getLineNumberStart();
1154    }
1155
1156    /**
1157     * Link together a Method and its parameters,the idea being if the method gets called with something evil
1158     * later, we can traceback anything it might affect.
1159     * Note that we don't do this for functions with hardcoded taint, in which case we assume that any dangerous
1160     * association was already hardcoded. This is also good for performance, because hardcoded function tend to be
1161     * used a lot (for MW, think of methods in Database or in Html).
1162     *
1163     * @param Variable $param The variable object for the parameter. This can also be
1164     *  instance of Parameter (subclass of Variable).
1165     * @param FunctionInterface $func The function/method in question
1166     * @param int $i Which argument number is $param
1167     */
1168    protected function linkParamAndFunc( Variable $param, FunctionInterface $func, int $i ): void {
1169        // $this->debug( __METHOD__, "Linking '$param' to '$func' arg $i" );
1170
1171        // TODO Use $func's builtin/annotated taintedness (available in PreTaintednessVisitor) to check this per
1172        // parameter (looking at NO_OVERRIDE)
1173        $canLinkParam = !SecurityCheckPlugin::$pluginInstance->builtinFuncHasTaint( $func->getFQSEN() );
1174        if ( !$canLinkParam ) {
1175            return;
1176        }
1177
1178        self::ensureVarLinksForArgExist( $func, $i );
1179
1180        $curLinks = self::getMethodLinks( $param ) ?? MethodLinks::emptySingleton();
1181        self::setMethodLinks( $param, $curLinks->withFuncAndParam( $func, $i, $param->isVariadic() ) );
1182    }
1183
1184    /**
1185     * Given a LHS and RHS make all the methods that can set RHS also for LHS
1186     *
1187     * Given 2 variables (e.g. $lhs = $rhs ), see to it that any function/method
1188     * which we marked as being able to set the value of rhs, is also marked
1189     * as being able to set the value of lhs. We use this information to figure
1190     * out what method parameter is causing the return statement to be tainted.
1191     *
1192     * @warning Be careful calling this function if lhs already has taint
1193     *  or rhs side is a compound statement. This could result in misattribution
1194     *  of where the taint is coming from.
1195     *
1196     * This also merges the information on what line caused the taint.
1197     *
1198     * @param TypedElementInterface $lhs Source of method list
1199     * @param MethodLinks $rhsLinks New links
1200     * @param bool $override
1201     */
1202    protected function mergeTaintDependencies(
1203        TypedElementInterface $lhs,
1204        MethodLinks $rhsLinks,
1205        bool $override
1206    ): void {
1207        // So if we have $a = $b;
1208        // First we find out all the methods that can set $b
1209        // Then we add $a to the list of variables that those methods can set.
1210        // Last we add these methods to $a's list of all methods that can set it.
1211
1212        $curLinks = self::getMethodLinks( $lhs );
1213        if ( $override || !$curLinks ) {
1214            $newLinks = $rhsLinks;
1215        } else {
1216            $newLinks = $curLinks->asMergedWith( $rhsLinks );
1217        }
1218
1219        // Don't attach things like Variable and Parameter. These are local elements, and setting taint
1220        // on them in markAllDependentVarsYes would have no effect. Additionally, since phan creates a new
1221        // Parameter object for each analysis, we will end up with duplicated links that do nothing but
1222        // eating memory.
1223        // Also unnecessary to attach PassByReferenceVariable, as that receives special handling in handlePassByRef.
1224        if ( $lhs instanceof Property || $lhs instanceof GlobalVariable ) {
1225            foreach ( $newLinks->getMethodAndParamTuples() as [ $method, $index ] ) {
1226                $varLinks = self::getVarLinks( $method, $index );
1227                assert( $varLinks instanceof VarLinksMap );
1228                // $this->debug( __METHOD__, "During assignment, we link $lhs to $method($index)" );
1229                $varLinks->offsetSet( $lhs, $newLinks->asPreservedTaintednessForFuncParam( $method, $index ) );
1230            }
1231        }
1232
1233        self::setMethodLinks( $lhs, $newLinks );
1234    }
1235
1236    /**
1237     * Mark any function setting a specific variable as EXEC taint
1238     *
1239     * If you do something like echo $this->foo;
1240     * This method is called to make all things that set $this->foo
1241     * as TAINT_EXEC.
1242     *
1243     * @note This might have annoying false positives with widely used properties
1244     * that are used with different levels of escaping, which is not a good idea anyway.
1245     *
1246     * @param TypedElementInterface $var The variable in question
1247     * @param Taintedness $sinkTaint What taint to mark them as.
1248     * @param CausedByLines|null $sinkError Any extra caused-by lines from the sink to add
1249     */
1250    protected function markAllDependentMethodsExec(
1251        TypedElementInterface $var,
1252        Taintedness $sinkTaint,
1253        ?CausedByLines $sinkError = null
1254    ): void {
1255        $futureTaint = $this->getPossibleFutureTaintOfElement( $var );
1256        if ( $futureTaint !== null && !$futureTaint->has( $sinkTaint->get() ) ) {
1257            return;
1258        }
1259        // Ensure we only set exec bits, not normal taint bits.
1260        $taint = $sinkTaint->withOnly( SecurityCheckPlugin::BACKPROP_TAINTS );
1261        if ( $taint->isSafe() || $this->isIssueSuppressedOrFalsePositive( $taint ) ) {
1262            return;
1263        }
1264
1265        $varLinks = self::getMethodLinks( $var );
1266        if ( $varLinks === null || $varLinks->isEmpty() ) {
1267            return;
1268        }
1269        $varError = self::getCausedByRaw( $var );
1270        $varError = $varError ? $varError->withOnlyLinks() : CausedByLines::emptySingleton();
1271        $sinkError ??= CausedByLines::emptySingleton();
1272
1273        $methodParamTuples = $varLinks->getMethodAndParamTuples();
1274        $linkedParameters = new SplObjectStorage();
1275        foreach ( $methodParamTuples as [ $method, $param ] ) {
1276            $curData = $linkedParameters[$method] ?? [];
1277            $curData[] = $param;
1278            $linkedParameters[$method] = $curData;
1279        }
1280        $emptyFunctionTaint = FunctionTaintedness::emptySingleton();
1281        $emptyFunctionCausedByLines = FunctionCausedByLines::emptySingleton();
1282        foreach ( $linkedParameters as $method ) {
1283            $parameters = $linkedParameters[$method];
1284            $calleeParamList = $method->getParameterList();
1285            $funcTaint = $emptyFunctionTaint;
1286            $funcArgError = $emptyFunctionCausedByLines;
1287            $funcSinkError = $emptyFunctionCausedByLines;
1288            foreach ( $parameters as $param ) {
1289                $filteredLinks = $varLinks->asFilteredForFuncAndParam( $method, $param );
1290                $newParamTaint = $sinkTaint->appliedToLinksForBackprop( $filteredLinks, $method, $param );
1291                $paramVarError = $varError
1292                    ->withTaintAddedToMethodArgLinks( $newParamTaint->asExecToYesTaint(), $method, $param, true );
1293                $paramSinkError = $sinkError->forSinkBackprop( $filteredLinks, $method, $param );
1294
1295                if ( isset( $calleeParamList[$param] ) && $calleeParamList[$param]->isVariadic() ) {
1296                    $funcTaint = $funcTaint->withVariadicParamSinkTaint( $param, $newParamTaint );
1297                    $funcArgError = $funcArgError->withVariadicParamSinkLines( $param, $paramVarError );
1298                    $funcSinkError = $funcSinkError->withVariadicParamSinkLines( $param, $paramSinkError );
1299                } else {
1300                    $funcTaint = $funcTaint->withParamSinkTaint( $param, $newParamTaint );
1301                    $funcArgError = $funcArgError->withParamSinkLines( $param, $paramVarError );
1302                    $funcSinkError = $funcSinkError->withParamSinkLines( $param, $paramSinkError );
1303                }
1304            }
1305
1306            $this->addFuncTaint( $method, $funcTaint );
1307            $newFuncTaint = self::getFuncTaint( $method );
1308            assert( $newFuncTaint !== null );
1309
1310            $this->mergeFuncError( $method, $funcArgError, $newFuncTaint );
1311            $this->maybeAddFuncError( $method, null, $funcTaint, $newFuncTaint );
1312            $this->mergeFuncError( $method, $funcSinkError, $newFuncTaint );
1313        }
1314    }
1315
1316    /**
1317     * Mark any function setting a specific variable as EXEC taint
1318     *
1319     * If you do something like echo $this->foo;
1320     * This method is called to make all things that set $this->foo
1321     * as TAINT_EXEC.
1322     *
1323     * @note This might have annoying false positives with widely used properties
1324     * that are used with different levels of escaping, which is not a good idea anyway.
1325     *
1326     * @param Node $node
1327     * @param Taintedness $taint What taint to mark them as.
1328     * @param CausedByLines|null $additionalError Additional caused-by lines to propagate
1329     * @param bool $tempNumkey Temporary param
1330     */
1331    protected function markAllDependentMethodsExecForNode(
1332        Node $node,
1333        Taintedness $taint,
1334        ?CausedByLines $additionalError = null,
1335        bool $tempNumkey = false
1336    ): void {
1337        if ( !$tempNumkey ) {
1338            $backpropVisitor = new TaintednessBackpropVisitor(
1339                $this->code_base,
1340                $this->context,
1341                $taint,
1342                $additionalError
1343            );
1344            $backpropVisitor( $node );
1345            return;
1346        }
1347        $phanObjs = $this->getObjsForNodeForNumkeyBackprop( $node );
1348        foreach ( array_unique( $phanObjs ) as $phanObj ) {
1349            $this->markAllDependentMethodsExec( $phanObj, $taint, $additionalError );
1350        }
1351    }
1352
1353    /**
1354     * This happens when someone calls foo( $evilTaintedVar );
1355     *
1356     * It makes sure that any variable that the function foo() sets takes on
1357     * the taint of the supplied argument.
1358     *
1359     * @param FunctionInterface $method The function or method in question
1360     * @param int $i The number of the argument in question.
1361     * @param Taintedness $taint The taint to apply.
1362     * @param CausedByLines $error Caused-by lines to propagate
1363     */
1364    protected function markAllDependentVarsYes(
1365        FunctionInterface $method,
1366        int $i,
1367        Taintedness $taint,
1368        CausedByLines $error
1369    ): void {
1370        if ( $method->isPHPInternal() ) {
1371            return;
1372        }
1373        $varLinks = self::getVarLinks( $method, $i );
1374        if ( $varLinks === null ) {
1375            return;
1376        }
1377
1378        $taintAdjusted = $taint->withOnly( SecurityCheckPlugin::ALL_TAINT );
1379
1380        foreach ( $varLinks as $var ) {
1381            $presTaint = $varLinks[$var];
1382            $taintToPropagate = $presTaint->asTaintednessForArgument( $taintAdjusted );
1383            $this->setTaintedness( $var, $taintToPropagate, false );
1384
1385            $newErrors = $this->getCausedByLinesToAdd( $taintToPropagate, null );
1386            $adjustedCausedBy = ( self::getCausedByRaw( $var ) ?? CausedByLines::emptySingleton() )
1387                ->withTaintAddedToMethodArgLinks( $taintToPropagate, $method, $i, false );
1388            $newCausedBy = $error->withAddedLines( $newErrors, $taintToPropagate )->asMergedWith( $adjustedCausedBy );
1389            self::setCausedByRaw( $var, $newCausedBy );
1390
1391            if ( $var instanceof GlobalVariable ) {
1392                $globalVar = $var->getElement();
1393                $this->setTaintedness( $globalVar, $taintToPropagate, false );
1394
1395                $adjustedGlobalCausedBy = ( self::getCausedByRaw( $globalVar ) ?? CausedByLines::emptySingleton() )
1396                    ->withTaintAddedToMethodArgLinks( $taintToPropagate, $method, $i, false );
1397                $newGlobalCausedBy = $error->withAddedLines( $newErrors, $taintToPropagate )
1398                    ->asMergedWith( $adjustedGlobalCausedBy );
1399                self::setCausedByRaw( $globalVar, $newGlobalCausedBy );
1400            }
1401        }
1402    }
1403
1404    /**
1405     * Get the original cause of taint for the given func
1406     */
1407    private function getCausedByLinesForFunc( FunctionInterface $element ): FunctionCausedByLines {
1408        $element = $this->getActualFuncWithCausedBy( $element );
1409        return self::getFuncCausedByRaw( $element ) ?? FunctionCausedByLines::emptySingleton();
1410    }
1411
1412    /**
1413     * Given a phan element, get the actual element where caused-by data is stored. For instance, for methods, this
1414     * returns the defining methods.
1415     */
1416    private function getActualFuncWithCausedBy( FunctionInterface $element ): FunctionInterface {
1417        if ( SecurityCheckPlugin::$pluginInstance->builtinFuncHasTaint( $element->getFQSEN() ) ) {
1418            return $element;
1419        }
1420        $definingFunc = $this->getDefiningFuncIfDifferent( $element );
1421        return $definingFunc ?? $element;
1422    }
1423
1424    /**
1425     * Output a debug message to stdout.
1426     *
1427     * @param string $method __METHOD__ in question
1428     * @param string $msg debug message
1429     * @codeCoverageIgnore
1430     */
1431    public function debug( string $method, string $msg ): void {
1432        if ( $this->debugOutput === null ) {
1433            $errorOutput = getenv( "SECCHECK_DEBUG" );
1434            if ( $errorOutput && $errorOutput !== '-' ) {
1435                $this->debugOutput = fopen( $errorOutput, "w" );
1436            } elseif ( $errorOutput === '-' ) {
1437                $this->debugOutput = '-';
1438            } else {
1439                $this->debugOutput = false;
1440            }
1441        }
1442        $line = $method . "\33[1m " . $this->dbgInfo() . " \33[0m" . $msg . "\n";
1443        if ( $this->debugOutput && $this->debugOutput !== '-' ) {
1444            fwrite(
1445                $this->debugOutput,
1446                $line
1447            );
1448        } elseif ( $this->debugOutput === '-' ) {
1449            // @phan-suppress-next-line PhanPluginRemoveDebugEcho This is the only wanted debug echo
1450            echo $line;
1451        }
1452    }
1453
1454    /**
1455     * Given an AST node that's a callable, try and determine what it is
1456     *
1457     * This is intended for functions that register callbacks.
1458     *
1459     * @param Node|mixed $node The thingy from AST expected to be a Callable
1460     */
1461    protected function getCallableFromNode( mixed $node ): ?FunctionInterface {
1462        if ( is_string( $node ) ) {
1463            // Easy case, 'Foo::Bar'
1464            // NOTE: ContextNode::getFunctionFromNode has a TODO about returning something here.
1465            // And also NOTE: 'self::methodname()' is not valid PHP.
1466            // TODO: We should probably emit a non-security issue in the missing case
1467            if ( !str_contains( $node, '::' ) ) {
1468                try {
1469                    $callback = FullyQualifiedFunctionName::fromFullyQualifiedString( $node );
1470                } catch ( FQSENException ) {
1471                    // String wasn't actually a callable.
1472                    return null;
1473                }
1474                return $this->code_base->hasFunctionWithFQSEN( $callback )
1475                    ? $this->code_base->getFunctionByFQSEN( $callback )
1476                    : null;
1477            }
1478
1479            try {
1480                $callback = FullyQualifiedMethodName::fromFullyQualifiedString( $node );
1481            } catch ( FQSENException ) {
1482                // String wasn't actually a callable.
1483                return null;
1484            }
1485            return $this->code_base->hasMethodWithFQSEN( $callback )
1486                ? $this->code_base->getMethodByFQSEN( $callback )
1487                : null;
1488        }
1489        if ( !$node instanceof Node ) {
1490            return null;
1491        }
1492        if (
1493            $node->kind === \ast\AST_CLOSURE ||
1494            $node->kind === \ast\AST_VAR ||
1495            ( $node->kind === \ast\AST_ARRAY && count( $node->children ) === 2 ) ||
1496            (
1497                ( $node->kind === AST_CALL || $node->kind === AST_METHOD_CALL || $node->kind === AST_STATIC_CALL ) &&
1498                $node->children['args']->kind === AST_CALLABLE_CONVERT
1499            )
1500        ) {
1501            // Note: intentionally emitting any issues here.
1502            $funcs = $this->getCtxN( $node )->getFunctionFromNode();
1503            return self::getFirstElmFromArrayOrGenerator( $funcs );
1504        }
1505        return null;
1506    }
1507
1508    /**
1509     * Utility function to get the first element from an iterable that can be either an array or a generator
1510     *
1511     * @template T
1512     * @param iterable<T> $iter
1513     * @return T|null Null if $iter is empty
1514     */
1515    protected static function getFirstElmFromArrayOrGenerator( iterable $iter ) {
1516        if ( is_array( $iter ) ) {
1517            return $iter ? $iter[0] : null;
1518        }
1519        assert( $iter instanceof Generator );
1520        return $iter->current() ?: null;
1521    }
1522
1523    /**
1524     * Get the issue names and severities given a taint, as well as the relevant taint type for each issue.
1525     *
1526     * @param Taintedness $combinedTaint The taint to warn for, i.e. the exec flags from LHS, intersected with the RHS
1527     * taint. Must have EXEC flags only.
1528     * @param int $combinedTaintInt All taintedness flags present in $combinedTaint
1529     * @return array<array<string|int|Taintedness>> List of issue type, severity, and taintedness
1530     * @phan-return non-empty-list<array{0:string,1:int,2:Taintedness}>
1531     */
1532    public function taintToIssuesAndSeverities( Taintedness $combinedTaint, int $combinedTaintInt ): array {
1533        $issues = [];
1534        if ( $combinedTaintInt & SecurityCheckPlugin::HTML_EXEC_TAINT ) {
1535            $issues[] = [
1536                'SecurityCheck-XSS',
1537                Issue::SEVERITY_NORMAL,
1538                $combinedTaint->withOnly( SecurityCheckPlugin::HTML_EXEC_TAINT )
1539            ];
1540        }
1541        $allSQLTaints = SecurityCheckPlugin::SQL_EXEC_TAINT | SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT;
1542        if ( $combinedTaintInt & $allSQLTaints ) {
1543            $issues[] = [
1544                'SecurityCheck-SQLInjection',
1545                Issue::SEVERITY_CRITICAL,
1546                $combinedTaint->withOnly( $allSQLTaints )
1547            ];
1548        }
1549        if ( $combinedTaintInt & SecurityCheckPlugin::SHELL_EXEC_TAINT ) {
1550            $issues[] = [
1551                'SecurityCheck-ShellInjection',
1552                Issue::SEVERITY_CRITICAL,
1553                $combinedTaint->withOnly( SecurityCheckPlugin::SHELL_EXEC_TAINT )
1554            ];
1555        }
1556        if ( $combinedTaintInt & SecurityCheckPlugin::SERIALIZE_EXEC_TAINT ) {
1557            // For now this is low because it seems to have a lot of false positives.
1558            $issues[] = [
1559                'SecurityCheck-PHPSerializeInjection',
1560                Issue::SEVERITY_NORMAL,
1561                $combinedTaint->withOnly( SecurityCheckPlugin::SERIALIZE_EXEC_TAINT )
1562            ];
1563        }
1564        if ( $combinedTaintInt & SecurityCheckPlugin::ESCAPED_EXEC_TAINT ) {
1565            $issues[] = [
1566                'SecurityCheck-DoubleEscaped',
1567                Issue::SEVERITY_NORMAL,
1568                $combinedTaint->withOnly( SecurityCheckPlugin::ESCAPED_EXEC_TAINT )
1569            ];
1570        }
1571        if ( $combinedTaintInt & SecurityCheckPlugin::PATH_EXEC_TAINT ) {
1572            $issues[] = [
1573                'SecurityCheck-PathTraversal',
1574                Issue::SEVERITY_CRITICAL,
1575                $combinedTaint->withOnly( SecurityCheckPlugin::PATH_EXEC_TAINT )
1576            ];
1577        }
1578        if ( $combinedTaintInt & SecurityCheckPlugin::CODE_EXEC_TAINT ) {
1579            $issues[] = [
1580                'SecurityCheck-RCE',
1581                Issue::SEVERITY_CRITICAL,
1582                $combinedTaint->withOnly( SecurityCheckPlugin::CODE_EXEC_TAINT )
1583            ];
1584        }
1585        if ( $combinedTaintInt & SecurityCheckPlugin::REGEX_EXEC_TAINT ) {
1586            $issues[] = [
1587                'SecurityCheck-ReDoS',
1588                Issue::SEVERITY_NORMAL,
1589                $combinedTaint->withOnly( SecurityCheckPlugin::REGEX_EXEC_TAINT )
1590            ];
1591        }
1592        if ( $combinedTaintInt & SecurityCheckPlugin::CUSTOM1_EXEC_TAINT ) {
1593            $issues[] = [
1594                'SecurityCheck-CUSTOM1',
1595                Issue::SEVERITY_NORMAL,
1596                $combinedTaint->withOnly( SecurityCheckPlugin::CUSTOM1_EXEC_TAINT )
1597            ];
1598        }
1599        if ( $combinedTaintInt & SecurityCheckPlugin::CUSTOM2_EXEC_TAINT ) {
1600            $issues[] = [
1601                'SecurityCheck-CUSTOM2',
1602                Issue::SEVERITY_NORMAL,
1603                $combinedTaint->withOnly( SecurityCheckPlugin::CUSTOM2_EXEC_TAINT )
1604            ];
1605        }
1606
1607        return $issues;
1608    }
1609
1610    /**
1611     * Simplified version of maybeEmitIssue which makes the following assumptions:
1612     *  - The caller would compute the RHS taint only to feed it to maybeEmitIssue
1613     *  - The message should be followed by caused-by lines
1614     *  - These caused-by lines should be taken from the same object passed as RHS
1615     *  - Only caused-by lines having the LHS taint should be included
1616     * If these conditions hold true, then this method should be preferred.
1617     *
1618     * @warning DO NOT use this method if the caller already needs to compute the RHS
1619     * taintedness! The taint would be computed twice!
1620     *
1621     * @param Taintedness $lhsTaint
1622     * @param mixed $rhsElement
1623     * @param string $msg
1624     * @param array $params Additional parameters for the message template
1625     * @phan-param list<string|FullyQualifiedFunctionLikeName> $params
1626     */
1627    public function maybeEmitIssueSimplified(
1628        Taintedness $lhsTaint,
1629        mixed $rhsElement,
1630        string $msg,
1631        array $params = []
1632    ): void {
1633        $rhsTaint = $this->getTaintedness( $rhsElement );
1634        $this->maybeEmitIssue(
1635            $lhsTaint,
1636            $rhsTaint->getTaintedness(),
1637            $msg . '{DETAILS}',
1638            array_merge( $params, [ [ 'lines' => $rhsTaint->getError(), 'sink' => false ] ] )
1639        );
1640    }
1641
1642    /**
1643     * Emit an issue using the appropriate issue type
1644     *
1645     * If $this->overrideContext is set, it will use that for the
1646     * file/line number to report. This is meant as a hack, so that
1647     * in MW we can force hook related issues to be in the extension
1648     * instead of where the hook is called from in MW core.
1649     *
1650     * @param Taintedness $lhsTaint Taint of left hand side (or equivalent)
1651     * @param Taintedness $rhsTaint Taint of right hand side (or equivalent)
1652     * @param string $msg Issue description
1653     * @param array|Closure $msgParamsOrGetter Message parameters passed to emitIssue. Can also be a closure
1654     * that returns said parameters, for performance.
1655     * @phan-param list|Closure():list $msgParamsOrGetter
1656     */
1657    public function maybeEmitIssue(
1658        Taintedness $lhsTaint,
1659        Taintedness $rhsTaint,
1660        string $msg,
1661        array|Closure $msgParamsOrGetter
1662    ): void {
1663        $rhsIsUnknown = $rhsTaint->has( SecurityCheckPlugin::UNKNOWN_TAINT );
1664        if ( $rhsIsUnknown && $lhsTaint->has( SecurityCheckPlugin::ALL_EXEC_TAINT ) ) {
1665            $combinedTaint = Taintedness::safeSingleton();
1666            $combinedTaintInt = SecurityCheckPlugin::NO_TAINT;
1667        } else {
1668            $combinedTaint = Taintedness::intersectForSink( $lhsTaint, $rhsTaint );
1669            if ( $combinedTaint->isSafe() ) {
1670                return;
1671            }
1672            $combinedTaintInt = $combinedTaint->get();
1673        }
1674
1675        if (
1676            ( $combinedTaintInt === SecurityCheckPlugin::NO_TAINT && $rhsIsUnknown ) ||
1677            SecurityCheckPlugin::$pluginInstance->isFalsePositive(
1678                Taintedness::flagsAsExecToYesTaint( $combinedTaintInt ),
1679                $msg,
1680                // FIXME should this be $this->overrideContext ?
1681                $this->context,
1682                $this->code_base
1683            )
1684        ) {
1685            $issues = [
1686                [ 'SecurityCheck-LikelyFalsePositive', Issue::SEVERITY_LOW, $combinedTaint ]
1687            ];
1688        } else {
1689            $issues = $this->taintToIssuesAndSeverities( $combinedTaint, $combinedTaintInt );
1690        }
1691
1692        if ( !$issues ) {
1693            return;
1694        }
1695
1696        $context = $this->context;
1697        if ( $this->overrideContext ) {
1698            // If we are overriding the file/line number,
1699            // report the original line number as well.
1700            $msg .= " (Originally at: $this->context)";
1701            $context = $this->overrideContext;
1702        }
1703
1704        $msgParams = $msgParamsOrGetter instanceof Closure ? $msgParamsOrGetter() : $msgParamsOrGetter;
1705        // Phan doesn't analyze the ternary correctly and thinks this might also be a closure.
1706        '@phan-var list $msgParams';
1707
1708        /** @var Taintedness $relevantSinkTaint */
1709        foreach ( $issues as [ $issueType, $severity, $relevantSinkTaint ] ) {
1710            $relevantRHSTaint = $rhsTaint->withNumkeyAddedToSQL()
1711                ->withOnly( $relevantSinkTaint->asExecToYesTaint()->get() );
1712            $curMsgParams = [];
1713            foreach ( $msgParams as $i => $par ) {
1714                if ( is_array( $par ) ) {
1715                    assert( isset( $par['lines'] ) && $par['lines'] instanceof CausedByLines );
1716                    $curMsgParams[$i] = $par['lines']->toStringForIssue(
1717                        $relevantSinkTaint,
1718                        $relevantRHSTaint,
1719                        $par['sink']
1720                    );
1721                } else {
1722                    $curMsgParams[$i] = $par;
1723                }
1724            }
1725            SecurityCheckPlugin::emitIssue(
1726                $this->code_base,
1727                $context,
1728                $issueType,
1729                $msg,
1730                $curMsgParams,
1731                $severity
1732            );
1733        }
1734    }
1735
1736    /**
1737     * Method to determine if a potential error isn't really real
1738     *
1739     * This is useful when a specific warning would have a side effect
1740     * and we want to know whether we should suppress the side effect in
1741     * addition to the warning.
1742     *
1743     * @param Taintedness $lhsTaint Must have at least one EXEC flag set
1744     */
1745    public function isIssueSuppressedOrFalsePositive( Taintedness $lhsTaint ): bool {
1746        assert( $lhsTaint->has( SecurityCheckPlugin::ALL_EXEC_TAINT ) );
1747        $combinedTaintInt = $lhsTaint->get();
1748
1749        $issues = $this->taintToIssuesAndSeverities( $lhsTaint, $combinedTaintInt );
1750        $context = $this->overrideContext ?: $this->context;
1751        foreach ( $issues as [ $issueType ] ) {
1752            if ( $context->hasSuppressIssue( $this->code_base, $issueType ) ) {
1753                return true;
1754            }
1755        }
1756
1757        $msg = "[dummy msg for false positive check]";
1758        return SecurityCheckPlugin::$pluginInstance->isFalsePositive(
1759            Taintedness::flagsAsExecToYesTaint( $combinedTaintInt ),
1760            $msg,
1761            // not using $this->overrideContext to be consistent with maybeEmitIssue()
1762            $this->context,
1763            $this->code_base
1764        );
1765    }
1766
1767    /**
1768     * Somebody invokes a method or function (or something similar)
1769     *
1770     * This has to figure out:
1771     *  Is the return value of the call tainted
1772     *  Are any of the arguments tainted
1773     *  Does the function do anything scary with its arguments
1774     * It also has to maintain quite a bit of book-keeping.
1775     *
1776     * @param FunctionInterface $func
1777     * @param FullyQualifiedFunctionLikeName $funcName
1778     * @param array $args Arguments to function/method
1779     * @phan-param array<Node|mixed> $args
1780     * @param bool $computePreserve Whether the caller wants to know which taintedness is preserved by this call
1781     * @param bool $isHookHandler Whether we're analyzing a hook handler for a hook handler call.
1782     *   FIXME This is MW-specific
1783     * @return TaintednessWithError|null Taint The resulting taint of the expression, or null if
1784     *   $computePreserve is false
1785     */
1786    public function handleMethodCall(
1787        FunctionInterface $func,
1788        FullyQualifiedFunctionLikeName $funcName,
1789        array $args,
1790        bool $computePreserve = true,
1791        bool $isHookHandler = false
1792    ): ?TaintednessWithError {
1793        $specialCallResult = null;
1794        if ( $this->maybeHandleSpecialCall( $func, $args, $computePreserve, $specialCallResult ) ) {
1795            return $specialCallResult;
1796        }
1797
1798        $taint = $this->getTaintOfFunction( $func );
1799        $funcError = $this->getCausedByLinesForFunc( $func );
1800
1801        $preserveArgumentsData = [];
1802        foreach ( $args as $i => $argument ) {
1803            if ( !( $argument instanceof Node ) ) {
1804                // Literal value
1805                continue;
1806            }
1807            $curParFlags = $taint->getParamFlags( $i );
1808            if ( ( $curParFlags & SecurityCheckPlugin::ARRAY_OK ) && $this->nodeIsArray( $argument ) ) {
1809                // This function specifies that arrays are always ok, so skip.
1810                continue;
1811            }
1812
1813            if ( $argument->kind === \ast\AST_NAMED_ARG ) {
1814                [ $i, $argument, $argName ] = $this->translateNamedArg( $argument, $func );
1815                if ( $i === null || !$argument instanceof Node ) {
1816                    // Cannot find argument or it's literal
1817                    continue;
1818                }
1819                $argName = "`$argName`";
1820            } else {
1821                $argName = '#' . ( $i + 1 );
1822            }
1823
1824            $paramSinkTaint = $taint->getParamSinkTaint( $i );
1825            $paramSinkError = $funcError->getParamSinkLines( $i );
1826
1827            $argTaintWithError = $this->getTaintednessNode( $argument );
1828            $curArgTaintedness = $argTaintWithError->getTaintedness();
1829            $baseArgError = $argTaintWithError->getError();
1830            if (
1831                $paramSinkTaint->has( SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT )
1832                && $curArgTaintedness->has( SecurityCheckPlugin::SQL_TAINT )
1833                && $this->nodeCanBeString( $argument )
1834            ) {
1835                // Special case to make NUMKEY work right for non-array values.
1836                // TODO Should consider if this is really best approach.
1837                $curArgTaintedness = $curArgTaintedness->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT );
1838            }
1839
1840            [ $paramSinkTaint, $paramSinkError ] = SecurityCheckPlugin::$pluginInstance->modifyParamSinkTaint(
1841                $paramSinkTaint,
1842                $curArgTaintedness,
1843                $argument,
1844                $i,
1845                $func,
1846                $taint,
1847                $paramSinkError,
1848                $this->context,
1849                $this->code_base
1850            );
1851
1852            // Add a hook in order to special case for codebases. This is primarily used as a hack so that in mediawiki
1853            // the Message class doesn't have double escape taint if method takes Message|string.
1854            // TODO This is quite hacky.
1855            $curArgTaintedness = SecurityCheckPlugin::$pluginInstance->modifyArgTaint(
1856                $curArgTaintedness,
1857                $argument,
1858                $i,
1859                $func,
1860                $taint,
1861                $this->context,
1862                $this->code_base
1863            );
1864
1865            // TODO: We also need to handle the case where someFunc( $execArg ) for pass by reference where
1866            // the parameter is later executed outside the func.
1867            if ( $curArgTaintedness->has( SecurityCheckPlugin::ALL_TAINT ) ) {
1868                $this->markAllDependentVarsYes( $func, $i, $curArgTaintedness, $baseArgError );
1869            }
1870
1871            // We are doing something like evilMethod( $arg ); where $arg is a parameter to the current function.
1872            // So backpropagate that assigning to $arg can cause evilness.
1873            if ( !$paramSinkTaint->isSafe() ) {
1874                $this->backpropagateArgTaint( $argument, $paramSinkTaint, $paramSinkError );
1875            }
1876
1877            $param = $func->getParameterForCaller( $i );
1878            // @todo Internal funcs that pass by reference. Should we assume that their variables are tainted? Most
1879            // common example is probably preg_match, which may very well be tainted much of the time.
1880            if ( $param && $param->isPassByReference() && !$func->isPHPInternal() ) {
1881                $this->handlePassByRef( $func, $argument, $args, $i, $isHookHandler );
1882            }
1883
1884            /** @phan-return list */
1885            $issueArgsGetter = function () use (
1886                $funcName, $argName, $argument, $paramSinkError, $baseArgError
1887            ): array {
1888                // Always include the ordinal (it helps for repeated arguments)
1889                $taintedArg = $argName;
1890                $argStr = ASTReverter::toShortString( $argument );
1891                if ( strlen( $argStr ) < 25 ) {
1892                    // If we have a short representation of the arg, include it as well.
1893                    $taintedArg .= " (`$argStr`)";
1894                }
1895
1896                return [
1897                    $funcName,
1898                    $this->getCurrentMethod(),
1899                    $taintedArg,
1900                    [ 'lines' => $paramSinkError, 'sink' => true ],
1901                    [ 'lines' => $baseArgError, 'sink' => false ],
1902                ];
1903            };
1904
1905            $this->maybeEmitIssue(
1906                $paramSinkTaint,
1907                $curArgTaintedness,
1908                "Calling method {FUNCTIONLIKE}() in {FUNCTIONLIKE}" .
1909                " that outputs using tainted argument {CODE}.{DETAILS}{DETAILS}",
1910                $issueArgsGetter
1911            );
1912
1913            $preserveArgumentsData[$i] = [ $curArgTaintedness, MethodLinks::emptySingleton(), $baseArgError ];
1914        }
1915
1916        if ( !$computePreserve ) {
1917            return null;
1918        }
1919
1920        $hardcodedPreservedTaint = $this->getHardcodedPreservedTaintForFunc( $func, $preserveArgumentsData );
1921        if ( $hardcodedPreservedTaint ) {
1922            $callLinks = $hardcodedPreservedTaint->getMethodLinks();
1923            $callLineCausedBy = $this->getCausedByLinesToAdd( Taintedness::safeSingleton(), $callLinks );
1924            $callError = $hardcodedPreservedTaint->getError()
1925                ->withAddedLines( $callLineCausedBy, Taintedness::safeSingleton(), $callLinks );
1926            return new TaintednessWithError(
1927                $hardcodedPreservedTaint->getTaintedness(),
1928                $callError,
1929                $callLinks
1930            );
1931        }
1932        $overallTaint = $taint->getOverall();
1933        $combinedArgTaint = Taintedness::safeSingleton();
1934        $combinedArgLinks = MethodLinks::emptySingleton();
1935        $combinedArgErrors = CausedByLines::emptySingleton();
1936        /**
1937         * @var Taintedness $curArgTaintedness
1938         * @var CausedByLines $baseArgError
1939         */
1940        foreach ( $preserveArgumentsData as $i => [ $curArgTaintedness, , $baseArgError ] ) {
1941            if ( $taint->hasParamPreserve( $i ) ) {
1942                $parTaint = $taint->getParamPreservedTaint( $i );
1943                if ( $parTaint->isEmpty() ) {
1944                    continue;
1945                }
1946                $preservedArgTaint = $parTaint->asTaintednessForArgument( $curArgTaintedness );
1947                $curArgLinks = MethodLinks::emptySingleton();
1948                $relevantParamError = $funcError->getParamPreservedLines( $i )
1949                    ->asPreservedForParameter( $curArgTaintedness, $curArgLinks, $func, $i );
1950                $callLineCausedBy = $this->getCausedByLinesToAdd( Taintedness::safeSingleton(), $curArgLinks );
1951                $curArgError = $baseArgError->asPreservedForArgument( $parTaint )
1952                    ->withAddedLines( $callLineCausedBy, Taintedness::safeSingleton(), $curArgLinks )
1953                    ->asMergedWith( $relevantParamError );
1954            } elseif (
1955                $overallTaint->has( SecurityCheckPlugin::PRESERVE_TAINT | SecurityCheckPlugin::UNKNOWN_TAINT )
1956            ) {
1957                // No info for this specific parameter, but the overall function either preserves taint
1958                // when unspecified or is unknown. So just pass the taint through, destroying the shape.
1959                $preservedArgTaint = $curArgTaintedness->asCollapsed();
1960                $curArgLinks = MethodLinks::emptySingleton();
1961                $relevantParamError = $funcError->getParamPreservedLines( $i )
1962                    ->asPreservedForParameter( $curArgTaintedness, $curArgLinks, $func, $i );
1963                $callLineCausedBy = $this->getCausedByLinesToAdd( Taintedness::safeSingleton(), $curArgLinks );
1964                $curArgError = $baseArgError->asAllCollapsed()
1965                    ->withAddedLines( $callLineCausedBy, Taintedness::safeSingleton(), $curArgLinks )
1966                    ->asMergedWith( $relevantParamError );
1967            } else {
1968                // This parameter has no taint info. And overall this function doesn't depend on param
1969                // for taint and isn't unknown. So we consider this argument untainted.
1970                continue;
1971            }
1972
1973            $combinedArgTaint = $combinedArgTaint->asMergedWith( $preservedArgTaint );
1974            $combinedArgLinks = $combinedArgLinks->asMergedWith( $curArgLinks );
1975            // NOTE: If any line inside the callee's body is responsible for preserving the taintedness of more
1976            // than one argument, it will appear once per preserved argument in the overall caused-by of the
1977            // call expression. This is probably a good thing, but can increase the length of caused-by lines.
1978            // TODO Something like T291379 might help here.
1979            $combinedArgErrors = $combinedArgErrors->asMergedWith( $curArgError );
1980        }
1981
1982        $callTaintedness = $overallTaint->without(
1983            SecurityCheckPlugin::PRESERVE_TAINT | SecurityCheckPlugin::ALL_EXEC_TAINT
1984        );
1985        $combinedArgTaint = $combinedArgTaint->without( SecurityCheckPlugin::ALL_EXEC_TAINT );
1986        $callTaintedness = $callTaintedness->asMergedWith( $combinedArgTaint );
1987        $callError = $funcError->getGenericLines()->asMergedWith( $combinedArgErrors );
1988        return new TaintednessWithError( $callTaintedness, $callError, $combinedArgLinks );
1989    }
1990
1991    /**
1992     * Handle calls to special built-in functions.
1993     *
1994     * @param FunctionInterface $func
1995     * @param array $args Arguments to function/method
1996     * @phan-param array<Node|mixed> $args
1997     * @param bool $computePreserve
1998     * @param TaintednessWithError|null &$result
1999     */
2000    private function maybeHandleSpecialCall(
2001        FunctionInterface $func,
2002        array $args,
2003        bool $computePreserve,
2004        ?TaintednessWithError &$result
2005    ): bool {
2006        switch ( ltrim( $func->getName(), '\\' ) ) {
2007            case 'call_user_func':
2008                if ( !$args ) {
2009                    // Syntax error, whatever.
2010                    return false;
2011                }
2012                $callbackNode = array_shift( $args );
2013                // Use the same method as ClosureReturnTypeOverridePlugin to avoid emitting new issues.
2014                $callbacks = UnionTypeVisitor::functionLikeListFromNodeAndContext(
2015                    $this->code_base,
2016                    $this->context,
2017                    $callbackNode,
2018                    false
2019                );
2020
2021                $result = $computePreserve ? TaintednessWithError::emptySingleton() : null;
2022                foreach ( $callbacks as $callback ) {
2023                    // No point in analyzing abstract function declarations
2024                    if ( !$callback instanceof FunctionLikeDeclarationType ) {
2025                        $callTaint = $this->handleMethodCall(
2026                            $callback, $callback->getFQSEN(), $args, $computePreserve
2027                        );
2028                        if ( $result && $callTaint ) {
2029                            $result = $result->asMergedWith( $callTaint );
2030                        }
2031                    }
2032                }
2033                return true;
2034            case 'call_user_func_array':
2035                if ( count( $args ) < 2 ) {
2036                    // Syntax error, whatever.
2037                    return false;
2038                }
2039                // Use the same method as ClosureReturnTypeOverridePlugin to avoid emitting new issues.
2040                $callbacks = UnionTypeVisitor::functionLikeListFromNodeAndContext(
2041                    $this->code_base,
2042                    $this->context,
2043                    $args[0],
2044                    false
2045                );
2046                $callbackArguments = self::extractArrayArgs( $args[1] );
2047                if ( $callbackArguments === null ) {
2048                    return false;
2049                }
2050
2051                $result = $computePreserve ? TaintednessWithError::emptySingleton() : null;
2052                foreach ( $callbacks as $callback ) {
2053                    // No point in analyzing abstract function declarations
2054                    if ( !$callback instanceof FunctionLikeDeclarationType ) {
2055                        $callTaint = $this->handleMethodCall(
2056                            $callback, $callback->getFQSEN(), $callbackArguments, $computePreserve
2057                        );
2058                        if ( $result && $callTaint ) {
2059                            $result = $result->asMergedWith( $callTaint );
2060                        }
2061                    }
2062                }
2063                return true;
2064            default:
2065                return false;
2066        }
2067    }
2068
2069    /**
2070     * Given the node for an array that contains arguments to a function, extract those arguments.
2071     * Shamelessly stolen from phan's ClosureReturnTypeOverridePlugin.
2072     *
2073     * @param Node|mixed $argArrayNode
2074     * @return array<Node|mixed>|null
2075     */
2076    private static function extractArrayArgs( mixed $argArrayNode ): ?array {
2077        if ( !( $argArrayNode instanceof Node ) || $argArrayNode->kind !== \ast\AST_ARRAY ) {
2078            return null;
2079        }
2080        $arguments = [];
2081        foreach ( $argArrayNode->children as $child ) {
2082            if ( !( $child instanceof Node ) ) {
2083                continue;
2084            }
2085            $arguments[] = $child->children['value'];
2086        }
2087        return $arguments;
2088    }
2089
2090    /**
2091     * @todo This should possibly be part of the public interface upstream
2092     * @see \Phan\Analysis\ArgumentType::analyzeParameterListForCallback
2093     * @return array
2094     * @phan-return array{0:int|null,1:Node|mixed,2:?string}
2095     */
2096    private function translateNamedArg( Node $argument, FunctionInterface $func ): array {
2097        [ 'name' => $argName, 'expr' => $argExpr ] = $argument->children;
2098        assert( $argExpr !== null );
2099
2100        foreach ( $func->getRealParameterList() as $i => $parameter ) {
2101            if ( $parameter->getName() === $argName ) {
2102                return [ $i, $argExpr, $argName ];
2103            }
2104        }
2105        return [ null, null, null ];
2106    }
2107
2108    /**
2109     * @param Node $argument
2110     * @param Taintedness $taint
2111     * @param CausedByLines|null $funcError
2112     *
2113     * @todo This has false negatives, because we don't collect function arguments in
2114     * getPhanObjsForNode (we'd have to pass option 'all'), so we can't handle e.g. array_merge
2115     * right now. However, collecting all args would create false positives with functions where
2116     * the arg taint isn't propagated to the return value. Ideally, we'd want to include an argument
2117     * iff the corresponding parameter passes $taint through.
2118     *
2119     * @note It's important that we don't backpropagate taintedness to every returned object in case
2120     * of function calls, but just props and the like (so excluding vars). See test 'toomanydeps'.
2121     */
2122    protected function backpropagateArgTaint(
2123        Node $argument,
2124        Taintedness $taint,
2125        ?CausedByLines $funcError = null
2126    ): void {
2127        if ( $taint->has( SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT ) ) {
2128            // Special case for numkey, we need to "filter" the argument.
2129            // TODO This doesn't return arrays with mixed keys. Currently, doing so would result
2130            // in arrays being considered as a unit, and the taint would be backpropagated to all
2131            // values, even ones with string keys. See TODO in elementCanBeNumkey
2132
2133            // TODO This should be limited to the outer array, see TODO in backpropnumkey test
2134            // Note that this is true in general for NUMKEY taint, not just when backpropagating it
2135            $numkeyTaint = $taint->withOnly( SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT );
2136            $this->markAllDependentMethodsExecForNode( $argument, $numkeyTaint, $funcError, true );
2137            $taint = $taint->without( SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT );
2138        }
2139
2140        $this->markAllDependentMethodsExecForNode( $argument, $taint, $funcError );
2141    }
2142
2143    /**
2144     * Handle pass-by-ref params when examining a function call. Phan handles passbyref by reanalyzing
2145     * the method with PassByReferenceVariable objects instead of Parameters. These objects contain
2146     * the info about the param, but proxy all calls to the underlying argument object.
2147     * We cannot 100% copy that behaviour: inside the function body, the local variable for the pbr param
2148     * would have the same taintedness as the argument, and things like `echo $pbr` would emit an issue
2149     * inside the function, which is unwanted for now. Additionally, it's unclear how we'd add a caused-by
2150     * entry for the line of the function call.
2151     * Hence, instead of adding taintedness to the underlying argument, we put it in a separate prop, which is only
2152     * written but never read inside the function body. Then after the call was analyzed, this method moves
2153     * the taintedness from the "special" prop onto the normal taintedness prop. We do the same thing for links
2154     * (so as to infer which taintedness from the argument is preserved by the function) and caused-by lines.
2155     * TODO In the future we might want to really copy phan's approach, as that would allow us to delete some hacks,
2156     *   and handle conditionals inside the function body more accurately.
2157     *
2158     * @param FunctionInterface $func
2159     * @param Node $argument The argument being passed by-ref
2160     * @param array<Node|mixed> $arguments The full list of arguments for this function call
2161     * @param int $refArgIndex @phan-unused-param
2162     * @param bool $isHookHandler Whether we're analyzing a hook handler for a hook handler call.
2163     *   FIXME This is MW-specific
2164     */
2165    private function handlePassByRef(
2166        FunctionInterface $func,
2167        Node $argument,
2168        array $arguments,
2169        int $refArgIndex,
2170        bool $isHookHandler
2171    ): void {
2172        $argObj = $this->getPassByRefObjFromNode( $argument );
2173        if ( !$argObj ) {
2174            return;
2175        }
2176        $refTaint = self::getTaintednessRef( $argObj );
2177        if ( !$refTaint ) {
2178            // If no ref taint was set, it's likely due to a recursive call or another instance where phan is not
2179            // reanalyzing the callee with PassByReferenceVariable objects.
2180            return;
2181        }
2182
2183        $globalVarObj = $argObj instanceof GlobalVariable ? $argObj->getElement() : null;
2184        // Move the ref taintedness to the "actual" taintedness of the object
2185        // Note: We assume that the order in which hook handlers are called is nondeterministic, thus
2186        // we never override arg taint for reference params in this case.
2187        $overrideTaint = !( $argObj instanceof Property || $globalVarObj || $isHookHandler );
2188        $newTaint = $refTaint;
2189        $refLinks = self::getMethodLinksRef( $argObj );
2190        $refError = self::getCausedByRef( $argObj ) ?? CausedByLines::emptySingleton();
2191        $addedError = CausedByLines::emptySingleton();
2192
2193        // Merge any taintedness from links, indicating that (part of) the original argument "survived" the call.
2194        if ( $refLinks ) {
2195            $linkedParameters = array_column(
2196                array_filter(
2197                    $refLinks->getMethodAndParamTuples(),
2198                    /** @phan-param array{0:FunctionInterface,1:int} $tuple */
2199                    static fn ( array $tuple ): bool => $tuple[0] === $func
2200                ),
2201                1
2202            );
2203        } else {
2204            $linkedParameters = [];
2205        }
2206
2207        if ( in_array( $refArgIndex, $linkedParameters, true ) ) {
2208            // Special handling for the original by-ref variable: process it first for proper caused-by ordering.
2209            $linkedParameters = array_diff( $linkedParameters, [ $refArgIndex ] );
2210            array_unshift( $linkedParameters, $refArgIndex );
2211        } else {
2212            $addedError = $addedError->withAddedLines( $this->getCausedByLinesToAdd( $refTaint, null ), $refTaint );
2213        }
2214
2215        foreach ( $linkedParameters as $paramIdx ) {
2216            if ( !isset( $arguments[$paramIdx] ) || !$arguments[$paramIdx] instanceof Node ) {
2217                // Optional parameter not passed here, or untainted literal.
2218                continue;
2219            }
2220            assert( $refLinks !== null );
2221            $argTaintFull = $this->getTaintedness( $arguments[$paramIdx] );
2222            $argTaint = $argTaintFull->getTaintedness();
2223            $argError = $argTaintFull->getError();
2224
2225            $paramPreservedTaint = $refLinks->asPreservedTaintednessForFuncParam( $func, $paramIdx );
2226            $preservedTaint = $paramPreservedTaint->asTaintednessForArgument( $argTaint );
2227            $newTaint = $preservedTaint->asMergedWith( $newTaint );
2228            $curLineErrorTaint = $paramIdx === $refArgIndex ? $refTaint : $preservedTaint;
2229            $curCallLine = $this->getCausedByLinesToAdd( $curLineErrorTaint, null );
2230            $curAddedError = $argError->asPreservedForArgument( $paramPreservedTaint )
2231                ->withAddedLines( $curCallLine, $curLineErrorTaint )
2232                ->asMergedWith( $refError->asPreservedForParameter( $argTaint, $refLinks, $func, $paramIdx ) );
2233            $addedError = $addedError->asMergedWith( $curAddedError );
2234        }
2235
2236        $newAddedError = $addedError->asMergedWith( $refError );
2237        if ( $overrideTaint ) {
2238            $newError = $newAddedError;
2239        } else {
2240            $prevError = self::getCausedByRaw( $argObj ) ?? CausedByLines::emptySingleton();
2241            $newError = $prevError->asMergedWith( $newAddedError );
2242        }
2243
2244        $this->setTaintedness( $argObj, $newTaint, $overrideTaint );
2245        self::setCausedByRaw( $argObj, $newError );
2246        if ( $globalVarObj ) {
2247            $this->setTaintedness( $globalVarObj, $newTaint, false );
2248            $curGlobalError = self::getCausedByRaw( $globalVarObj ) ?? CausedByLines::emptySingleton();
2249            self::setCausedByRaw( $globalVarObj, $curGlobalError->asMergedWith( $newError ) );
2250        }
2251        // We clear method links since the by-ref call might have modified them, and precise tracking is not
2252        // trivial to implement, and most probably not worth the effort.
2253        self::setMethodLinks( $argObj, MethodLinks::emptySingleton() );
2254        self::clearRefData( $argObj );
2255    }
2256
2257    /**
2258     * Given the node of an argument that is passed by reference, return a list of phan objects
2259     * corresponding to that node.
2260     */
2261    private function getPassByRefObjFromNode( Node $node ): ?TypedElementInterface {
2262        switch ( $node->kind ) {
2263            case \ast\AST_PROP:
2264            case \ast\AST_STATIC_PROP:
2265                return $this->getPropFromNode( $node );
2266            case \ast\AST_VAR:
2267                $cn = $this->getCtxN( $node );
2268                if ( Variable::isHardcodedGlobalVariableWithName( $cn->getVariableName() ) ) {
2269                    return null;
2270                }
2271                try {
2272                    return $cn->getVariable();
2273                } catch ( NodeException | IssueException ) {
2274                    return null;
2275                }
2276            case \ast\AST_DIM:
2277                // Phan doesn't handle this case with PassByReferenceVariable objects, so nothing we can do anyway.
2278                return null;
2279            default:
2280                $this->debug( __METHOD__, 'Unhandled pass-by-ref case: ' . Debug::nodeName( $node ) );
2281                return null;
2282        }
2283    }
2284
2285    /**
2286     * Get the taintedness of the return value of $func (a special-cased internal PHP function) given the taintedness
2287     * of its arguments. Note that this doesn't handle passbyref parameters. If the function is not special-cased,
2288     * returns null.
2289     *
2290     * @param FunctionInterface $func
2291     * @param array<array<Taintedness|MethodLinks|CausedByLines>> $preserveArgumentsData Actual taintedness, links and
2292     * caused-by lines of each argument. Literal arguments aren't included here.
2293     * @phan-param array<int,array{0:Taintedness,1:MethodLinks,2:CausedByLines}> $preserveArgumentsData
2294     * @suppress PhanUnusedVariable
2295     */
2296    private function getHardcodedPreservedTaintForFunc(
2297        FunctionInterface $func,
2298        array $preserveArgumentsData
2299    ): ?TaintednessWithError {
2300        switch ( ltrim( $func->getName(), '\\' ) ) {
2301            // Functions that return one element of the array (first and only parameter)
2302            case 'array_pop':
2303            case 'array_shift':
2304            case 'current':
2305            case 'end':
2306            case 'next':
2307            case 'pos':
2308            case 'prev':
2309            case 'reset':
2310                if ( !isset( $preserveArgumentsData[0] ) ) {
2311                    return TaintednessWithError::emptySingleton();
2312                }
2313                $taint = $preserveArgumentsData[0][0]->asValueFirstLevel();
2314                $links = MethodLinks::emptySingleton();
2315                $error = $preserveArgumentsData[0][2]->asAllValueFirstLevel()->asIntersectedWithTaintedness( $taint );
2316                return new TaintednessWithError( $taint, $error, $links );
2317            case 'array_values':
2318                // Same taintedness as the original array (first and only param), but with safe keys and numkey.
2319                if ( !isset( $preserveArgumentsData[0] ) ) {
2320                    return TaintednessWithError::emptySingleton();
2321                }
2322                $taint = $preserveArgumentsData[0][0]->withoutKeys();
2323                if ( $taint->has( SecurityCheckPlugin::SQL_TAINT ) ) {
2324                    $taint = $taint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT );
2325                }
2326                $links = MethodLinks::emptySingleton();
2327                $error = $preserveArgumentsData[0][2]->asAllCollapsed()->asIntersectedWithTaintedness( $taint );
2328                return new TaintednessWithError( $taint, $error, $links );
2329            // Functions that return a key from the array (first and only parameter)
2330            case 'key':
2331            case 'array_key_first':
2332            case 'array_key_last':
2333            // array_keys returns all keys from the array (first param), and can also take two more parameters
2334            // that don't contribute to the resulting taintedness.
2335            case 'array_keys':
2336                if ( !isset( $preserveArgumentsData[0] ) ) {
2337                    return TaintednessWithError::emptySingleton();
2338                }
2339                $taint = $preserveArgumentsData[0][0]->asKeyForForeach();
2340                $links = MethodLinks::emptySingleton();
2341                $error = $preserveArgumentsData[0][2]->asAllKeyForForeach()->asIntersectedWithTaintedness( $taint );
2342                return new TaintednessWithError( $taint, $error, $links );
2343            case 'array_change_key_case':
2344                // The overall shape remains the same, but the keys of the outermost array (first param) have different
2345                // case. Second param (lower vs upper) is safe.
2346                if ( !isset( $preserveArgumentsData[0] ) ) {
2347                    return TaintednessWithError::emptySingleton();
2348                }
2349                // TODO: actually handle case changes!
2350                $taint = $preserveArgumentsData[0][0];
2351                $links = MethodLinks::emptySingleton();
2352                $error = $preserveArgumentsData[0][2]->asIntersectedWithTaintedness( $taint );
2353                return new TaintednessWithError( $taint, $error, $links );
2354            case 'array_flip':
2355                // Swaps keys and values of the array (first and only param)
2356                if ( !isset( $preserveArgumentsData[0] ) ) {
2357                    return TaintednessWithError::emptySingleton();
2358                }
2359                $taint = $preserveArgumentsData[0][0]->asKeyForForeach()
2360                    ->withAddedKeysTaintedness( $preserveArgumentsData[0][0]->asValueFirstLevel()->get() );
2361                $links = MethodLinks::emptySingleton();
2362                $error = $preserveArgumentsData[0][2]->asAllCollapsed()->asIntersectedWithTaintedness( $taint );
2363                return new TaintednessWithError( $taint, $error, $links );
2364            case 'implode':
2365            case 'join':
2366                // This function can be called in three different ways:
2367                // - implode( $string, $array ) -> joins elements in $array using $string
2368                // - implode( $array ) -> joins elements in $array using the empty string
2369                // - implode( $array, $string ) -> same as the first one but inverted params, removed in PHP 8,
2370                // not supported here
2371                if ( isset( $preserveArgumentsData[0] ) ) {
2372                    $joinerTaint = $preserveArgumentsData[0][0]->asCollapsed();
2373                    $joinerLinks = MethodLinks::emptySingleton();
2374                    $joinerError = $preserveArgumentsData[0][2]->asAllCollapsed()
2375                        ->asIntersectedWithTaintedness( $joinerTaint );
2376                }
2377                $combinedTaint = $joinerTaint ?? Taintedness::safeSingleton();
2378                $combinedLinks = $joinerLinks ?? MethodLinks::emptySingleton();
2379                $combinedError = $joinerError ?? CausedByLines::emptySingleton();
2380                if ( isset( $preserveArgumentsData[1] ) ) {
2381                    $arrayTaint = $preserveArgumentsData[1][0]->withoutKeys()->asCollapsed();
2382                    $arrayLinks = MethodLinks::emptySingleton();
2383                    $arrayError = $preserveArgumentsData[1][2]->asAllCollapsed();
2384
2385                    $combinedTaint = $combinedTaint->asMergedWith( $arrayTaint );
2386                    $combinedLinks = $combinedLinks->asMergedWith( $arrayLinks );
2387                    $combinedError = $combinedError->asMergedWith(
2388                        $arrayError->asIntersectedWithTaintedness( $arrayTaint )
2389                    );
2390                }
2391                return new TaintednessWithError( $combinedTaint, $combinedError, $combinedLinks );
2392            case 'array_fill':
2393                // array_fill( $start, $count, $value ) creates an array with $count copies of $value, starting
2394                // at key $start. The first two params are integers, and thus safe.
2395                if ( !isset( $preserveArgumentsData[2] ) ) {
2396                    return TaintednessWithError::emptySingleton();
2397                }
2398                $preservedArgTaint = $preserveArgumentsData[2][0];
2399                // TODO: We may actually be able to infer the actual keys, instead of setting as unknown
2400                $taint = Taintedness::newFromShape( [], $preservedArgTaint );
2401                $links = MethodLinks::emptySingleton();
2402                // TODO: We should also add numkey if the argument has sql.
2403                $error = $preserveArgumentsData[2][2]->asAllCollapsed()
2404                    ->asIntersectedWithTaintedness( $preservedArgTaint );
2405                return new TaintednessWithError( $taint, $error, $links );
2406            case 'array_fill_keys':
2407                // array_fill_keys( $keys, $value ) creates an array whose keys are the element in $keys, and whose
2408                // values are all equal to $value.
2409                $taint = Taintedness::safeSingleton();
2410                $links = MethodLinks::emptySingleton();
2411                $error = CausedByLines::emptySingleton();
2412                if ( isset( $preserveArgumentsData[0] ) ) {
2413                    $keysTaintedness = $preserveArgumentsData[0][0]->asValueFirstLevel();
2414                    $keysLinks = MethodLinks::emptySingleton();
2415                    $keysError = $preserveArgumentsData[0][2]->asAllCollapsed();
2416
2417                    $taint = $taint->withAddedKeysTaintedness( $keysTaintedness->get() );
2418                    $links = $links->withKeysLinks( $keysLinks->getLinksCollapsing() );
2419                    $error = $error->asMergedWith(
2420                        $keysError->asIntersectedWithTaintedness( $taint )
2421                    );
2422                }
2423                if ( isset( $preserveArgumentsData[1] ) ) {
2424                    $valueTaint = $preserveArgumentsData[1][0];
2425                    $valueLinks = MethodLinks::emptySingleton();
2426                    $valueError = $preserveArgumentsData[1][2]->asAllCollapsed();
2427
2428                    $taint = $taint->withAddedOffsetTaintedness( null, $valueTaint );
2429                    $links = $links->withLinksAtDim( null, $valueLinks );
2430                    $error = $error->asMergedWith(
2431                        $valueError->asIntersectedWithTaintedness( $valueTaint )
2432                    );
2433                }
2434                return new TaintednessWithError( $taint, $error, $links );
2435            case 'array_combine':
2436                // array_fill_keys( $keys, $values ) creates an array whose keys are the element in $keys, and whose
2437                // values the elements in $values.
2438                $taint = Taintedness::safeSingleton();
2439                $links = MethodLinks::emptySingleton();
2440                $error = CausedByLines::emptySingleton();
2441                if ( isset( $preserveArgumentsData[0] ) ) {
2442                    $keysTaintedness = $preserveArgumentsData[0][0]->asValueFirstLevel();
2443                    $keysLinks = MethodLinks::emptySingleton();
2444                    $keysError = $preserveArgumentsData[0][2]->asAllCollapsed();
2445
2446                    $taint = $taint->withAddedKeysTaintedness( $keysTaintedness->get() );
2447                    $links = $links->withKeysLinks( $keysLinks->getLinksCollapsing() );
2448                    $error = $error->asMergedWith(
2449                        $keysError->asIntersectedWithTaintedness( $taint )
2450                    );
2451                }
2452                if ( isset( $preserveArgumentsData[1] ) ) {
2453                    $valueTaint = $preserveArgumentsData[1][0]->withoutKeys();
2454                    $valueLinks = MethodLinks::emptySingleton();
2455                    $valueError = $preserveArgumentsData[1][2]->asAllCollapsed();
2456
2457                    $taint = $taint->asMergedWith( $valueTaint );
2458                    $links = $links->asMergedWith( $valueLinks );
2459                    $error = $error->asMergedWith(
2460                        $valueError->asIntersectedWithTaintedness( $valueTaint )
2461                    );
2462                }
2463                return new TaintednessWithError( $taint, $error, $links );
2464            case 'array_unique':
2465                // Removes duplicate from an array (first param). We can't tell what gets removed, and what's the effect
2466                // of this function on array keys. Second param is safe.
2467                if ( !isset( $preserveArgumentsData[0] ) ) {
2468                    return TaintednessWithError::emptySingleton();
2469                }
2470                $taint = $preserveArgumentsData[0][0]->asKnownKeysMadeUnknown();
2471                $links = MethodLinks::emptySingleton();
2472                $error = $preserveArgumentsData[0][2]->asAllCollapsed()->asIntersectedWithTaintedness( $taint );
2473                return new TaintednessWithError( $taint, $error, $links );
2474            case 'array_diff':
2475            case 'array_diff_assoc':
2476                // - array_diff( $arr, $x_1, ..., $x_n ) returns elements in $arr that are NOT in any of the $x_i.
2477                //   The equality of two elements is determined by looking at their values.
2478                //   Only the first argument contributes to the preserved taintedness.
2479                // - array_diff_assoc does the same, but two elements are considered equal if they have the same value
2480                //   AND the same key.
2481                if ( !isset( $preserveArgumentsData[0] ) ) {
2482                    return TaintednessWithError::emptySingleton();
2483                }
2484                // We can't infer shape mutations because Taintedness doesn't keep track of the values, so just
2485                // return the taintedness of the first argument.
2486                $preservedArgTaint = $preserveArgumentsData[0][0];
2487                $preservedArgLinks = MethodLinks::emptySingleton();
2488                return new TaintednessWithError(
2489                    $preservedArgTaint,
2490                    $preserveArgumentsData[0][2]->asIntersectedWithTaintedness( $preservedArgTaint ),
2491                    $preservedArgLinks
2492                );
2493            case 'array_diff_key':
2494                // array_diff_key( $arr, $x_1, ..., $x_n ) is similar to array_diff, but here two elements are
2495                // considered equal if they have the same key (regardless of the value).
2496                if ( !isset( $preserveArgumentsData[0] ) ) {
2497                    return TaintednessWithError::emptySingleton();
2498                }
2499                /** @var Taintedness $taint */
2500                [ $taint, $links, $error ] = array_shift( $preserveArgumentsData );
2501                foreach ( $preserveArgumentsData as $argData ) {
2502                    $taint = $taint->withoutKnownKeysFrom( $argData[0] );
2503                    // No argument besides the first one can contribute to caused-by lines, although
2504                    // ideally we would remove the current error from $error.
2505                }
2506                // The shape is destroyed to avoid pretending that we know anything about the final shape of the array.
2507                return new TaintednessWithError(
2508                    $taint->asKnownKeysMadeUnknown(),
2509                    $error->asAllCollapsed(),
2510                    $links
2511                );
2512            case 'array_intersect':
2513            case 'array_intersect_assoc':
2514                // - array_intersect( $arr_1, ..., $arr_n ) returns an array of elements that are in ALL of the $x_i.
2515                //   The equality of two elements is determined by looking at their values.
2516                //   Only values from the first array are used for the return value.
2517                // - array_intersect_assoc does the same, but two elements are considered equal if they have the same
2518                //   value AND the same key.
2519                if ( !$preserveArgumentsData ) {
2520                    return TaintednessWithError::emptySingleton();
2521                }
2522                // Note: we can't do an actual intersect on the values because Taintedness does not store them, but
2523                // intersecting the taintedness flags, although not perfect, is correct and approximates that.
2524                // The shape is destroyed to avoid pretending that we know anything about the final shape of the array.
2525                /** @var Taintedness $taint */
2526                [ $taint, $links, $error ] = array_shift( $preserveArgumentsData );
2527                $taint = $taint->asKnownKeysMadeUnknown();
2528                foreach ( $preserveArgumentsData as $argData ) {
2529                    $taint = $taint->withOnly( $argData[0]->get() );
2530                    // No argument besides the first one can contribute to caused-by lines, although
2531                    // ideally we would intersect $error with the current error.
2532                }
2533                return new TaintednessWithError( $taint, $error->asAllCollapsed(), $links );
2534            case 'array_intersect_key':
2535                // array_intersect_key( $arr, $x_1, ..., $x_n ) is similar to array_intersect, but here two elements are
2536                // considered equal if they have the same key (irregardless of the value).
2537                if ( !isset( $preserveArgumentsData[0] ) ) {
2538                    return TaintednessWithError::emptySingleton();
2539                }
2540                // We can't infer shape mutations because there might be unknown keys in either argument, so just
2541                // return the taintedness of the first argument.
2542                $preservedArgTaint = $preserveArgumentsData[0][0];
2543                $preservedArgLinks = MethodLinks::emptySingleton();
2544                return new TaintednessWithError(
2545                    $preservedArgTaint,
2546                    $preserveArgumentsData[0][2]->asIntersectedWithTaintedness( $preservedArgTaint ),
2547                    $preservedArgLinks
2548                );
2549            // TODO The last parameter of these functions is a callback, so probably hard to handle. They're also
2550            // variadic, so we'd need to know the arg type to analyze the callback.
2551            case 'array_diff_uassoc':
2552            case 'array_diff_ukey':
2553            case 'array_intersect_uassoc':
2554            case 'array_intersect_ukey':
2555            case 'array_udiff':
2556            case 'array_udiff_assoc':
2557            case 'array_uintersect':
2558            case 'array_uintersect_assoc':
2559            // The last two params of these are callbacks, so twice as hard
2560            case 'array_udiff_uassoc':
2561            case 'array_uintersect_uassoc':
2562                // Only the taintedness from first argument is preserved.
2563                if ( !isset( $preserveArgumentsData[0] ) ) {
2564                    return TaintednessWithError::emptySingleton();
2565                }
2566                $preservedArgTaint = $preserveArgumentsData[0][0];
2567                $preservedArgLinks = MethodLinks::emptySingleton();
2568                return new TaintednessWithError(
2569                    $preservedArgTaint,
2570                    $preserveArgumentsData[0][2]->asIntersectedWithTaintedness( $preservedArgTaint ),
2571                    $preservedArgLinks
2572                );
2573            case 'array_map':
2574                // array_map( $cb, $arr, $arr_1, ..., $arr_n ) returns the result of applying $cb to all the array
2575                // arguments, element by element.
2576                // TODO: Analyze the callback. For now we only preserve taintedness of array arguments.
2577                unset( $preserveArgumentsData[0] );
2578                $taint = Taintedness::safeSingleton();
2579                $links = MethodLinks::emptySingleton();
2580                $error = CausedByLines::emptySingleton();
2581                foreach ( $preserveArgumentsData as [ $argTaint, $argLinks, $argError ] ) {
2582                    $preservedArgTaint = $argTaint->asCollapsed();
2583                    $preservedArgLinks = MethodLinks::emptySingleton();
2584                    $preservedArgError = $argError->asAllCollapsed();
2585
2586                    $taint = $taint->asMergedWith( $preservedArgTaint );
2587                    $links = $links->asMergedWith( $preservedArgLinks );
2588                    $error = $error->asMergedWith(
2589                        $preservedArgError->asIntersectedWithTaintedness( $preservedArgTaint )
2590                    );
2591                }
2592                return new TaintednessWithError( $taint, $error, $links );
2593            case 'array_filter':
2594                // array_filter( $arr, $cb, $mode ) filters the $arr by using $cb.
2595                // TODO: Analyze the callback. For now we preserve the whole taintedness of the array.
2596                if ( !isset( $preserveArgumentsData[0] ) ) {
2597                    return TaintednessWithError::emptySingleton();
2598                }
2599                $preservedArgTaint = $preserveArgumentsData[0][0]->asKnownKeysMadeUnknown();
2600                $preservedArgLinks = MethodLinks::emptySingleton();
2601                $preservedArgError = $preserveArgumentsData[0][2]->asAllCollapsed();
2602                return new TaintednessWithError(
2603                    $preservedArgTaint,
2604                    $preservedArgError->asIntersectedWithTaintedness( $preservedArgTaint ),
2605                    $preservedArgLinks
2606                );
2607            case 'array_reduce':
2608                // array_reduce( $arr, $cb, $initial ) applies $cb to $arr to obtain a single value.
2609                // TODO: Analyze the callback. For now we preserve the whole taintedness of the array.
2610                if ( !isset( $preserveArgumentsData[0] ) ) {
2611                    return TaintednessWithError::emptySingleton();
2612                }
2613                $preservedArgTaint = $preserveArgumentsData[0][0]->asCollapsed();
2614                $preservedArgLinks = MethodLinks::emptySingleton();
2615                $preservedArgError = $preserveArgumentsData[0][2]->asAllCollapsed();
2616                return new TaintednessWithError(
2617                    $preservedArgTaint,
2618                    $preservedArgError->asIntersectedWithTaintedness( $preservedArgTaint ),
2619                    $preservedArgLinks
2620                );
2621            case 'array_reverse':
2622                // array_reverse( $arr, $preserveKeys ) reverses the order of an array. String keys are always
2623                // preserved, the second param controls whether int keys are also preserved.
2624                // TODO: By knowing the value of the second arg, we could improve this by:
2625                // - Removing only int keys if false
2626                // - Preserving the whole shape if true
2627                if ( !isset( $preserveArgumentsData[0] ) ) {
2628                    return TaintednessWithError::emptySingleton();
2629                }
2630                $preservedArgTaint = $preserveArgumentsData[0][0]->asKnownKeysMadeUnknown();
2631                $preservedArgLinks = MethodLinks::emptySingleton();
2632                $preservedArgError = $preserveArgumentsData[0][2]->asAllCollapsed();
2633                return new TaintednessWithError(
2634                    $preservedArgTaint,
2635                    $preservedArgError->asIntersectedWithTaintedness( $preservedArgTaint ),
2636                    $preservedArgLinks
2637                );
2638            case 'array_pad':
2639                // array_pad( $arr, $length, $val ) returns a copy of $arr padded to the size specified by $length
2640                // by adding copies of $val.
2641                if ( isset( $preserveArgumentsData[0] ) ) {
2642                    $taint = $preserveArgumentsData[0][0];
2643                    $links = MethodLinks::emptySingleton();
2644                    $error = $preserveArgumentsData[0][2]->asIntersectedWithTaintedness( $taint );
2645                } else {
2646                    $taint = Taintedness::safeSingleton();
2647                    $links = MethodLinks::emptySingleton();
2648                    $error = CausedByLines::emptySingleton();
2649                }
2650                if ( isset( $preserveArgumentsData[2] ) ) {
2651                    $valArgTaint = $preserveArgumentsData[2][0];
2652                    $valArgLinks = MethodLinks::emptySingleton();
2653                    $valArgError = $preserveArgumentsData[2][2]->asAllCollapsed();
2654
2655                    $taint = $taint->withAddedOffsetTaintedness( null, $valArgTaint );
2656                    $links = $links->withLinksAtDim( null, $valArgLinks );
2657                    $error = $error->asMergedWith(
2658                        $valArgError->asIntersectedWithTaintedness( $valArgTaint )
2659                    );
2660                }
2661                return new TaintednessWithError( $taint, $error, $links );
2662            case 'array_slice':
2663                // array_slice( $arr, $offset, $len, $preserveKeys ) returns the segment of $arr starting at $offset
2664                // and of size $len. String keys are always preserved, $preserveKeys controls whether int keys
2665                // are also preserved.
2666                if ( !isset( $preserveArgumentsData[0] ) ) {
2667                    return TaintednessWithError::emptySingleton();
2668                }
2669                $preservedArgTaint = $preserveArgumentsData[0][0]->asKnownKeysMadeUnknown();
2670                $preservedArgLinks = MethodLinks::emptySingleton();
2671                $preservedArgError = $preserveArgumentsData[0][2]->asAllCollapsed();
2672                return new TaintednessWithError(
2673                    $preservedArgTaint,
2674                    $preservedArgError->asIntersectedWithTaintedness( $preservedArgTaint ),
2675                    $preservedArgLinks
2676                );
2677            case 'array_replace':
2678                // array_replace( $arr, $rep_1, ..., $rep_n ) returns a copy of $arr where each element is replaced
2679                // with the element having the same key in the rightmost argument.
2680                if ( !isset( $preserveArgumentsData[0] ) ) {
2681                    return TaintednessWithError::emptySingleton();
2682                }
2683                $firstArgData = array_shift( $preserveArgumentsData );
2684                /** @var Taintedness $taint */
2685                $taint = $firstArgData[0];
2686                $links = MethodLinks::emptySingleton();
2687                $error = $firstArgData[2]->asIntersectedWithTaintedness( $taint );
2688                foreach ( $preserveArgumentsData as [ $argTaint, $argLinks, $argError ] ) {
2689                    $taint = $taint->asArrayReplaceWith( $argTaint );
2690                    // Note: we may be adding too many caused-by lines here
2691                    $error = $error->asMergedWith(
2692                        $argError->asAllCollapsed()->asIntersectedWithTaintedness( $argTaint )
2693                    );
2694                }
2695                return new TaintednessWithError( $taint, $error, $links );
2696            case 'array_merge':
2697                // array_merge( $arr_1, ... $arr_n ) merges the given array arguments. If any two (or more) input arrays
2698                // have the same string key, the value from the rightmost argument with that key will be used. Integer
2699                // keys are always appended, and never replaced. Additionally, integer keys in the resulting array
2700                // will be renumbered incrementally starting from 0.
2701                if ( !$preserveArgumentsData ) {
2702                    return TaintednessWithError::emptySingleton();
2703                }
2704                /** @var Taintedness $taint */
2705                /** @var CausedByLines $error */
2706                [ $taint, $links, $error ] = array_shift( $preserveArgumentsData );
2707                foreach ( $preserveArgumentsData as [ $argTaint, $argLinks, $argError ] ) {
2708                    $taint = $taint->asArrayMergeWith( $argTaint );
2709                    $error = $error->asMergedWith( $argError->asIntersectedWithTaintedness( $argTaint ) );
2710                }
2711                return new TaintednessWithError( $taint, $error, $links );
2712            // TODO Handle these with recursion.
2713            case 'array_merge_recursive':
2714            case 'array_replace_recursive':
2715                $taint = Taintedness::safeSingleton();
2716                $links = MethodLinks::emptySingleton();
2717                $error = CausedByLines::emptySingleton();
2718                foreach ( $preserveArgumentsData as [ $curArgTaintedness, $baseArgLinks, $baseArgError ] ) {
2719                    $preservedArgTaint = $curArgTaintedness->asKnownKeysMadeUnknown();
2720                    $preservedArgLinks = MethodLinks::emptySingleton();
2721                    $preservedArgError = $baseArgError->asAllCollapsed();
2722
2723                    $taint = $taint->asMergedWith( $preservedArgTaint );
2724                    $links = $links->asMergedWith( $preservedArgLinks );
2725                    $error = $error->asMergedWith(
2726                        $preservedArgError->asIntersectedWithTaintedness( $preservedArgTaint )
2727                    );
2728                }
2729                return new TaintednessWithError( $taint, $error, $links );
2730            case 'array_chunk':
2731                // array_chunk( $array, $length, $preserve_keys = false ) returns a list of chunks of $array. The keys
2732                // in each chunk are the same of $array if $preserve_keys is true. Else, they're just numbers.
2733                if ( !isset( $preserveArgumentsData[0] ) ) {
2734                    return TaintednessWithError::emptySingleton();
2735                }
2736                // TODO: Check value of $preserve_keys to determine the key taintedness more accurately.
2737                // For now, we just assume that keys are preserved.
2738                $taint = Taintedness::newFromShape( [], $preserveArgumentsData[0][0]->asKnownKeysMadeUnknown() );
2739                $links = MethodLinks::emptySingleton();
2740                $error = $preserveArgumentsData[0][2]->asAllCollapsed()->asIntersectedWithTaintedness( $taint );
2741                return new TaintednessWithError( $taint, $error, $links );
2742            default:
2743                return null;
2744        }
2745    }
2746
2747    /**
2748     * Given a binary operator, compute which taint will be preserved. Safe ops don't preserve
2749     * any taint, whereas unsafe ops will preserve all taints. The taint of a binop is basically
2750     * ( lhs_taint | rhs_taint ) & taint_mask
2751     *
2752     * @warning This method should avoid computing the taint of $lhs and $rhs, because it might be
2753     * called in preorder, but it would trigger a postorder visit.
2754     *
2755     * @param Node $opNode
2756     * @param Node|mixed $lhs Either a Node or a scalar
2757     * @param Node|mixed $rhs Either a Node or a scalar
2758     */
2759    protected function getBinOpTaintMask( Node $opNode, mixed $lhs, mixed $rhs ): int {
2760        static $safeBinOps = [
2761            \ast\flags\BINARY_BOOL_XOR,
2762            \ast\flags\BINARY_DIV,
2763            \ast\flags\BINARY_IS_EQUAL,
2764            \ast\flags\BINARY_IS_IDENTICAL,
2765            \ast\flags\BINARY_IS_NOT_EQUAL,
2766            \ast\flags\BINARY_IS_NOT_IDENTICAL,
2767            \ast\flags\BINARY_IS_SMALLER,
2768            \ast\flags\BINARY_IS_SMALLER_OR_EQUAL,
2769            \ast\flags\BINARY_MOD,
2770            \ast\flags\BINARY_MUL,
2771            \ast\flags\BINARY_POW,
2772            // BINARY_ADD handled below due to array addition.
2773            \ast\flags\BINARY_SUB,
2774            \ast\flags\BINARY_BOOL_AND,
2775            \ast\flags\BINARY_BOOL_OR,
2776            \ast\flags\BINARY_IS_GREATER,
2777            \ast\flags\BINARY_IS_GREATER_OR_EQUAL,
2778            \ast\flags\BINARY_SHIFT_LEFT,
2779            \ast\flags\BINARY_SHIFT_RIGHT,
2780            \ast\flags\BINARY_SPACESHIP,
2781        ];
2782
2783        // This list is mostly used for debugging purposes
2784        static $knownUnsafeOps = [
2785            \ast\flags\BINARY_ADD,
2786            \ast\flags\BINARY_CONCAT,
2787            \ast\flags\BINARY_COALESCE,
2788            // The result of bitwise ops can be a string, so we err on the side of caution.
2789            \ast\flags\BINARY_BITWISE_AND,
2790            \ast\flags\BINARY_BITWISE_OR,
2791            \ast\flags\BINARY_BITWISE_XOR,
2792            // Note, the pipe operator is also special-cased in TaintednessVisitor
2793            \ast\flags\BINARY_PIPE,
2794        ];
2795
2796        if ( in_array( $opNode->flags, $safeBinOps, true ) ) {
2797            return SecurityCheckPlugin::NO_TAINT;
2798        }
2799        if (
2800            $opNode->flags === \ast\flags\BINARY_ADD &&
2801            ( !$this->nodeCanBeArray( $lhs ) || !$this->nodeCanBeArray( $rhs ) )
2802        ) {
2803            // Array addition is the only way `+` can preserve taintedness; if at least one operand
2804            // is definitely NOT an array, then the result will be an integer, or a fatal error will
2805            // occurr (depending on the other operand). Note that if we cannot be 100% sure that the
2806            // node cannot be an array (e.g. if it has mixed type), we err on the side of caution and
2807            // consider it potentially tainted.
2808            return SecurityCheckPlugin::NO_TAINT;
2809        }
2810
2811        if ( !in_array( $opNode->flags, $knownUnsafeOps, true ) ) {
2812            $this->debug(
2813                __METHOD__,
2814                'Unhandled binop ' . Debug::astFlagDescription( $opNode->flags, $opNode->kind )
2815            );
2816        }
2817
2818        return SecurityCheckPlugin::ALL_TAINT_FLAGS;
2819    }
2820
2821    /**
2822     * Get the possible UnionType of a node, without emitting issues.
2823     */
2824    protected function getNodeType( Node $node ): ?UnionType {
2825        // Don't emit issues, as this method might be called e.g. on a LHS (see T249647).
2826        // FIXME Improve this. Is it still necessary now that we cache taintedness?
2827        $catchIssueException = false;
2828        // And since we don't emit issues, use a cloned context so phan won't cache any union type. In particular,
2829        // in the event of possibly-undefined union types, the issue about a variable being possibly undeclared would
2830        // get lost, because we don't emit it, and phan will cache the union type without the undefined bit.
2831        $ctx = clone $this->context;
2832        try {
2833            return UnionTypeVisitor::unionTypeFromNode(
2834                $this->code_base,
2835                $ctx,
2836                $node,
2837                $catchIssueException
2838            );
2839        } catch ( IssueException $e ) {
2840            $this->debug( __METHOD__, "Got error " . $this->getDebugInfo( $e ) );
2841            return null;
2842        }
2843    }
2844
2845    /**
2846     * Given a Node, is it an array? (And definitely not a string)
2847     *
2848     * @param Node|mixed $node A node object or simple value from AST tree
2849     * @return bool Is it an array?
2850     */
2851    protected function nodeIsArray( mixed $node ): bool {
2852        if ( !( $node instanceof Node ) ) {
2853            // simple literal
2854            return false;
2855        }
2856        if ( $node->kind === \ast\AST_ARRAY ) {
2857            // Exit early in the simple case.
2858            return true;
2859        }
2860        $type = $this->getNodeType( $node );
2861        return $type && $type->hasArrayLike( $this->code_base ) &&
2862            !$type->hasMixedOrNonEmptyMixedType() && !$type->hasStringType();
2863    }
2864
2865    /**
2866     * Can $node potentially be an array?
2867     *
2868     * @param Node|mixed $node
2869     */
2870    protected function nodeCanBeArray( mixed $node ): bool {
2871        if ( !( $node instanceof Node ) ) {
2872            return is_array( $node );
2873        }
2874        $type = $this->getNodeType( $node );
2875        if ( !$type ) {
2876            return true;
2877        }
2878        $type = $type->getRealUnionType();
2879        return $type->hasArrayLike( $this->code_base ) || $type->hasMixedOrNonEmptyMixedType() || $type->isEmpty();
2880    }
2881
2882    /**
2883     * Given a Node, is it a string?
2884     *
2885     * @todo Unclear if this should return true for things that can
2886     *   autocast to a string (e.g. ints)
2887     * @param Node|mixed $node A node object or simple value from AST tree
2888     * @return bool Is it a string?
2889     */
2890    protected function nodeCanBeString( mixed $node ): bool {
2891        if ( !( $node instanceof Node ) ) {
2892            // simple literal
2893            return is_string( $node );
2894        }
2895        $type = $this->getNodeType( $node );
2896        // @todo Should having mixed type result in returning false here?
2897        return $type && $type->hasStringType();
2898    }
2899
2900    /**
2901     * @param TypedElementInterface $el
2902     * @param bool $definitely Whether $el is *definitely* numkey, not just possibly
2903     */
2904    protected function elementCanBeNumkey( TypedElementInterface $el, bool $definitely ): bool {
2905        $type = $el->getUnionType()->getRealUnionType();
2906        if ( $type->hasMixedOrNonEmptyMixedType() || $type->isEmpty() ) {
2907            return !$definitely;
2908        }
2909        if ( !$type->hasArray() ) {
2910            return false;
2911        }
2912
2913        $keyTypes = GenericArrayType::keyUnionTypeFromTypeSetStrict( $el->getUnionType()->getRealTypeSet() );
2914        // NOTE: This might lead to false positives if the array has mixed keys, but since we're talking about
2915        // SQLi, we prefer false positives. Also, the mixed keys case isn't fully handled, see backpropagateArgTaint
2916        return $definitely
2917            ? $keyTypes === GenericArrayType::KEY_INT
2918            : ( $keyTypes & GenericArrayType::KEY_INT ) !== 0;
2919    }
2920
2921    /**
2922     * Given a Node that is used as array key, can the key be integer?
2923     * Floats are not considered ints here.
2924     * Note: this method cannot be 100% accurate. First, we don't use the real type, so we may have a false positive
2925     * if e.g. a parameter is annotated as string but the argument is an int. Second, even if something has a real type
2926     * and is not an integer, it could be a string that gets autocast to an integer.
2927     *
2928     * @param Node|mixed $node A node object or simple value from AST tree
2929     * @return bool Is it an int?
2930     * @fixme A lot of duplication with other similar methods...
2931     */
2932    protected function nodeCanBeIntKey( mixed $node ): bool {
2933        if ( !( $node instanceof Node ) ) {
2934            // simple number; floats are autocast to integers (with a warning)
2935            if ( is_int( $node ) || is_float( $node ) ) {
2936                return true;
2937            }
2938            // Strings that are canonical representation of numbers are coerced to int keys.
2939            $testArr = [ $node => 'foo' ];
2940            $key = key( $testArr );
2941            return is_int( $key );
2942        }
2943        $type = $this->getNodeType( $node );
2944        if ( !$type ) {
2945            return true;
2946        }
2947        return $type->hasIntType() || $type->hasMixedOrNonEmptyMixedType() || $type->isEmpty();
2948    }
2949
2950    /**
2951     * Get the phan objects from the return line of a Func/Method
2952     *
2953     * This is primarily used to handle the case where a method
2954     * returns a member (e.g. return $this->foo), and then something
2955     * else does something evil with it - e.g. echo $someObj->getFoo().
2956     * This allows keeping track that $this->foo is outputted, so if
2957     * somewhere else in the code someone calls $someObj->setFoo( $unsafe )
2958     * we can trigger a warning.
2959     *
2960     * This of course will only work in simple cases. It may also potentially
2961     * have false positives if one instance is used solely for escaped stuff
2962     * and a different instance is used for unsafe values that are later
2963     * escaped, as all the different instances are treated the same.
2964     *
2965     * It needs the return statement to be trivial (e.g. return $this->foo;). It
2966     * will not work even with something as simple as $a = $this->foo; return $a;
2967     * However, this code path will only happen if the plugin encounters the
2968     * code to output the value prior to reading the code that sets the value to
2969     * something evil. The other code path where the set happens first is much
2970     * more robust and hopefully the more common code path.
2971     *
2972     * @param FunctionInterface $func The function/method. Must use Analyzable trait
2973     * @return TypedElementInterface[] An array of phan objects
2974     */
2975    public function getReturnObjsOfFunc( FunctionInterface $func ): array {
2976        $retObjs = self::getRetObjs( $func );
2977        if ( $retObjs === null ) {
2978            // We still have to see the function. Analyze it now.
2979            $this->analyzeFunc( $func );
2980            $retObjs = self::getRetObjs( $func );
2981            if ( $retObjs === null ) {
2982                // If it still doesn't exist, perhaps we reached the recursion limit, or it may be a recursive
2983                // function, or a kind of function that we can't handle.
2984                return [];
2985            }
2986        }
2987
2988        // Note that if a function is recursively calling itself, this list might be incomplete.
2989        // This could be remediated with another dynamic property (e.g. retObjsCollected), initialized
2990        // inside visitMethod in preorder, and set to true inside visitMethod in postorder.
2991        // It would be pointless, though, as returning a partial list is better than returning no list.
2992        return array_filter(
2993            $retObjs,
2994            static function ( TypedElementInterface $el ): bool {
2995                return !( $el instanceof Variable );
2996            }
2997        );
2998    }
2999
3000    /**
3001     * Shorthand to check if $child is subclass of $parent.
3002     */
3003    public static function isSubclassOf(
3004        FullyQualifiedClassName $child,
3005        FullyQualifiedClassName $parent,
3006        CodeBase $codeBase
3007    ): bool {
3008        return $child->asType()->asExpandedTypes( $codeBase )->hasType( $parent->asType() );
3009    }
3010}