Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.73% |
473 / 489 |
|
40.00% |
6 / 15 |
CRAP | |
0.00% |
0 / 1 |
SecurityCheckPlugin | |
96.73% |
473 / 489 |
|
40.00% |
6 / 15 |
83 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
assertRequiredConfig | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
getMergeVariableInfoClosure | |
92.68% |
38 / 41 |
|
0.00% |
0 / 1 |
18.13 | |||
analyzeStringLiteralStatement | |
97.30% |
36 / 37 |
|
0.00% |
0 / 1 |
8 | |||
taintToString | |
98.08% |
51 / 52 |
|
0.00% |
0 / 1 |
5 | |||
builtinFuncHasTaint | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBuiltinFuncTaint | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
7 | |||
assertFunctionTaintArrayWellFormed | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
13.62 | |||
getCustomFuncTaints | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
isFalsePositive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parseTaintLine | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
12 | |||
modifyParamSinkTaint | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
modifyArgTaint | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
convertTaintNameToConstant | |
88.89% |
24 / 27 |
|
0.00% |
0 / 1 |
16.35 | |||
getPHPFuncTaints | |
100.00% |
237 / 237 |
|
100.00% |
1 / 1 |
1 | |||
getBeforeLoopBodyAnalysisVisitorClassName | |
100.00% |
1 / 1 |
|
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 | |
23 | namespace SecurityCheckPlugin; |
24 | |
25 | use ast\Node; |
26 | use Closure; |
27 | use Error; |
28 | use InvalidArgumentException; |
29 | use Phan\CodeBase; |
30 | use Phan\Config; |
31 | use Phan\Language\Context; |
32 | use Phan\Language\Element\Comment\Builder; |
33 | use Phan\Language\Element\FunctionInterface; |
34 | use Phan\Language\Element\Variable; |
35 | use Phan\Language\FQSEN\FullyQualifiedFunctionLikeName; |
36 | use Phan\Language\FQSEN\FullyQualifiedMethodName; |
37 | use Phan\Language\Scope; |
38 | use Phan\PluginV3; |
39 | use Phan\PluginV3\AnalyzeLiteralStatementCapability; |
40 | use Phan\PluginV3\BeforeLoopBodyAnalysisCapability; |
41 | use Phan\PluginV3\MergeVariableInfoCapability; |
42 | use Phan\PluginV3\PostAnalyzeNodeCapability; |
43 | use Phan\PluginV3\PreAnalyzeNodeCapability; |
44 | use RuntimeException; |
45 | |
46 | /** |
47 | * Base class used by the Generic and MediaWiki flavours of the plugin. |
48 | */ |
49 | abstract 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; |