Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.52% covered (success)
96.52%
471 / 488
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SecurityCheckPlugin
96.52% covered (success)
96.52%
471 / 488
40.00% covered (danger)
40.00%
6 / 15
83
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 assertRequiredConfig
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 getMergeVariableInfoClosure
92.68% covered (success)
92.68%
38 / 41
0.00% covered (danger)
0.00%
0 / 1
18.13
 analyzeStringLiteralStatement
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
8
 taintToString
98.08% covered (success)
98.08%
51 / 52
0.00% covered (danger)
0.00%
0 / 1
5
 builtinFuncHasTaint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBuiltinFuncTaint
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
7
 assertFunctionTaintArrayWellFormed
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
13.62
 getCustomFuncTaints
n/a
0 / 0
n/a
0 / 0
0
 isFalsePositive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseTaintLine
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
12
 modifyParamSinkTaint
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 modifyArgTaint
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 convertTaintNameToConstant
85.71% covered (warning)
85.71%
24 / 28
0.00% covered (danger)
0.00%
0 / 1
16.75
 getPHPFuncTaints
100.00% covered (success)
100.00%
237 / 237
100.00% covered (success)
100.00%
1 / 1
1
 getBeforeLoopBodyAnalysisVisitorClassName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare( strict_types=1 );
2
3/**
4 * Base class for SecurityCheckPlugin. Extend if you want to customize.
5 *
6 * Copyright (C) 2017  Brian Wolff <bawolff@gmail.com>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 */
22
23namespace SecurityCheckPlugin;
24
25use AssertionError;
26use ast\Node;
27use Closure;
28use Error;
29use Phan\CodeBase;
30use Phan\Config;
31use Phan\Language\Context;
32use Phan\Language\Element\Comment\Builder;
33use Phan\Language\Element\FunctionInterface;
34use Phan\Language\Element\Variable;
35use Phan\Language\FQSEN\FullyQualifiedFunctionLikeName;
36use Phan\Language\FQSEN\FullyQualifiedMethodName;
37use Phan\Language\Scope;
38use Phan\PluginV3;
39use Phan\PluginV3\AnalyzeLiteralStatementCapability;
40use Phan\PluginV3\BeforeLoopBodyAnalysisCapability;
41use Phan\PluginV3\MergeVariableInfoCapability;
42use Phan\PluginV3\PostAnalyzeNodeCapability;
43use Phan\PluginV3\PreAnalyzeNodeCapability;
44
45/**
46 * Base class used by the Generic and MediaWiki flavours of the plugin.
47 */
48abstract class SecurityCheckPlugin extends PluginV3 implements
49    PostAnalyzeNodeCapability,
50    PreAnalyzeNodeCapability,
51    BeforeLoopBodyAnalysisCapability,
52    MergeVariableInfoCapability,
53    AnalyzeLiteralStatementCapability
54{
55    use TaintednessAccessorsTrait;
56
57    // Various taint flags. The _EXEC_ varieties mean
58    // that it is unsafe to assign that type of taint
59    // to the variable in question.
60
61    public const NO_TAINT = 0;
62
63    // Flag to denote that we don't know
64    public const UNKNOWN_TAINT = 1 << 0;
65
66    // Flag for function parameters and the like, where it
67    // preserves whatever taint the function is given.
68    public const PRESERVE_TAINT = 1 << 1;
69
70    // In future might separate out different types of html quoting.
71    // e.g. "<div data-foo='" . htmlspecialchars( $bar ) . "'>";
72    // is unsafe.
73    public const HTML_TAINT = 1 << 2;
74    public const HTML_EXEC_TAINT = 1 << 3;
75
76    public const SQL_TAINT = 1 << 4;
77    public const SQL_EXEC_TAINT = 1 << 5;
78
79    public const SHELL_TAINT = 1 << 6;
80    public const SHELL_EXEC_TAINT = 1 << 7;
81
82    public const SERIALIZE_TAINT = 1 << 8;
83    public const SERIALIZE_EXEC_TAINT = 1 << 9;
84
85    // Tainted paths, as input to include(), require() and some FS functions (path traversal)
86    public const PATH_TAINT = 1 << 10;
87    public const PATH_EXEC_TAINT = 1 << 11;
88
89    // User-controlled code, for RCE
90    public const CODE_TAINT = 1 << 12;
91    public const CODE_EXEC_TAINT = 1 << 13;
92
93    // User-controlled regular expressions, for ReDoS
94    public const REGEX_TAINT = 1 << 14;
95    public const REGEX_EXEC_TAINT = 1 << 15;
96
97    // To allow people to add other application specific taints.
98    public const CUSTOM1_TAINT = 1 << 16;
99    public const CUSTOM1_EXEC_TAINT = 1 << 17;
100    public const CUSTOM2_TAINT = 1 << 18;
101    public const CUSTOM2_EXEC_TAINT = 1 << 19;
102
103    // Special purpose for supporting MediaWiki's IDatabase::select
104    // and friends. Like SQL_TAINT, but only applies to the numeric
105    // keys of an array. Note: These are not included in YES_TAINT/EXEC_TAINT.
106    // e.g. given $f = [ $_GET['foo'] ]; $f would have the flag, but
107    // $g = $_GET['foo']; or $h = [ 's' => $_GET['foo'] ] would not.
108    // The associative keys also have this flag if they are tainted.
109    // It is also assumed anything with this flag will also have
110    // the SQL_TAINT flag set.
111    public const SQL_NUMKEY_TAINT = 1 << 20;
112    public const SQL_NUMKEY_EXEC_TAINT = 1 << 21;
113
114    // For double escaped variables
115    public const ESCAPED_TAINT = 1 << 22;
116    public const ESCAPED_EXEC_TAINT = 1 << 23;
117
118    // Special purpose flags (Starting at 2^28)
119    // TODO Renumber these. Requires changing format of the hardcoded arrays
120    // Cancel's out all EXEC flags on a function arg if arg is array.
121    public const ARRAY_OK = 1 << 28;
122
123    // Do not allow autodetected taint info override given taint.
124    // TODO Store this and other special flags somewhere else in the FunctionTaintedness object, not
125    // as normal taint flags.
126    public const NO_OVERRIDE = 1 << 29;
127
128    public const VARIADIC_PARAM = 1 << 30;
129
130    // *All* function flags
131    //TODO Add a structure test for this
132    public const FUNCTION_FLAGS = self::ARRAY_OK | self::NO_OVERRIDE;
133
134    // Combination flags.
135
136    // YES_TAINT denotes all taint a user controlled variable would have
137    public const YES_TAINT = self::HTML_TAINT | self::SQL_TAINT | self::SHELL_TAINT | self::SERIALIZE_TAINT |
138        self::PATH_TAINT | self::CODE_TAINT | self::REGEX_TAINT | self::CUSTOM1_TAINT | self::CUSTOM2_TAINT;
139    public const EXEC_TAINT = self::YES_TAINT << 1;
140    // @phan-suppress-next-line PhanUnreferencedPublicClassConstant
141    public const YES_EXEC_TAINT = self::YES_TAINT | self::EXEC_TAINT;
142
143    // ALL taint is YES + special purpose taints, but not including special flags.
144    public const ALL_TAINT = self::YES_TAINT | self::SQL_NUMKEY_TAINT | self::ESCAPED_TAINT;
145    public const ALL_EXEC_TAINT =
146        self::EXEC_TAINT | self::SQL_NUMKEY_EXEC_TAINT | self::ESCAPED_EXEC_TAINT;
147    public const ALL_YES_EXEC_TAINT = self::ALL_TAINT | self::ALL_EXEC_TAINT;
148
149    // Taints that support backpropagation.
150    public const BACKPROP_TAINTS = self::ALL_EXEC_TAINT;
151
152    public const ESCAPES_HTML = ( self::YES_TAINT & ~self::HTML_TAINT ) | self::ESCAPED_EXEC_TAINT;
153
154    // As the name would suggest, this must include *ALL* possible taint flags.
155    public const ALL_TAINT_FLAGS = self::ALL_YES_EXEC_TAINT | self::FUNCTION_FLAGS |
156        self::UNKNOWN_TAINT | self::PRESERVE_TAINT | self::VARIADIC_PARAM;
157
158    /**
159     * Used to print taint debug data, see BlockAnalysisVisitor::PHAN_DEBUG_VAR_REGEX
160     */
161    private const DEBUG_TAINTEDNESS_REGEXP =
162        '/@phan-debug-var-taintedness\s+\$(' . Builder::WORD_REGEX . '(,\s*\$' . Builder::WORD_REGEX . ')*)/';
163    // @phan-suppress-previous-line PhanAccessClassConstantInternal It's just perfect for use here
164
165    public const PARAM_ANNOTATION_REGEX =
166        '/@param-taint\s+&?(?P<variadic>\.\.\.)?\$(?P<paramname>\S+)\s+(?P<taint>.*)$/';
167
168    /**
169     * @var self Passed to the visitor for context
170     */
171    public static $pluginInstance;
172
173    /**
174     * @var array<array<FunctionTaintedness|MethodLinks>> Cache of parsed docblocks. This is declared here (as opposed
175     *  to the BaseVisitor) so that PHPUnit can snapshot and restore it.
176     * @phan-var array<array{0:FunctionTaintedness,1:MethodLinks}>
177     */
178    public static $docblockCache = [];
179
180    /** @var FunctionTaintedness[] Cache of taintedness of builtin functions */
181    private static $builtinFuncTaintCache = [];
182
183    /**
184     * Save the subclass instance to make it accessible from the visitor
185     */
186    public function __construct() {
187        $this->assertRequiredConfig();
188        self::$pluginInstance = $this;
189    }
190
191    /**
192     * Ensure that the options we need are enabled.
193     */
194    private function assertRequiredConfig(): void {
195        if ( Config::get_quick_mode() ) {
196            throw new AssertionError( 'Quick mode must be disabled to run taint-check' );
197        }
198    }
199
200    /**
201     * @inheritDoc
202     */
203    public function getMergeVariableInfoClosure(): Closure {
204        /**
205         * For branches that are not guaranteed to be executed, merge taint info for any involved
206         * variable across all branches.
207         *
208         * @note This method is HOT, so keep it optimized
209         *
210         * @param Variable $variable
211         * @param Scope[] $scopeList
212         * @param bool $varExistsInAllScopes @phan-unused-param
213         * @suppress PhanUnreferencedClosure, PhanUndeclaredProperty, UnusedSuppression
214         */
215        return static function ( Variable $variable, array $scopeList, bool $varExistsInAllScopes ) {
216            $varName = $variable->getName();
217
218            $vars = [];
219            $firstVar = null;
220            foreach ( $scopeList as $scope ) {
221                $localVar = $scope->getVariableByNameOrNull( $varName );
222                if ( $localVar ) {
223                    if ( !$firstVar ) {
224                        $firstVar = $localVar;
225                    } else {
226                        $vars[] = $localVar;
227                    }
228                }
229            }
230
231            if ( !$firstVar ) {
232                return;
233            }
234
235            $taintedness = $prevTaint = $firstVar->taintedness ?? null;
236            $methodLinks = $prevLinks = $firstVar->taintedMethodLinks ?? null;
237            $error = $prevErr = $firstVar->taintedOriginalError ?? null;
238
239            foreach ( $vars as $localVar ) {
240                // Below we only merge data if it's non-null in the current scope and different from the previous
241                // branch. Using arrays to save all previous values and then in_array seems useless on MW core,
242                // since >99% cases of duplication are already covered by these simple checks.
243
244                $taintOrNull = $localVar->taintedness ?? null;
245                if ( $taintOrNull && $taintOrNull !== $prevTaint ) {
246                    $prevTaint = $taintOrNull;
247                    if ( $taintedness ) {
248                        $taintedness->mergeWith( $taintOrNull );
249                    } else {
250                        $taintedness = $taintOrNull;
251                    }
252                }
253
254                $variableObjLinksOrNull = $localVar->taintedMethodLinks ?? null;
255                if ( $variableObjLinksOrNull && $variableObjLinksOrNull !== $prevLinks ) {
256                    $prevLinks = $variableObjLinksOrNull;
257                    if ( $methodLinks ) {
258                        $methodLinks->mergeWith( $variableObjLinksOrNull );
259                    } else {
260                        $methodLinks = $variableObjLinksOrNull;
261                    }
262                }
263
264                $varErrorOrNull = $localVar->taintedOriginalError ?? null;
265                if ( $varErrorOrNull && $varErrorOrNull !== $prevErr ) {
266                    $prevErr = $varErrorOrNull;
267                    if ( $error ) {
268                        $error->mergeWith( $varErrorOrNull );
269                    } else {
270                        $error = $varErrorOrNull;
271                    }
272                }
273            }
274
275            if ( $taintedness ) {
276                self::setTaintednessRaw( $variable, $taintedness );
277            }
278            if ( $methodLinks ) {
279                self::setMethodLinks( $variable, $methodLinks );
280            }
281            if ( $error ) {
282                self::setCausedByRaw( $variable, $error );
283            }
284        };
285    }
286
287    /**
288     * Print the taintedness of a variable, when requested
289     * @see BlockAnalysisVisitor::analyzeSubstituteVarAssert()
290     * @inheritDoc
291     * @suppress PhanUndeclaredProperty, UnusedSuppression
292     */
293    public function analyzeStringLiteralStatement( CodeBase $codeBase, Context $context, string $statement ): bool {
294        $found = false;
295        if ( preg_match_all( self::DEBUG_TAINTEDNESS_REGEXP, $statement, $matches, PREG_SET_ORDER ) ) {
296            foreach ( $matches as $group ) {
297                foreach ( explode( ',', $group[1] ) as $rawVar ) {
298                    $varName = ltrim( trim( $rawVar ), '$' );
299                    if ( $context->getScope()->hasVariableWithName( $varName ) ) {
300                        $var = $context->getScope()->getVariableByName( $varName );
301                        $taintOrNull = self::getTaintednessRaw( $var );
302                        $taint = $taintOrNull ? $taintOrNull->toShortString() : 'unset';
303                        $msg = "Variable {CODE} has taintedness: {DETAILS}";
304                        $params = [ "\$$varName", $taint ];
305                    } else {
306                        $msg = "Variable {CODE} doesn't exist in scope";
307                        $params = [ "\$$varName" ];
308                    }
309                    self::emitIssue(
310                        $codeBase,
311                        $context,
312                        'SecurityCheckDebugTaintedness',
313                        $msg,
314                        $params
315                    );
316                    $found = true;
317                }
318            }
319        } elseif ( strpos( $statement, '@taint-check-debug-method-first-arg' ) !== false ) {
320            // FIXME This is a hack. The annotation is INTERNAL, for use only in the backpropoffsets-blowup
321            // test. We should either find a better way to test that, or maybe add a public annotation
322            // for debugging taintedness of a method (probably unreadable on a single line).
323            $funcName = preg_replace( '/@taint-check-debug-method-first-arg ([a-z:]+)\b.*/i', '$1', $statement );
324            // Let any exception bubble up here, the annotation is for internal use in testing
325            $fqsen = FullyQualifiedMethodName::fromStringInContext( $funcName, $context );
326            $method = $codeBase->getMethodByFQSEN( $fqsen );
327            /** @var FunctionTaintedness|null $fTaint */
328            $fTaint = $method->funcTaint ?? null;
329            if ( !$fTaint ) {
330                return false;
331            }
332            self::emitIssue(