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