Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.73% covered (success)
96.73%
473 / 489
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SecurityCheckPlugin
96.73% covered (success)
96.73%
473 / 489
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%
37 / 37
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
88.89% covered (warning)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
16.35
 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 ast\Node;
26use Closure;
27use Error;
28use InvalidArgumentException;
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;
44use RuntimeException;
45
46/**
47 * Base class used by the Generic and MediaWiki flavours of the plugin.
48 */
49abstract class SecurityCheckPlugin extends PluginV3 implements
50    PostAnalyzeNodeCapability,
51    PreAnalyzeNodeCapability,
52    BeforeLoopBodyAnalysisCapability,
53    MergeVariableInfoCapability,
54    AnalyzeLiteralStatementCapability
55{
56    use TaintednessAccessorsTrait;
57
58    // Various taint flags. The _EXEC_ varieties mean
59    // that it is unsafe to assign that type of taint
60    // to the variable in question.
61
62    public const NO_TAINT = 0;
63
64    // Flag to denote that we don't know
65    public const UNKNOWN_TAINT = 1 << 0;
66
67    // Flag for function parameters and the like, where it
68    // preserves whatever taint the function is given.
69    public const PRESERVE_TAINT = 1 << 1;
70
71    // In future might separate out different types of html quoting.
72    // e.g. "<div data-foo='" . htmlspecialchars( $bar ) . "'>";
73    // is unsafe.
74    public const HTML_TAINT = 1 << 2;
75    public const HTML_EXEC_TAINT = 1 << 3;
76
77    public const SQL_TAINT = 1 << 4;
78    public const SQL_EXEC_TAINT = 1 << 5;
79
80    public const SHELL_TAINT = 1 << 6;
81    public const SHELL_EXEC_TAINT = 1 << 7;
82
83    public const SERIALIZE_TAINT = 1 << 8;
84    public const SERIALIZE_EXEC_TAINT = 1 << 9;
85
86    // Tainted paths, as input to include(), require() and some FS functions (path traversal)
87    public const PATH_TAINT = 1 << 10;
88    public const PATH_EXEC_TAINT = 1 << 11;
89
90    // User-controlled code, for RCE
91    public const CODE_TAINT = 1 << 12;
92    public const CODE_EXEC_TAINT = 1 << 13;
93
94    // User-controlled regular expressions, for ReDoS
95    public const REGEX_TAINT = 1 << 14;
96    public const REGEX_EXEC_TAINT = 1 << 15;
97
98    // To allow people to add other application specific taints.
99    public const CUSTOM1_TAINT = 1 << 16;
100    public const CUSTOM1_EXEC_TAINT = 1 << 17;
101    public const CUSTOM2_TAINT = 1 << 18;
102    public const CUSTOM2_EXEC_TAINT = 1 << 19;
103
104    // Special purpose for supporting MediaWiki's IDatabase::select
105    // and friends. Like SQL_TAINT, but only applies to the numeric
106    // keys of an array. Note: These are not included in YES_TAINT/EXEC_TAINT.
107    // e.g. given $f = [ $_GET['foo'] ]; $f would have the flag, but
108    // $g = $_GET['foo']; or $h = [ 's' => $_GET['foo'] ] would not.
109    // The associative keys also have this flag if they are tainted.
110    // It is also assumed anything with this flag will also have
111    // the SQL_TAINT flag set.
112    public const SQL_NUMKEY_TAINT = 1 << 20;
113    public const SQL_NUMKEY_EXEC_TAINT = 1 << 21;
114
115    // For double escaped variables
116    public const ESCAPED_TAINT = 1 << 22;
117    public const ESCAPED_EXEC_TAINT = 1 << 23;
118
119    // Special purpose flags (Starting at 2^28)
120    // TODO Renumber these. Requires changing format of the hardcoded arrays
121    // Cancel's out all EXEC flags on a function arg if arg is array.
122    public const ARRAY_OK = 1 << 28;
123
124    // Do not allow autodetected taint info override given taint.
125    // TODO Store this and other special flags somewhere else in the FunctionTaintedness object, not
126    // as normal taint flags.
127    public const NO_OVERRIDE = 1 << 29;
128
129    public const VARIADIC_PARAM = 1 << 30;
130
131    // *All* function flags
132    //TODO Add a structure test for this
133    public const FUNCTION_FLAGS = self::ARRAY_OK | self::NO_OVERRIDE;
134
135    // Combination flags.
136
137    // YES_TAINT denotes all taint a user controlled variable would have
138    public const YES_TAINT = self::HTML_TAINT | self::SQL_TAINT | self::SHELL_TAINT | self::SERIALIZE_TAINT |
139        self::PATH_TAINT | self::CODE_TAINT | self::REGEX_TAINT | self::CUSTOM1_TAINT | self::CUSTOM2_TAINT;
140    public const EXEC_TAINT = self::YES_TAINT << 1;
141    // @phan-suppress-next-line PhanUnreferencedPublicClassConstant
142    public const YES_EXEC_TAINT = self::YES_TAINT | self::EXEC_TAINT;
143
144    // ALL taint is YES + special purpose taints, but not including special flags.
145    public const ALL_TAINT = self::YES_TAINT | self::SQL_NUMKEY_TAINT | self::ESCAPED_TAINT;
146    public const ALL_EXEC_TAINT =
147        self::EXEC_TAINT | self::SQL_NUMKEY_EXEC_TAINT | self::ESCAPED_EXEC_TAINT;
148    public const ALL_YES_EXEC_TAINT = self::ALL_TAINT | self::ALL_EXEC_TAINT;
149
150    // Taints that support backpropagation.
151    public const BACKPROP_TAINTS = self::ALL_EXEC_TAINT;
152
153    public const ESCAPES_HTML = ( self::YES_TAINT & ~self::HTML_TAINT ) | self::ESCAPED_EXEC_TAINT;
154
155    // As the name would suggest, this must include *ALL* possible taint flags.
156    public const ALL_TAINT_FLAGS = self::ALL_YES_EXEC_TAINT | self::FUNCTION_FLAGS |
157        self::UNKNOWN_TAINT | self::PRESERVE_TAINT | self::VARIADIC_PARAM;
158
159    /**
160     * Used to print taint debug data, see BlockAnalysisVisitor::PHAN_DEBUG_VAR_REGEX
161     */
162    private const DEBUG_TAINTEDNESS_REGEXP =
163        '/@phan-debug-var-taintedness\s+\$(' . Builder::WORD_REGEX . '(,\s*\$' . Builder::WORD_REGEX . ')*)/';
164    // @phan-suppress-previous-line PhanAccessClassConstantInternal It's just perfect for use here
165
166    public const PARAM_ANNOTATION_REGEX =
167        '/@param-taint\s+&?(?P<variadic>\.\.\.)?\$(?P<paramname>\S+)\s+(?P<taint>.*)$/';
168
169    /**
170     * @var self Passed to the visitor for context
171     */
172    public static $pluginInstance;
173
174    /**
175     * @var array<array<FunctionTaintedness|MethodLinks>> Cache of parsed docblocks. This is declared here (as opposed
176     *  to the BaseVisitor) so that PHPUnit can snapshot and restore it.
177     * @phan-var array<array{0:FunctionTaintedness,1:MethodLinks}>
178     */
179    public static $docblockCache = [];
180
181    /** @var FunctionTaintedness[] Cache of taintedness of builtin functions */
182    private static $builtinFuncTaintCache = [];
183
184    /**
185     * Save the subclass instance to make it accessible from the visitor
186     */
187    public function __construct() {
188        $this->assertRequiredConfig();
189        self::$pluginInstance = $this;
190    }
191
192    /**
193     * Ensure that the options we need are enabled.
194     */
195    private function assertRequiredConfig(): void {
196        if ( Config::get_quick_mode() ) {
197            throw new RuntimeException( 'Quick mode must be disabled to run taint-check' );
198        }
199    }
200
201    /**
202     * @inheritDoc
203     */
204    public function getMergeVariableInfoClosure(): Closure {
205        /**
206         * For branches that are not guaranteed to be executed, merge taint info for any involved
207         * variable across all branches.
208         *
209         * @note This method is HOT, so keep it optimized
210         *
211         * @param Variable $variable
212         * @param Scope[] $scopeList
213         * @param bool $varExistsInAllScopes @phan-unused-param
214         * @suppress PhanUnreferencedClosure, PhanUndeclaredProperty, UnusedSuppression
215         */
216        return static function ( Variable $variable, array $scopeList, bool $varExistsInAllScopes ) {
217            $varName = $variable->getName();
218
219            $vars = [];
220            $firstVar = null;
221            foreach ( $scopeList as $scope ) {
222                $localVar = $scope->getVariableByNameOrNull( $varName );
223                if ( $localVar ) {
224                    if ( !$firstVar ) {
225                        $firstVar = $localVar;
226                    } else {
227                        $vars[] = $localVar;
228                    }
229                }
230            }
231
232            if ( !$firstVar ) {
233                return;
234            }
235
236            /** @var Taintedness $taintedness */
237            $taintedness = $prevTaint = $firstVar->taintedness ?? null;
238            /** @var MethodLinks $methodLinks */
239            $methodLinks = $prevLinks = $firstVar->taintedMethodLinks ?? null;
240            /** @var CausedByLines $error */
241            $error = $prevErr = $firstVar->taintedOriginalError ?? null;
242
243            foreach ( $vars as $localVar ) {
244                // Below we only merge data if it's non-null in the current scope and different from the previous
245                // branch. Using arrays to save all previous values and then in_array seems useless on MW core,
246                // since >99% cases of duplication are already covered by these simple checks.
247
248                $taintOrNull = $localVar->taintedness ?? null;
249                if ( $taintOrNull && $taintOrNull !== $prevTaint ) {
250                    $prevTaint = $taintOrNull;
251                    if ( $taintedness ) {
252                        $taintedness = $taintedness->asMergedWith( $taintOrNull );
253                    } else {
254                        $taintedness = $taintOrNull;
255                    }
256                }
257
258                $variableObjLinksOrNull = $localVar->taintedMethodLinks ?? null;
259                if ( $variableObjLinksOrNull && $variableObjLinksOrNull !== $prevLinks ) {
260                    $prevLinks = $variableObjLinksOrNull;
261                    if ( $methodLinks ) {
262                        $methodLinks = $methodLinks->asMergedWith( $variableObjLinksOrNull );
263                    } else {
264                        $methodLinks = $variableObjLinksOrNull;
265                    }
266                }
267
268                $varErrorOrNull = $localVar->taintedOriginalError ?? null;
269                if ( $varErrorOrNull && $varErrorOrNull !== $prevErr ) {
270                    $prevErr = $varErrorOrNull;
271                    if ( $error ) {
272                        $error = $error->asMergedWith( $varErrorOrNull );
273                    } else {
274                        $error = $varErrorOrNull;
275                    }
276                }
277            }
278
279            if ( $taintedness ) {
280                self::setTaintednessRaw( $variable, $taintedness );
281            }
282            if ( $methodLinks ) {
283                self::setMethodLinks( $variable, $methodLinks );
284            }
285            if ( $error ) {
286                self::setCausedByRaw( $variable, $error );
287            }
288        };
289    }
290
291    /**
292     * Print the taintedness of a variable, when requested
293     * @see BlockAnalysisVisitor::analyzeSubstituteVarAssert()
294     * @inheritDoc
295     * @suppress PhanUndeclaredProperty, UnusedSuppression
296     */
297    public function analyzeStringLiteralStatement( CodeBase $codeBase, Context $context, string $statement ): bool {
298        $found = false;