Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.94% |
507 / 523 |
|
88.71% |
55 / 62 |
CRAP | |
0.00% |
0 / 1 |
TaintednessVisitor | |
96.94% |
507 / 523 |
|
88.71% |
55 / 62 |
154 | |
0.00% |
0 / 1 |
analyzeNodeAndGetTaintedness | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setCachedData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCurTaintUnknown | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCurTaintSafe | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visit | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
visitClosure | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
visitFuncDecl | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitMethod | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitArrowFunc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
analyzeFunctionLike | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
4.25 | |||
visitClassName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitThrow | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitUnset | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
handleUnsetDim | |
72.22% |
13 / 18 |
|
0.00% |
0 / 1 |
9.37 | |||
visitClone | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitAssignOp | |
94.12% |
32 / 34 |
|
0.00% |
0 / 1 |
3.00 | |||
visitStatic | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitAssignRef | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitAssign | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
2 | |||
doVisitAssign | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
visitBinaryOp | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
2 | |||
getBinOpTaint | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
visitDim | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
visitPrint | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitExit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitShellExec | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
visitIncludeOrEval | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
visitEcho | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
visitSimpleSinkAndPropagate | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
visitStaticCall | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitNew | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
8 | |||
visitMethodCall | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
analyzeCallNode | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
visitNullsafeMethodCall | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitCall | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
visitVar | |
91.67% |
22 / 24 |
|
0.00% |
0 / 1 |
4.01 | |||
getHardcodedTaintednessForVar | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
14 | |||
visitGlobal | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
6 | |||
visitReturn | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
3.33 | |||
setFuncTaintFromReturn | |
97.22% |
35 / 36 |
|
0.00% |
0 / 1 |
11 | |||
visitArray | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
9 | |||
visitClassConst | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitConst | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitStaticProp | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
2.03 | |||
visitProp | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
7 | |||
visitNullsafeProp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitConditional | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
visitName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitNameList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitUnaryOp | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
visitPostInc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitPreInc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitPostDec | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitPreDec | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
analyzeIncOrDec | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitCast | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
visitEncapsList | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
visitIsset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitMagicConst | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitInstanceOf | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitMatch | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php declare( strict_types=1 ); |
2 | /** |
3 | * Copyright (C) 2017 Brian Wolff <bawolff@gmail.com> |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
18 | */ |
19 | |
20 | namespace SecurityCheckPlugin; |
21 | |
22 | use ast\Node; |
23 | use Phan\Analysis\BlockExitStatusChecker; |
24 | use Phan\AST\ContextNode; |
25 | use Phan\Debug; |
26 | use Phan\Exception\CodeBaseException; |
27 | use Phan\Exception\IssueException; |
28 | use Phan\Exception\NodeException; |
29 | use Phan\Language\Element\FunctionInterface; |
30 | use Phan\Language\Element\GlobalVariable; |
31 | use Phan\Language\FQSEN\FullyQualifiedClassName; |
32 | use Phan\Language\FQSEN\FullyQualifiedFunctionName; |
33 | use Phan\Language\Type\FunctionLikeDeclarationType; |
34 | use Phan\PluginV3\PluginAwarePostAnalysisVisitor; |
35 | |
36 | /** |
37 | * This class visits all the nodes in the ast. It has two jobs: |
38 | * |
39 | * 1) Return the taint value of the current node we are visiting. |
40 | * 2) In the event of an assignment (and similar things) propagate |
41 | * the taint value from the left hand side to the right hand side. |
42 | * |
43 | * For the moment, the taint values are stored in a "taintedness" |
44 | * property of various phan TypedElement objects. This is probably |
45 | * not the best solution for where to store the data, but its what |
46 | * this does for now. |
47 | * |
48 | * This also maintains some other properties, such as where the error |
49 | * originates, and dependencies in certain cases. |
50 | * |
51 | * @phan-file-suppress PhanUnusedPublicMethodParameter Many methods don't use $node |
52 | */ |
53 | class TaintednessVisitor extends PluginAwarePostAnalysisVisitor { |
54 | use TaintednessBaseVisitor; |
55 | |
56 | /** |
57 | * Node kinds whose taintedness is not well-defined and for which we don't need a visit* method. |
58 | */ |
59 | public const INAPPLICABLE_NODES_WITHOUT_VISITOR = [ |
60 | \ast\AST_ARG_LIST => true, |
61 | \ast\AST_TYPE => true, |
62 | \ast\AST_NULLABLE_TYPE => true, |
63 | \ast\AST_PARAM_LIST => true, |
64 | // Params are handled in PreTaintednessVisitor |
65 | \ast\AST_PARAM => true, |
66 | \ast\AST_CLASS => true, |
67 | \ast\AST_USE_ELEM => true, |
68 | \ast\AST_STMT_LIST => true, |
69 | \ast\AST_CLASS_CONST_DECL => true, |
70 | \ast\AST_CLASS_CONST_GROUP => true, |
71 | \ast\AST_CONST_DECL => true, |
72 | \ast\AST_IF => true, |
73 | \ast\AST_IF_ELEM => true, |
74 | \ast\AST_PROP_DECL => true, |
75 | \ast\AST_CONST_ELEM => true, |
76 | \ast\AST_USE => true, |
77 | \ast\AST_USE_TRAIT => true, |
78 | \ast\AST_BREAK => true, |
79 | \ast\AST_CONTINUE => true, |
80 | \ast\AST_GOTO => true, |
81 | \ast\AST_CATCH => true, |
82 | \ast\AST_NAMESPACE => true, |
83 | \ast\AST_SWITCH => true, |
84 | \ast\AST_SWITCH_CASE => true, |
85 | \ast\AST_SWITCH_LIST => true, |
86 | \ast\AST_WHILE => true, |
87 | \ast\AST_DO_WHILE => true, |
88 | \ast\AST_FOR => true, |
89 | // Handled in TaintednessLoopVisitor |
90 | \ast\AST_FOREACH => true, |
91 | \ast\AST_EXPR_LIST => true, |
92 | \ast\AST_TRY => true, |
93 | // Array elems are handled directly in visitArray |
94 | \ast\AST_ARRAY_ELEM => true, |
95 | // Initializing the prop is done in preorder |
96 | \ast\AST_PROP_ELEM => true, |
97 | \ast\AST_PROP_GROUP => true, |
98 | // Variables are already handled in visitVar |
99 | \ast\AST_CLOSURE_VAR => true, |
100 | \ast\AST_CLOSURE_USES => true, |
101 | \ast\AST_LABEL => true, |
102 | \ast\AST_ATTRIBUTE => true, |
103 | \ast\AST_ATTRIBUTE_GROUP => true, |
104 | \ast\AST_ATTRIBUTE_LIST => true, |
105 | ]; |
106 | |
107 | /** |
108 | * Node kinds whose taintedness is not well-defined, but for which we still need a visit* method. |
109 | * Trying to get the taintedness of these nodes will still result in an error. |
110 | */ |
111 | public const INAPPLICABLE_NODES_WITH_VISITOR = [ |
112 | \ast\AST_GLOBAL => true, |
113 | \ast\AST_RETURN => true, |
114 | \ast\AST_STATIC => true, |
115 | \ast\AST_FUNC_DECL => true, |
116 | \ast\AST_METHOD => true, |
117 | ]; |
118 | |
119 | /** |
120 | * Map of node kinds whose taintedness is not well-defined, e.g. because that node |
121 | * cannot be used as an expression. Note that it's safe to use array plus here. |
122 | */ |
123 | private const INAPPLICABLE_NODES = self::INAPPLICABLE_NODES_WITHOUT_VISITOR + self::INAPPLICABLE_NODES_WITH_VISITOR; |
124 | |
125 | /** @var TaintednessWithError|null */ |
126 | private $curTaintWithError; |
127 | |
128 | /** |
129 | * @param Node $node |
130 | * @return TaintednessWithError |
131 | */ |
132 | public function analyzeNodeAndGetTaintedness( Node $node ): TaintednessWithError { |
133 | assert( |
134 | !isset( self::INAPPLICABLE_NODES[$node->kind] ), |
135 | 'Should not try to get taintedness of inapplicable nodes (got ' . Debug::nodeName( $node ) . ')' |
136 | ); |
137 | $this->__invoke( $node ); |
138 | $this->setCachedData( $node ); |
139 | return $this->curTaintWithError; |
140 | } |
141 | |
142 | /** |
143 | * Cache taintedness data in an AST node. Ideally we'd want this to happen at the end of __invoke, but phan |
144 | * calls visit* methods by name, so that doesn't work. |
145 | * Caching a node *may* improve the speed, but *will* increase the memory usage, so only do that for nodes |
146 | * whose taintedness: |
147 | * - Is not trivial to compute, and |
148 | * - Might be needed from another node (via getTaintednessNode) |
149 | * @param Node $node |
150 | */ |
151 | private function setCachedData( Node $node ): void { |
152 | // @phan-suppress-next-line PhanUndeclaredProperty |
153 | $node->taint = $this->curTaintWithError; |
154 | } |
155 | |
156 | /** |
157 | * Sets $this->curTaint to UNKNOWN. Shorthand to filter the usages of curTaint. |
158 | */ |
159 | private function setCurTaintUnknown(): void { |
160 | $this->curTaintWithError = TaintednessWithError::unknownSingleton(); |
161 | } |
162 | |
163 | private function setCurTaintSafe(): void { |
164 | $this->curTaintWithError = TaintednessWithError::emptySingleton(); |
165 | } |
166 | |
167 | /** |
168 | * Generic visitor when we haven't defined a more specific one. |
169 | * |
170 | * @param Node $node |
171 | */ |
172 | public function visit( Node $node ): void { |
173 | // This method will be called on all nodes for which |
174 | // there is no implementation of its kind visitor. |
175 | |
176 | if ( isset( self::INAPPLICABLE_NODES_WITHOUT_VISITOR[$node->kind] ) ) { |
177 | return; |
178 | } |
179 | |
180 | // To see what kinds of nodes are passing through here, |
181 | // you can run `Debug::printNode($node)`. |
182 | # Debug::printNode( $node ); |
183 | $this->debug( __METHOD__, "unhandled case " . Debug::nodeName( $node ) ); |
184 | $this->setCurTaintUnknown(); |
185 | } |
186 | |
187 | /** |
188 | * @param Node $node |
189 | */ |
190 | public function visitClosure( Node $node ): void { |
191 | // We cannot use getFunctionLikeInScope for closures |
192 | $closureFQSEN = FullyQualifiedFunctionName::fromClosureInContext( $this->context, $node ); |
193 | |
194 | if ( $this->code_base->hasFunctionWithFQSEN( $closureFQSEN ) ) { |
195 | $func = $this->code_base->getFunctionByFQSEN( $closureFQSEN ); |
196 | $this->analyzeFunctionLike( $func ); |
197 | } else { |
198 | // @codeCoverageIgnoreStart |
199 | $this->debug( __METHOD__, 'closure doesn\'t exist' ); |
200 | // @codeCoverageIgnoreEnd |
201 | } |
202 | $this->setCurTaintSafe(); |
203 | $this->setCachedData( $node ); |
204 | } |
205 | |
206 | /** |
207 | * @param Node $node |
208 | */ |
209 | public function visitFuncDecl( Node $node ): void { |
210 | $func = $this->context->getFunctionLikeInScope( $this->code_base ); |
211 | $this->analyzeFunctionLike( $func ); |
212 | } |
213 | |
214 | /** |
215 | * Visit a method declaration |
216 | * |
217 | * @param Node $node |
218 | */ |
219 | public function visitMethod( Node $node ): void { |
220 | $method = $this->context->getFunctionLikeInScope( $this->code_base ); |
221 | $this->analyzeFunctionLike( $method ); |
222 | } |
223 | |
224 | /** |
225 | * @param Node $node |
226 | */ |
227 | public function visitArrowFunc( Node $node ): void { |
228 | $this->visitClosure( $node ); |
229 | } |
230 | |
231 | /** |
232 | * Handles methods, functions and closures. |
233 | * |
234 | * @param FunctionInterface $func The func to analyze |
235 | */ |
236 | private function analyzeFunctionLike( FunctionInterface $func ): void { |
237 | if ( self::getFuncTaint( $func ) === null ) { |
238 | // If we still have no data, presumably the function doesn't return anything, so mark as safe. |
239 | if ( $func->hasReturn() || $func->hasYield() ) { |
240 | $this->debug( __METHOD__, "TODO: $func returns something but has no taint after analysis" ); |
241 | } |
242 | |
243 | // NOTE: If the method stores its arg to a class prop, and that class prop gets output later, |
244 | // the exec status of this won't be detected until the output is analyzed, we might miss some issues |
245 | // in the inbetween period. |
246 | self::doSetFuncTaint( $func, FunctionTaintedness::emptySingleton() ); |
247 | } |
248 | } |
249 | |
250 | /** |
251 | * FooBar::class, presumably safe since class names cannot have special chars. |
252 | * |
253 | * @param Node $node |
254 | */ |
255 | public function visitClassName( Node $node ): void { |
256 | $this->setCurTaintSafe(); |
257 | $this->setCachedData( $node ); |
258 | } |
259 | |
260 | /** |
261 | * @note AST_THROW is an expression since PHP 8 |
262 | * @param Node $node |
263 | */ |
264 | public function visitThrow( Node $node ): void { |
265 | $this->setCurTaintSafe(); |
266 | $this->setCachedData( $node ); |
267 | } |
268 | |
269 | /** |
270 | * @param Node $node |
271 | */ |
272 | public function visitUnset( Node $node ): void { |
273 | $varNode = $node->children['var']; |
274 | if ( $varNode instanceof Node && $varNode->kind === \ast\AST_DIM ) { |
275 | $this->handleUnsetDim( $varNode ); |
276 | } |
277 | $this->setCurTaintSafe(); |
278 | } |
279 | |
280 | /** |
281 | * Analyzes expressions like unset( $arr['foo'] ) to infer shape mutations. |
282 | * @param Node $node |
283 | * @return void |
284 | */ |
285 | private function handleUnsetDim( Node $node ): void { |
286 | $expr = $node->children['expr']; |
287 | if ( !$expr instanceof Node ) { |
288 | // Syntax error. |
289 | return; |
290 | } |
291 | if ( $expr->kind !== \ast\AST_VAR ) { |
292 | // For now, we only handle a single offset. |
293 | // TODO actually recurse. |
294 | return; |
295 | } |
296 | |
297 | $keyNode = $node->children['dim']; |
298 | $key = $keyNode !== null ? $this->resolveOffset( $keyNode ) : null; |
299 | if ( $key instanceof Node ) { |
300 | // We can't tell what gets removed. |
301 | return; |
302 | } |
303 | |
304 | try { |
305 | $var = $this->getCtxN( $expr )->getVariable(); |
306 | } catch ( NodeException | IssueException $_ ) { |
307 | return; |
308 | } |
309 | |
310 | if ( $var instanceof GlobalVariable ) { |
311 | // Don't handle for now. |
312 | return; |
313 | } |
314 | |
315 | $curTaint = self::getTaintednessRaw( $var ); |
316 | if ( !$curTaint ) { |
317 | // Is this even possible? Don't do anything, just in case. |
318 | return; |
319 | } |
320 | self::setTaintednessRaw( $var, $curTaint->withoutKey( $key ) ); |
321 | } |
322 | |
323 | /** |
324 | * @param Node $node |
325 | */ |
326 | public function visitClone( Node $node ): void { |
327 | $this->curTaintWithError = $this->getTaintedness( $node->children['expr'] ); |
328 | $this->setCachedData( $node ); |
329 | } |
330 | |
331 | /** |
332 | * Assignment operators are: .=, +=, -=, /=, %=, *=, **=, ??=, |=, &=, ^=, <<=, >>= |
333 | * @param Node $node |
334 | */ |
335 | public function visitAssignOp( Node $node ): void { |
336 | $lhs = $node->children['var']; |
337 | if ( !$lhs instanceof Node ) { |
338 | // Syntax error, don't crash |
339 | $this->setCurTaintSafe(); |
340 | return; |
341 | } |
342 | $rhs = $node->children['expr']; |
343 | $lhsTaintedness = $this->getTaintedness( $lhs ); |
344 | $rhsTaintedness = $this->getTaintedness( $rhs ); |
345 | |
346 | if ( property_exists( $node, 'assignTaintMask' ) ) { |
347 | // @phan-suppress-next-line PhanUndeclaredProperty |
348 | $mask = $node->assignTaintMask; |
349 | // TODO Should we consume the value, since it depends on the union types? |
350 | } else { |
351 | $this->debug( __METHOD__, 'FIXME no preorder visit?' ); |
352 | $mask = SecurityCheckPlugin::ALL_TAINT_FLAGS; |
353 | } |
354 | |
355 | // Expand rhs to include implicit lhs ophand. |
356 | $allRHSTaint = $this->getBinOpTaint( |
357 | $lhsTaintedness->getTaintedness(), |
358 | $rhsTaintedness->getTaintedness(), |
359 | $node->flags, |
360 | $mask |
361 | ); |
362 | $allError = $lhsTaintedness->getError()->asMergedWith( $rhsTaintedness->getError() ); |
363 | $allLinks = $lhsTaintedness->getMethodLinks()->asMergedWith( $rhsTaintedness->getMethodLinks() ); |
364 | |
365 | $curTaint = $this->doVisitAssign( |
366 | $lhs, |
367 | $rhs, |
368 | $allRHSTaint, |
369 | $allError, |
370 | $allLinks, |
371 | $rhsTaintedness->getTaintedness(), |
372 | $rhsTaintedness->getMethodLinks() |
373 | ); |
374 | // TODO Links and error? |
375 | $this->curTaintWithError = new TaintednessWithError( |
376 | $curTaint, |
377 | CausedByLines::emptySingleton(), |
378 | MethodLinks::emptySingleton() |
379 | ); |
380 | $this->setCachedData( $node ); |
381 | } |
382 | |
383 | /** |
384 | * `static $var = 'foo'` Handle it as an assignment of a safe value, to initialize the taintedness |
385 | * on $var. Ideally, we'd want to retain any taintedness on this object, but it's currently impossible |
386 | * (upstream has the same limitation with union types). |
387 | * |
388 | * @param Node $node |
389 | */ |
390 | public function visitStatic( Node $node ): void { |
391 | $var = $this->getCtxN( $node->children['var'] )->getVariable(); |
392 | $this->ensureTaintednessIsSet( $var ); |
393 | } |
394 | |
395 | /** |
396 | * @param Node $node |
397 | */ |
398 | public function visitAssignRef( Node $node ): void { |
399 | $this->visitAssign( $node ); |
400 | } |
401 | |
402 | /** |
403 | * @param Node $node |
404 | */ |
405 | public function visitAssign( Node $node ): void { |
406 | $lhs = $node->children['var']; |
407 | if ( !$lhs instanceof Node ) { |
408 | // Syntax error, don't crash |
409 | $this->setCurTaintSafe(); |
410 | return; |
411 | } |
412 | $rhs = $node->children['expr']; |
413 | |
414 | $rhsTaintedness = $this->getTaintedness( $rhs ); |
415 | $curTaint = $this->doVisitAssign( |
416 | $lhs, |
417 | $rhs, |
418 | $rhsTaintedness->getTaintedness(), |
419 | $rhsTaintedness->getError(), |
420 | $rhsTaintedness->getMethodLinks(), |
421 | $rhsTaintedness->getTaintedness(), |
422 | $rhsTaintedness->getMethodLinks() |
423 | ); |
424 | // TODO Links and error? |
425 | $this->curTaintWithError = new TaintednessWithError( |
426 | $curTaint, |
427 | CausedByLines::emptySingleton(), |
428 | MethodLinks::emptySingleton() |
429 | ); |
430 | $this->setCachedData( $node ); |
431 | } |
432 | |
433 | /** |
434 | * @param Node $lhs |
435 | * @param Node|mixed $rhs |
436 | * @param Taintedness $rhsTaint |
437 | * @param CausedByLines $rhsError |
438 | * @param MethodLinks $rhsLinks |
439 | * @param Taintedness $errorTaint |
440 | * @param MethodLinks $errorLinks |
441 | * @return Taintedness |
442 | */ |
443 | private function doVisitAssign( |
444 | Node $lhs, |
445 | $rhs, |
446 | Taintedness $rhsTaint, |
447 | CausedByLines $rhsError, |
448 | MethodLinks $rhsLinks, |
449 | Taintedness $errorTaint, |
450 | MethodLinks $errorLinks |
451 | ): Taintedness { |
452 | $vis = new TaintednessAssignVisitor( |
453 | $this->code_base, |
454 | $this->context, |
455 | $rhsTaint, |
456 | $rhsError, |
457 | $rhsLinks, |
458 | $errorTaint, |
459 | $errorLinks, |
460 | function () use ( $rhs ): bool { |
461 | return $this->nodeIsArray( $rhs ); |
462 | } |
463 | ); |
464 | $vis( $lhs ); |
465 | return $rhsTaint; |
466 | } |
467 | |
468 | /** |
469 | * @param Node $node |
470 | */ |
471 | public function visitBinaryOp( Node $node ): void { |
472 | $lhs = $node->children['left']; |
473 | $rhs = $node->children['right']; |
474 | $mask = $this->getBinOpTaintMask( $node, $lhs, $rhs ); |
475 | if ( $mask === SecurityCheckPlugin::NO_TAINT ) { |
476 | // If the operation is safe, don't waste time analyzing children.This might also create bugs |
477 | // like the test undeclaredvar2 |
478 | $this->setCurTaintSafe(); |
479 | $this->setCachedData( $node ); |
480 | return; |
481 | } |
482 | $leftTaint = $this->getTaintedness( $lhs ); |
483 | $rightTaint = $this->getTaintedness( $rhs ); |
484 | $curTaint = $this->getBinOpTaint( |
485 | $leftTaint->getTaintedness(), |
486 | $rightTaint->getTaintedness(), |
487 | $node->flags, |
488 | $mask |
489 | ); |
490 | $this->curTaintWithError = new TaintednessWithError( |
491 | $curTaint, |
492 | $leftTaint->getError()->asMergedWith( $rightTaint->getError() ), |
493 | $leftTaint->getMethodLinks()->asMergedWith( $rightTaint->getMethodLinks() ) |
494 | ); |
495 | $this->setCachedData( $node ); |
496 | } |
497 | |
498 | /** |
499 | * Get the taintedness of a binop, depending on the op type, applying the given flags |
500 | * @param Taintedness $leftTaint |
501 | * @param Taintedness $rightTaint |
502 | * @param int $op Represented by a flags in \ast\flags |
503 | * @param int $mask |
504 | * @return Taintedness |
505 | */ |
506 | private function getBinOpTaint( |
507 | Taintedness $leftTaint, |
508 | Taintedness $rightTaint, |
509 | int $op, |
510 | int $mask |
511 | ): Taintedness { |
512 | if ( $mask === SecurityCheckPlugin::NO_TAINT ) { |
513 | return Taintedness::safeSingleton(); |
514 | } |
515 | if ( $op === \ast\flags\BINARY_ADD ) { |
516 | // HACK: This means that a node can be array, so assume array plus |
517 | return $leftTaint->asArrayPlusWith( $rightTaint ); |
518 | } |
519 | return $leftTaint->asMergedWith( $rightTaint )->asCollapsed()->withOnly( $mask ); |
520 | } |
521 | |
522 | /** |
523 | * @param Node $node |
524 | */ |
525 | public function visitDim( Node $node ): void { |
526 | $varNode = $node->children['expr']; |
527 | if ( !$varNode instanceof Node ) { |
528 | // Accessing offset of a string literal |
529 | $this->setCurTaintSafe(); |
530 | $this->setCachedData( $node ); |
531 | return; |
532 | } |
533 | $nodeTaint = $this->getTaintednessNode( $varNode ); |
534 | if ( $node->children['dim'] === null ) { |
535 | // This should only happen in assignments: $x[] = 'foo'. Just return |
536 | // the taint of the whole object. |
537 | $this->curTaintWithError = $nodeTaint; |
538 | $this->setCachedData( $node ); |
539 | return; |
540 | } |
541 | $offset = $this->resolveOffset( $node->children['dim'] ); |
542 | $this->curTaintWithError = new TaintednessWithError( |
543 | $nodeTaint->getTaintedness()->getTaintednessForOffsetOrWhole( $offset ), |
544 | $nodeTaint->getError()->getForDim( $offset ), |
545 | $nodeTaint->getMethodLinks()->getForDim( $offset ) |
546 | ); |
547 | $this->setCachedData( $node ); |
548 | } |
549 | |
550 | /** |
551 | * @param Node $node |
552 | */ |
553 | public function visitPrint( Node $node ): void { |
554 | $this->visitEcho( $node ); |
555 | } |
556 | |
557 | /** |
558 | * This is for exit() and die(). If they're passed an argument, they behave the |
559 | * same as print. |
560 | * @param Node $node |
561 | */ |
562 | public function visitExit( Node $node ): void { |
563 | $this->visitEcho( $node ); |
564 | } |
565 | |
566 | /** |
567 | * Visits the backtick operator. Note that shell_exec() has a simple AST_CALL node. |
568 | * @param Node $node |
569 | */ |
570 | public function visitShellExec( Node $node ): void { |
571 | $this->visitSimpleSinkAndPropagate( |
572 | $node, |
573 | SecurityCheckPlugin::SHELL_EXEC_TAINT, |
574 | 'Backtick shell execution operator contains user controlled arg' |
575 | ); |
576 | // Its unclear if we should consider this tainted or not |
577 | $this->curTaintWithError = new TaintednessWithError( |
578 | Taintedness::newTainted(), |
579 | CausedByLines::emptySingleton(), |
580 | MethodLinks::emptySingleton() |
581 | ); |
582 | $this->setCachedData( $node ); |
583 | } |
584 | |
585 | /** |
586 | * @param Node $node |
587 | */ |
588 | public function visitIncludeOrEval( Node $node ): void { |
589 | if ( $node->flags === \ast\flags\EXEC_EVAL ) { |
590 | $taintValue = SecurityCheckPlugin::CODE_EXEC_TAINT; |
591 | $msg = 'The code supplied to `eval` is user controlled'; |
592 | } else { |
593 | $taintValue = SecurityCheckPlugin::PATH_EXEC_TAINT; |
594 | $msg = 'The included path is user controlled'; |
595 | } |
596 | $this->visitSimpleSinkAndPropagate( $node, $taintValue, $msg ); |
597 | // Strictly speaking we have no idea if the result |
598 | // of an eval() or require() is safe. But given that we |
599 | // don't know, and at least in the require() case its |
600 | // fairly likely to be safe, no point in complaining. |
601 | $this->setCurTaintSafe(); |
602 | $this->setCachedData( $node ); |
603 | } |
604 | |
605 | /** |
606 | * Also handles exit() and print |
607 | * |
608 | * We assume a web based system, where outputting HTML via echo |
609 | * is bad. This will have false positives in a CLI environment. |
610 | * |
611 | * @param Node $node |
612 | */ |
613 | public function visitEcho( Node $node ): void { |
614 | $this->visitSimpleSinkAndPropagate( |
615 | $node, |
616 | SecurityCheckPlugin::HTML_EXEC_TAINT, |
617 | 'Echoing expression that was not html escaped' |
618 | ); |
619 | $this->setCurTaintSafe(); |
620 | $this->setCachedData( $node ); |
621 | } |
622 | |
623 | /** |
624 | * @param Node $node |
625 | * @param int $sinkTaintInt |
626 | * @param string $issueMsg |
627 | */ |
628 | private function visitSimpleSinkAndPropagate( Node $node, int $sinkTaintInt, string $issueMsg ): void { |
629 | if ( !isset( $node->children['expr'] ) ) { |
630 | return; |
631 | } |
632 | $expr = $node->children['expr']; |
633 | $exprTaint = $this->getTaintedness( $expr ); |
634 | |
635 | $sinkTaint = new Taintedness( $sinkTaintInt ); |
636 | $rhsTaint = $exprTaint->getTaintedness(); |
637 | $this->maybeEmitIssue( |
638 | $sinkTaint, |
639 | $rhsTaint, |
640 | "$issueMsg{DETAILS}", |
641 | [ [ 'lines' => $exprTaint->getError(), 'sink' => false ] ] |
642 | ); |
643 | |
644 | if ( $expr instanceof Node && !$rhsTaint->has( Taintedness::flagsAsExecToYesTaint( $sinkTaintInt ) ) ) { |
645 | $this->backpropagateArgTaint( $expr, $sinkTaint ); |
646 | } |
647 | } |
648 | |
649 | /** |
650 | * @param Node $node |
651 | */ |
652 | public function visitStaticCall( Node $node ): void { |
653 | $this->visitMethodCall( $node ); |
654 | } |
655 | |
656 | /** |
657 | * @param Node $node |
658 | */ |
659 | public function visitNew( Node $node ): void { |
660 | $ctxNode = $this->getCtxN( $node ); |
661 | if ( !$node->children['class'] instanceof Node ) { |
662 | // Syntax error, don't crash |
663 | $this->setCurTaintSafe(); |
664 | return; |
665 | } |
666 | |
667 | // We check the __construct() method first, but the |
668 | // final resulting taint is from the __toString() |
669 | // method. This is a little hacky. |
670 | try { |
671 | // First do __construct() |
672 | $constructor = $ctxNode->getMethod( |
673 | '__construct', |
674 | false, |
675 | false, |
676 | true |
677 | ); |
678 | } catch ( NodeException | CodeBaseException | IssueException $_ ) { |
679 | $constructor = null; |
680 | } |
681 | |
682 | if ( $constructor ) { |
683 | $this->handleMethodCall( |
684 | $constructor, |
685 | $constructor->getFQSEN(), |
686 | $node->children['args']->children, |
687 | false |
688 | ); |
689 | } |
690 | |
691 | // Now return __toString() |
692 | try { |
693 | $clazzes = $ctxNode->getClassList( |
694 | false, |
695 | ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME, |
696 | null, |
697 | false |
698 | ); |
699 | } catch ( CodeBaseException | IssueException $e ) { |
700 | $this->debug( __METHOD__, 'Cannot get class: ' . $this->getDebugInfo( $e ) ); |
701 | $this->setCurTaintUnknown(); |
702 | $this->setCachedData( $node ); |
703 | return; |
704 | } |
705 | |
706 | foreach ( $clazzes as $clazz ) { |
707 | try { |
708 | $toString = $clazz->getMethodByName( $this->code_base, '__toString' ); |
709 | } catch ( CodeBaseException $_ ) { |
710 | // No __toString() in this class |
711 | continue; |
712 | } |
713 | |
714 | $toStringTaint = $this->handleMethodCall( $toString, $toString->getFQSEN(), [] ); |
715 | if ( !$this->curTaintWithError ) { |
716 | $this->curTaintWithError = $toStringTaint; |
717 | } else { |
718 | $this->curTaintWithError = $this->curTaintWithError->asMergedWith( $toStringTaint ); |
719 | } |
720 | } |
721 | |
722 | // If we find no __toString(), then presumably the object can't be outputted, so should be safe. |
723 | $this->curTaintWithError ??= TaintednessWithError::emptySingleton(); |
724 | |
725 | $this->setCachedData( $node ); |
726 | } |
727 | |
728 | /** |
729 | * @param Node $node |
730 | */ |
731 | public function visitMethodCall( Node $node ): void { |
732 | $methodName = $node->children['method']; |
733 | $isStatic = $node->kind === \ast\AST_STATIC_CALL; |
734 | try { |
735 | $method = $this->getCtxN( $node )->getMethod( $methodName, $isStatic, true ); |
736 | } catch ( NodeException | CodeBaseException | IssueException $e ) { |
737 | $this->debug( __METHOD__, "Cannot find method in node. " . $this->getDebugInfo( $e ) ); |
738 | $this->setCurTaintUnknown(); |
739 | $this->setCachedData( $node ); |
740 | return; |
741 | } |
742 | |
743 | $this->analyzeCallNode( $node, [ $method ] ); |
744 | } |
745 | |
746 | /** |
747 | * @param Node $node |
748 | * @param iterable<FunctionInterface> $funcs |
749 | */ |
750 | protected function analyzeCallNode( Node $node, iterable $funcs ): void { |
751 | $args = $node->children['args']->children; |
752 | foreach ( $funcs as $func ) { |
753 | // No point in analyzing abstract function declarations |
754 | if ( !$func instanceof FunctionLikeDeclarationType ) { |
755 | $callTaint = $this->handleMethodCall( $func, $func->getFQSEN(), $args ); |
756 | if ( !$this->curTaintWithError ) { |
757 | $this->curTaintWithError = $callTaint; |
758 | } else { |
759 | $this->curTaintWithError = $this->curTaintWithError->asMergedWith( $callTaint ); |
760 | } |
761 | } |
762 | } |
763 | $this->curTaintWithError ??= TaintednessWithError::emptySingleton(); |
764 | $this->setCachedData( $node ); |
765 | } |
766 | |
767 | /** |
768 | * @param Node $node |
769 | */ |
770 | public function visitNullsafeMethodCall( Node $node ): void { |
771 | $this->visitMethodCall( $node ); |
772 | } |
773 | |
774 | /** |
775 | * A function call |
776 | * |
777 | * @param Node $node |
778 | */ |
779 | public function visitCall( Node $node ): void { |
780 | $funcs = $this->getCtxN( $node->children['expr'] )->getFunctionFromNode(); |
781 | |
782 | if ( !$funcs ) { |
783 | $this->setCurTaintUnknown(); |
784 | $this->setCachedData( $node ); |
785 | return; |
786 | } |
787 | |
788 | $this->analyzeCallNode( $node, $funcs ); |
789 | } |
790 | |
791 | /** |
792 | * A variable (e.g. $foo) |
793 | * |
794 | * This always considers superglobals as tainted |
795 | * |
796 | * @param Node $node |
797 | */ |
798 | public function visitVar( Node $node ): void { |
799 | $varName = $this->getCtxN( $node )->getVariableName(); |
800 | if ( $varName === '' ) { |
801 | // Something that phan can't understand, e.g. `$$foo` with unknown `$foo`. |
802 | $this->setCurTaintUnknown(); |
803 | return; |
804 | } |
805 | |
806 | $hardcodedTaint = $this->getHardcodedTaintednessForVar( $varName ); |
807 | if ( $hardcodedTaint ) { |
808 | $this->curTaintWithError = new TaintednessWithError( |
809 | $hardcodedTaint, |
810 | CausedByLines::emptySingleton(), |
811 | MethodLinks::emptySingleton() |
812 | ); |
813 | $this->setCachedData( $node ); |
814 | return; |
815 | } |
816 | if ( !$this->context->getScope()->hasVariableWithName( $varName ) ) { |
817 | // Probably the var just isn't in scope yet. |
818 | // $this->debug( __METHOD__, "No var with name \$$varName in scope (Setting Unknown taint)" ); |
819 | $this->setCurTaintUnknown(); |
820 | $this->setCachedData( $node ); |
821 | return; |
822 | } |
823 | $variableObj = $this->context->getScope()->getVariableByName( $varName ); |
824 | $this->curTaintWithError = new TaintednessWithError( |
825 | $this->getTaintednessPhanObj( $variableObj ), |
826 | self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton(), |
827 | self::getMethodLinks( $variableObj ) ?? MethodLinks::emptySingleton() |
828 | ); |
829 | $this->setCachedData( $node ); |
830 | } |
831 | |
832 | /** |
833 | * If we hardcode taintedness for the given var name, return that taintedness; return null otherwise. |
834 | * This is currently used for superglobals, since they're always tainted, regardless of whether they're in |
835 | * the current scope: `function foo() use ($argv)` puts $argv in the local scope, but it retains its |
836 | * taintedness (see test closure2). |
837 | * |
838 | * @param string $varName |
839 | * @return Taintedness|null |
840 | */ |
841 | private function getHardcodedTaintednessForVar( string $varName ): ?Taintedness { |
842 | switch ( $varName ) { |
843 | case '_GET': |
844 | case '_POST': |
845 | case 'argc': |
846 | case 'argv': |
847 | case 'http_response_header': |
848 | case '_COOKIE': |
849 | case '_REQUEST': |
850 | // It's not entirely clear what $_ENV and $_SESSION should be considered |
851 | case '_ENV': |
852 | case '_SESSION': |
853 | // Hopefully we don't need to specify all keys for $_SERVER... |
854 | case '_SERVER': |
855 | return Taintedness::newTainted(); |
856 | case '_FILES': |
857 | $elTaint = Taintedness::newFromArray( [ |
858 | 'name' => Taintedness::newTainted(), |
859 | 'type' => Taintedness::newTainted(), |
860 | 'tmp_name' => Taintedness::safeSingleton(), |
861 | 'error' => Taintedness::safeSingleton(), |
862 | 'size' => Taintedness::safeSingleton(), |
863 | ] ); |
864 | return Taintedness::safeSingleton()->withAddedKeysTaintedness( SecurityCheckPlugin::YES_TAINT ) |
865 | // Use 'null' as fake offset to set unknownDims |
866 | ->withAddedOffsetTaintedness( null, $elTaint ); |
867 | case 'GLOBALS': |
868 | // Ideally this would recurse properly, but hopefully nobody is using $GLOBALS in complex ways |
869 | // that wouldn't be covered by this approximation. |
870 | |
871 | $filesTaintedness = $this->getHardcodedTaintednessForVar( '_FILES' ); |
872 | assert( $filesTaintedness !== null ); |
873 | return Taintedness::newFromArray( [ |
874 | '_GET' => Taintedness::newTainted(), |
875 | '_POST' => Taintedness::newTainted(), |
876 | '_SERVER' => Taintedness::newTainted(), |
877 | '_COOKIE' => Taintedness::newTainted(), |
878 | '_SESSION' => Taintedness::newTainted(), |
879 | '_REQUEST' => Taintedness::newTainted(), |
880 | '_ENV' => Taintedness::newTainted(), |
881 | '_FILES' => $filesTaintedness, |
882 | 'GLOBALS' => Taintedness::newTainted() |
883 | ] ); |
884 | default: |
885 | return null; |
886 | } |
887 | } |
888 | |
889 | /** |
890 | * A global declaration. Assume most globals are untainted. |
891 | * |
892 | * @param Node $node |
893 | */ |
894 | public function visitGlobal( Node $node ): void { |
895 | assert( isset( $node->children['var'] ) && $node->children['var']->kind === \ast\AST_VAR ); |
896 | |
897 | $varName = $node->children['var']->children['name']; |
898 | if ( !is_string( $varName ) || !$this->context->getScope()->hasVariableWithName( $varName ) ) { |
899 | // Something like global $$indirectReference; or the variable wasn't created somehow |
900 | return; |
901 | } |
902 | // Copy taintedness data from the actual global into the scoped clone |
903 | $gvar = $this->context->getScope()->getVariableByName( $varName ); |
904 | if ( !$gvar instanceof GlobalVariable ) { |
905 | // Likely a superglobal, nothing to do. |
906 | return; |
907 | } |
908 | $actualGlobal = $gvar->getElement(); |
909 | self::setTaintednessRaw( $gvar, self::getTaintednessRaw( $actualGlobal ) ?: Taintedness::safeSingleton() ); |
910 | self::setCausedByRaw( $gvar, self::getCausedByRaw( $actualGlobal ) ?? CausedByLines::emptySingleton() ); |
911 | self::setMethodLinks( $gvar, self::getMethodLinks( $actualGlobal ) ?? MethodLinks::emptySingleton() ); |
912 | } |
913 | |
914 | /** |
915 | * Set the taint of the function based on what's returned |
916 | * |
917 | * This attempts to match the return value up to the argument |
918 | * to figure out which argument might taint the function. This won't |
919 | * work in complex cases though. |
920 | * |
921 | * @param Node $node |
922 | */ |
923 | public function visitReturn( Node $node ): void { |
924 | if ( !$this->context->isInFunctionLikeScope() ) { |
925 | // E.g. a file that can be included. |
926 | $this->setCurTaintUnknown(); |
927 | $this->setCachedData( $node ); |
928 | return; |
929 | } |
930 | |
931 | $curFunc = $this->context->getFunctionLikeInScope( $this->code_base ); |
932 | |
933 | $this->setFuncTaintFromReturn( $node, $curFunc ); |
934 | |
935 | if ( $node->children['expr'] instanceof Node ) { |
936 | $collector = new ReturnObjectsCollectVisitor( $this->code_base, $this->context ); |
937 | self::addRetObjs( $curFunc, $collector->collectFromNode( $node ) ); |
938 | } |
939 | } |
940 | |
941 | /** |
942 | * @param Node $node |
943 | * @param FunctionInterface $func |
944 | */ |
945 | private function setFuncTaintFromReturn( Node $node, FunctionInterface $func ): void { |
946 | assert( $node->kind === \ast\AST_RETURN ); |
947 | $retExpr = $node->children['expr']; |
948 | $retTaintednessWithError = $this->getTaintedness( $retExpr ); |
949 | // Ensure we don't transmit any EXEC flag. |
950 | $retTaintedness = $retTaintednessWithError->getTaintedness()->withOnly( SecurityCheckPlugin::ALL_TAINT ); |
951 | if ( !$retExpr instanceof Node ) { |
952 | assert( $retTaintedness->isSafe() ); |
953 | $this->ensureFuncTaintIsSet( $func ); |
954 | return; |
955 | } |
956 | |
957 | $overallFuncTaint = $retTaintedness; |
958 | // Note, it's important that we only use the real type here (e.g. from typehints) and NOT |
959 | // the PHPDoc type, as it may be wrong. |
960 | $retTaintMask = $this->getTaintMaskForType( $func->getRealReturnType() ); |
961 | if ( $retTaintMask !== null ) { |
962 | $overallFuncTaint = $overallFuncTaint->withOnly( $retTaintMask->get() ); |
963 | } |
964 | |
965 | $paramTaint = new FunctionTaintedness( $overallFuncTaint ); |
966 | |
967 | if ( $retTaintMask === null || !$retTaintMask->isSafe() ) { |
968 | $funcError = FunctionCausedByLines::emptySingleton(); |
969 | $links = $retTaintednessWithError->getMethodLinks(); |
970 | $retError = $retTaintednessWithError->getError(); |
971 | // Note, not forCaller, as that doesn't see variadic parameters |
972 | $calleeParamList = $func->getParameterList(); |
973 | foreach ( $calleeParamList as $i => $param ) { |
974 | $presTaint = $links->asPreservedTaintednessForFuncParam( $func, $i ); |
975 | $paramError = $retError->asFilteredForFuncAndParam( $func, $i ); |
976 | if ( !$presTaint->isEmpty() ) { |
977 | $paramTaint = $param->isVariadic() |
978 | ? $paramTaint->withVariadicParamPreservedTaint( $i, $presTaint ) |
979 | : $paramTaint->withParamPreservedTaint( $i, $presTaint ); |
980 | } |
981 | if ( !$paramError->isEmpty() ) { |
982 | if ( $param->isVariadic() ) { |
983 | $funcError = $funcError->withVariadicParamPreservedLines( $i, $paramError ); |
984 | } else { |
985 | $funcError = $funcError->withParamPreservedLines( $i, $paramError ); |
986 | } |
987 | } |
988 | } |
989 | $genericError = $retError->getLinesForGenericReturn(); |
990 | if ( !$genericError->isEmpty() ) { |
991 | $funcError = $funcError->withGenericLines( $genericError ); |
992 | } |
993 | |
994 | $this->addFuncTaint( $func, $paramTaint ); |
995 | $newFuncTaint = self::getFuncTaint( $func ); |
996 | assert( $newFuncTaint !== null ); |
997 | |
998 | $this->maybeAddFuncError( $func, null, $paramTaint->withoutPreserved(), $newFuncTaint ); |
999 | $this->mergeFuncError( $func, $funcError, $newFuncTaint ); |
1000 | $this->maybeAddFuncError( $func, null, $paramTaint->asOnlyPreserved(), $newFuncTaint, $links ); |
1001 | } else { |
1002 | $this->addFuncTaint( $func, $paramTaint ); |
1003 | } |
1004 | } |
1005 | |
1006 | /** |
1007 | * @param Node $node |
1008 | */ |
1009 | public function visitArray( Node $node ): void { |
1010 | $curTaint = Taintedness::safeSingleton(); |
1011 | $curError = CausedByLines::emptySingleton(); |
1012 | $links = MethodLinks::emptySingleton(); |
1013 | // Current numeric key in the array |
1014 | $curNumKey = 0; |
1015 | foreach ( $node->children as $child ) { |
1016 | if ( $child === null ) { |
1017 | // Happens for list( , $x ) = foo() |
1018 | continue; |
1019 | } |
1020 | if ( $child->kind === \ast\AST_UNPACK ) { |
1021 | // PHP 7.4's in-place unpacking. |
1022 | // TODO Do something? |
1023 | continue; |
1024 | } |
1025 | assert( $child->kind === \ast\AST_ARRAY_ELEM ); |
1026 | $key = $child->children['key']; |
1027 | $keyTaintAll = $this->getTaintedness( $key ); |
1028 | $keyTaint = $keyTaintAll->getTaintedness(); |
1029 | $value = $child->children['value']; |
1030 | $valTaintAll = $this->getTaintedness( $value ); |
1031 | $valTaint = $valTaintAll->getTaintedness(); |
1032 | $sqlTaint = SecurityCheckPlugin::SQL_TAINT; |
1033 | |
1034 | if ( |
1035 | ( $keyTaint->has( $sqlTaint ) ) || ( |
1036 | ( $key === null || $this->nodeCanBeIntKey( $key ) ) |
1037 | && $valTaint->has( $sqlTaint ) |
1038 | && $this->nodeCanBeString( $value ) |
1039 | ) |
1040 | ) { |
1041 | $curTaint = $curTaint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
1042 | } |
1043 | // FIXME This will fail with in-place spread and when some numeric keys are specified |
1044 | // explicitly (at least). |
1045 | $offset = $key ?? $curNumKey++; |
1046 | $offset = $this->resolveOffset( $offset ); |
1047 | $curTaint = $curTaint->withAddedOffsetTaintedness( $offset, $valTaint ) |
1048 | ->withAddedKeysTaintedness( $keyTaint->get() ); |
1049 | $valError = $valTaintAll->getError()->asAllMaybeMovedAtOffset( $offset ); |
1050 | $keyError = $keyTaintAll->getError()->asAllMovedToKeys(); |
1051 | $curError = $curError->asMergedWith( $keyError ) |
1052 | ->asMergedWith( $valError ); |
1053 | $links = $links->withKeysLinks( $keyTaintAll->getMethodLinks()->getLinksCollapsing() ) |
1054 | ->withLinksAtDim( $offset, $valTaintAll->getMethodLinks() ); |
1055 | } |
1056 | $this->curTaintWithError = new TaintednessWithError( $curTaint, $curError, $links ); |
1057 | $this->setCachedData( $node ); |
1058 | } |
1059 | |
1060 | /** |
1061 | * @param Node $node |
1062 | */ |
1063 | public function visitClassConst( Node $node ): void { |
1064 | $this->setCurTaintSafe(); |
1065 | $this->setCachedData( $node ); |
1066 | } |
1067 | |
1068 | /** |
1069 | * @param Node $node |
1070 | */ |
1071 | public function visitConst( Node $node ): void { |
1072 | // We are going to assume nobody is doing stupid stuff like |
1073 | // define( "foo", $_GET['bar'] ); |
1074 | $this->setCurTaintSafe(); |
1075 | $this->setCachedData( $node ); |
1076 | } |
1077 | |
1078 | /** |
1079 | * The :: operator (for props) |
1080 | * @param Node $node |
1081 | */ |
1082 | public function visitStaticProp( Node $node ): void { |
1083 | $prop = $this->getPropFromNode( $node ); |
1084 | if ( !$prop ) { |
1085 | $this->setCurTaintUnknown(); |
1086 | return; |
1087 | } |
1088 | $this->curTaintWithError = new TaintednessWithError( |
1089 | $this->getTaintednessPhanObj( $prop ), |
1090 | self::getCausedByRaw( $prop ) ?? CausedByLines::emptySingleton(), |
1091 | self::getMethodLinks( $prop ) ?? MethodLinks::emptySingleton() |
1092 | ); |
1093 | $this->setCachedData( $node ); |
1094 | } |
1095 | |
1096 | /** |
1097 | * The -> operator (when not a method call) |
1098 | * @param Node $node |
1099 | */ |
1100 | public function visitProp( Node $node ): void { |
1101 | $nodeExpr = $node->children['expr']; |
1102 | if ( !$nodeExpr instanceof Node ) { |
1103 | // Syntax error. |
1104 | $this->setCurTaintSafe(); |
1105 | return; |
1106 | } |
1107 | |
1108 | // If the LHS expr can potentially be a stdClass, merge in its taintedness as well. |
1109 | // TODO Improve this (should similar to array offsets) |
1110 | $foundStdClass = false; |
1111 | $exprType = $this->getNodeType( $nodeExpr ); |
1112 | $stdClassType = FullyQualifiedClassName::getStdClassFQSEN()->asType(); |
1113 | if ( $exprType && $exprType->hasType( $stdClassType ) ) { |
1114 | $exprTaint = $this->getTaintedness( $nodeExpr ); |
1115 | $this->curTaintWithError = new TaintednessWithError( |
1116 | $exprTaint->getTaintedness(), |
1117 | $exprTaint->getError(), |
1118 | // TODO Links? |
1119 | MethodLinks::emptySingleton() |
1120 | ); |
1121 | $foundStdClass = true; |
1122 | } |
1123 | |
1124 | $prop = $this->getPropFromNode( $node ); |
1125 | if ( !$prop ) { |
1126 | if ( !$foundStdClass ) { |
1127 | $this->setCurTaintUnknown(); |
1128 | } |
1129 | $this->setCachedData( $node ); |
1130 | return; |
1131 | } |
1132 | |
1133 | $objTaint = $this->getTaintednessPhanObj( $prop ); |
1134 | $objError = self::getCausedByRaw( $prop ) ?? CausedByLines::emptySingleton(); |
1135 | $objLinks = self::getMethodLinks( $prop ) ?? MethodLinks::emptySingleton(); |
1136 | |
1137 | if ( $foundStdClass ) { |
1138 | $newTaint = $this->curTaintWithError->getTaintedness() |
1139 | ->asMergedWith( $objTaint->without( SecurityCheckPlugin::UNKNOWN_TAINT ) ); |
1140 | $this->curTaintWithError = new TaintednessWithError( |
1141 | $newTaint, |
1142 | $this->curTaintWithError->getError()->asMergedWith( $objError ), |
1143 | $this->curTaintWithError->getMethodLinks()->asMergedWith( $objLinks ) |
1144 | ); |
1145 | } else { |
1146 | $this->curTaintWithError = new TaintednessWithError( $objTaint, $objError, $objLinks ); |
1147 | } |
1148 | |
1149 | $this->setCachedData( $node ); |
1150 | } |
1151 | |
1152 | /** |
1153 | * @param Node $node |
1154 | */ |
1155 | public function visitNullsafeProp( Node $node ): void { |
1156 | $this->visitProp( $node ); |
1157 | } |
1158 | |
1159 | /** |
1160 | * Ternary operator. |
1161 | * @param Node $node |
1162 | */ |
1163 | public function visitConditional( Node $node ): void { |
1164 | if ( $node->children['true'] === null ) { |
1165 | // $foo ?: $bar; |
1166 | $trueTaint = $this->getTaintedness( $node->children['cond'] ); |
1167 | } else { |
1168 | $trueTaint = $this->getTaintedness( $node->children['true'] ); |
1169 | } |
1170 | $falseTaint = $this->getTaintedness( $node->children['false'] ); |
1171 | $this->curTaintWithError = $trueTaint->asMergedWith( $falseTaint ); |
1172 | $this->setCachedData( $node ); |
1173 | } |
1174 | |
1175 | /** |
1176 | * @param Node $node |
1177 | */ |
1178 | public function visitName( Node $node ): void { |
1179 | $this->setCurTaintSafe(); |
1180 | } |
1181 | |
1182 | /** |
1183 | * This is e.g. for class X implements Name,List |
1184 | * |
1185 | * @param Node $node |
1186 | */ |
1187 | public function visitNameList( Node $node ): void { |
1188 | $this->setCurTaintSafe(); |
1189 | } |
1190 | |
1191 | /** |
1192 | * @param Node $node |
1193 | */ |
1194 | public function visitUnaryOp( Node $node ): void { |
1195 | // ~ and @ are the only two unary ops |
1196 | // that can preserve taint (others cast bool or int) |
1197 | $unsafe = [ |
1198 | \ast\flags\UNARY_BITWISE_NOT, |
1199 | \ast\flags\UNARY_SILENCE |
1200 | ]; |
1201 | if ( in_array( $node->flags, $unsafe, true ) ) { |
1202 | $this->curTaintWithError = $this->getTaintedness( $node->children['expr'] ); |
1203 | } else { |
1204 | $this->setCurTaintSafe(); |
1205 | } |
1206 | $this->setCachedData( $node ); |
1207 | } |
1208 | |
1209 | /** |
1210 | * @param Node $node |
1211 | */ |
1212 | public function visitPostInc( Node $node ): void { |
1213 | $this->analyzeIncOrDec( $node ); |
1214 | } |
1215 | |
1216 | /** |
1217 | * @param Node $node |
1218 | */ |
1219 | public function visitPreInc( Node $node ): void { |
1220 | $this->analyzeIncOrDec( $node ); |
1221 | } |
1222 | |
1223 | /** |
1224 | * @param Node $node |
1225 | */ |
1226 | public function visitPostDec( Node $node ): void { |
1227 | $this->analyzeIncOrDec( $node ); |
1228 | } |
1229 | |
1230 | /** |
1231 | * @param Node $node |
1232 | */ |
1233 | public function visitPreDec( Node $node ): void { |
1234 | $this->analyzeIncOrDec( $node ); |
1235 | } |
1236 | |
1237 | /** |
1238 | * Handles all post/pre-increment/decrement operators. They have no effect on the |
1239 | * taintedness of a variable. |
1240 | * |
1241 | * @param Node $node |
1242 | */ |
1243 | private function analyzeIncOrDec( Node $node ): void { |
1244 | $this->curTaintWithError = $this->getTaintedness( $node->children['var'] ); |
1245 | $this->setCachedData( $node ); |
1246 | } |
1247 | |
1248 | /** |
1249 | * @param Node $node |
1250 | */ |
1251 | public function visitCast( Node $node ): void { |
1252 | // Casting between an array and object maintains |
1253 | // taint. Casting an object to a string calls __toString(). |
1254 | // Future TODO: handle the string case properly. |
1255 | $dangerousCasts = [ |
1256 | \ast\flags\TYPE_STRING, |
1257 | \ast\flags\TYPE_ARRAY, |
1258 | \ast\flags\TYPE_OBJECT |
1259 | ]; |
1260 | |
1261 | if ( !in_array( $node->flags, $dangerousCasts, true ) ) { |
1262 | $this->setCurTaintSafe(); |
1263 | } else { |
1264 | $exprTaint = $this->getTaintedness( $node->children['expr'] ); |
1265 | // Note, casting deletes shapes. |
1266 | $this->curTaintWithError = new TaintednessWithError( |
1267 | $exprTaint->getTaintedness()->asCollapsed(), |
1268 | $exprTaint->getError(), |
1269 | $exprTaint->getMethodLinks()->asCollapsed() |
1270 | ); |
1271 | } |
1272 | $this->setCachedData( $node ); |
1273 | } |
1274 | |
1275 | /** |
1276 | * The taint is the taint of all the child elements |
1277 | * |
1278 | * @param Node $node |
1279 | */ |
1280 | public function visitEncapsList( Node $node ): void { |
1281 | $this->curTaintWithError = TaintednessWithError::emptySingleton(); |
1282 | foreach ( $node->children as $child ) { |
1283 | $this->curTaintWithError = $this->curTaintWithError->asMergedWith( $this->getTaintedness( $child ) ); |
1284 | } |
1285 | $this->setCachedData( $node ); |
1286 | } |
1287 | |
1288 | /** |
1289 | * Visit a node that is always safe |
1290 | * |
1291 | * @param Node $node |
1292 | */ |
1293 | public function visitIsset( Node $node ): void { |
1294 | $this->setCurTaintSafe(); |
1295 | } |
1296 | |
1297 | /** |
1298 | * Visits calls to empty(), which is always safe |
1299 | * |
1300 | * @param Node $node |
1301 | */ |
1302 | public function visitEmpty( Node $node ): void { |
1303 | $this->setCurTaintSafe(); |
1304 | } |
1305 | |
1306 | /** |
1307 | * Visit a node that is always safe |
1308 | * |
1309 | * @param Node $node |
1310 | */ |
1311 | public function visitMagicConst( Node $node ): void { |
1312 | $this->setCurTaintSafe(); |
1313 | } |
1314 | |
1315 | /** |
1316 | * Visit a node that is always safe |
1317 | * |
1318 | * @param Node $node |
1319 | */ |
1320 | public function visitInstanceOf( Node $node ): void { |
1321 | $this->setCurTaintSafe(); |
1322 | } |
1323 | |
1324 | /** |
1325 | * @param Node $node |
1326 | */ |
1327 | public function visitMatch( Node $node ): void { |
1328 | $this->curTaintWithError = TaintednessWithError::emptySingleton(); |
1329 | // Based on UnionTypeVisitor |
1330 | foreach ( $node->children['stmts']->children as $armNode ) { |
1331 | // It sounds a bit weird to have to call this ourselves, but aight. |
1332 | if ( !BlockExitStatusChecker::willUnconditionallyThrowOrReturn( $armNode ) ) { |
1333 | // Note, we're straight using the expr to avoid implementing visitMatchArm |
1334 | $this->curTaintWithError = $this->curTaintWithError |
1335 | ->asMergedWith( $this->getTaintedness( $armNode->children['expr'] ) ); |
1336 | } |
1337 | } |
1338 | |
1339 | $this->setCachedData( $node ); |
1340 | } |
1341 | } |