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