Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.95% |
142 / 148 |
|
55.56% |
5 / 9 |
CRAP | |
0.00% |
0 / 1 |
TaintednessAssignVisitor | |
95.95% |
142 / 148 |
|
55.56% |
5 / 9 |
47 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
isRHSArray | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
visitArray | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
7 | |||
visitVar | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
visitProp | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
visitStaticProp | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
maybeAddNumkeyOnAssignmentLHS | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
visitDim | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
3.00 | |||
doAssignmentSingleElement | |
100.00% |
73 / 73 |
|
100.00% |
1 / 1 |
21 |
1 | <?php |
2 | |
3 | namespace SecurityCheckPlugin; |
4 | |
5 | use ast\Node; |
6 | use Closure; |
7 | use Phan\CodeBase; |
8 | use Phan\Exception\IssueException; |
9 | use Phan\Exception\NodeException; |
10 | use Phan\Exception\UnanalyzableException; |
11 | use Phan\Language\Context; |
12 | use Phan\Language\Element\GlobalVariable; |
13 | use Phan\Language\Element\Property; |
14 | use Phan\Language\Element\TypedElementInterface; |
15 | use Phan\PluginV3\PluginAwareBaseAnalysisVisitor; |
16 | |
17 | /** |
18 | * @see \Phan\Analysis\AssignmentVisitor |
19 | */ |
20 | class TaintednessAssignVisitor extends PluginAwareBaseAnalysisVisitor { |
21 | use TaintednessBaseVisitor; |
22 | |
23 | /** @var Taintedness */ |
24 | private $rightTaint; |
25 | |
26 | /** @var Taintedness */ |
27 | private $errorTaint; |
28 | |
29 | /** @var MethodLinks */ |
30 | private $errorLinks; |
31 | |
32 | /** @var CausedByLines */ |
33 | private $rightError; |
34 | |
35 | /** @var MethodLinks */ |
36 | private $rightLinks; |
37 | |
38 | /** @var bool|null */ |
39 | private $rhsIsArray; |
40 | |
41 | /** @var Closure|null */ |
42 | private $rhsIsArrayGetter; |
43 | |
44 | /** @var int */ |
45 | private $dimDepth; |
46 | |
47 | /** |
48 | * @inheritDoc |
49 | * @param Taintedness $rightTaint |
50 | * @param CausedByLines $rightLines |
51 | * @param MethodLinks $rightLinks |
52 | * @param Taintedness $errorTaint |
53 | * @param MethodLinks $errorLinks |
54 | * @param Closure|bool $rhsIsArrayOrGetter |
55 | * @phan-param Closure():bool|bool $rhsIsArrayOrGetter |
56 | * @param int $depth |
57 | */ |
58 | public function __construct( |
59 | CodeBase $code_base, |
60 | Context $context, |
61 | Taintedness $rightTaint, |
62 | CausedByLines $rightLines, |
63 | MethodLinks $rightLinks, |
64 | Taintedness $errorTaint, |
65 | MethodLinks $errorLinks, |
66 | $rhsIsArrayOrGetter, |
67 | int $depth = 0 |
68 | ) { |
69 | parent::__construct( $code_base, $context ); |
70 | $this->rightTaint = $rightTaint; |
71 | $this->rightError = $rightLines; |
72 | $this->rightLinks = $rightLinks; |
73 | $this->errorTaint = $errorTaint; |
74 | $this->errorLinks = $errorLinks; |
75 | if ( is_callable( $rhsIsArrayOrGetter ) ) { |
76 | $this->rhsIsArrayGetter = $rhsIsArrayOrGetter; |
77 | } else { |
78 | $this->rhsIsArray = $rhsIsArrayOrGetter; |
79 | } |
80 | $this->dimDepth = $depth; |
81 | } |
82 | |
83 | private function isRHSArray(): bool { |
84 | if ( $this->rhsIsArray !== null ) { |
85 | return $this->rhsIsArray; |
86 | } |
87 | $this->rhsIsArray = ( $this->rhsIsArrayGetter )(); |
88 | return $this->rhsIsArray; |
89 | } |
90 | |
91 | /** |
92 | * @param Node $node |
93 | */ |
94 | public function visitArray( Node $node ): void { |
95 | $numKey = 0; |
96 | foreach ( $node->children as $child ) { |
97 | if ( $child === null ) { |
98 | $numKey++; |
99 | continue; |
100 | } |
101 | if ( !$child instanceof Node || $child->kind !== \ast\AST_ARRAY_ELEM ) { |
102 | // Syntax error. |
103 | return; |
104 | } |
105 | $key = $child->children['key'] !== null ? $this->resolveOffset( $child->children['key'] ) : $numKey++; |
106 | $value = $child->children['value']; |
107 | if ( !$value instanceof Node ) { |
108 | // Syntax error, don't crash, and bail out immediately. |
109 | return; |
110 | } |
111 | $childVisitor = new self( |
112 | $this->code_base, |
113 | $this->context, |
114 | $this->rightTaint->getTaintednessForOffsetOrWhole( $key ), |
115 | $this->rightError->getForDim( $key ), |
116 | $this->rightLinks, |
117 | $this->errorTaint->getTaintednessForOffsetOrWhole( $key ), |
118 | $this->errorLinks, |
119 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
120 | $this->rhsIsArray ?? $this->rhsIsArrayGetter, |
121 | $this->dimDepth |
122 | ); |
123 | $childVisitor( $value ); |
124 | } |
125 | } |
126 | |
127 | /** |
128 | * @param Node $node |
129 | */ |
130 | public function visitVar( Node $node ): void { |
131 | try { |
132 | $var = $this->getCtxN( $node )->getVariable(); |
133 | } catch ( NodeException | IssueException $_ ) { |
134 | return; |
135 | } |
136 | $this->doAssignmentSingleElement( $var ); |
137 | } |
138 | |
139 | /** |
140 | * @param Node $node |
141 | */ |
142 | public function visitProp( Node $node ): void { |
143 | try { |
144 | $prop = $this->getCtxN( $node )->getProperty( false ); |
145 | } catch ( NodeException | IssueException | UnanalyzableException $_ ) { |
146 | return; |
147 | } |
148 | $this->doAssignmentSingleElement( $prop ); |
149 | } |
150 | |
151 | /** |
152 | * @param Node $node |
153 | */ |
154 | public function visitStaticProp( Node $node ): void { |
155 | try { |
156 | $prop = $this->getCtxN( $node )->getProperty( true ); |
157 | } catch ( NodeException | IssueException | UnanalyzableException $_ ) { |
158 | return; |
159 | } |
160 | $this->doAssignmentSingleElement( $prop ); |
161 | } |
162 | |
163 | /** |
164 | * If we're assigning an SQL tainted value as an array key |
165 | * or as the value of a numeric key, then set NUMKEY taint. |
166 | * |
167 | * @param Node $dimLHS |
168 | */ |
169 | private function maybeAddNumkeyOnAssignmentLHS( Node $dimLHS ): void { |
170 | if ( $this->rightTaint->has( SecurityCheckPlugin::SQL_NUMKEY_TAINT ) ) { |
171 | // Already there, no need to add it again. |
172 | return; |
173 | } |
174 | |
175 | $dim = $dimLHS->children['dim']; |
176 | if ( |
177 | $this->rightTaint->has( SecurityCheckPlugin::SQL_TAINT ) |
178 | && ( $dim === null || $this->nodeCanBeIntKey( $dim ) ) |
179 | && !$this->isRHSArray() |
180 | ) { |
181 | $this->rightTaint = $this->rightTaint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
182 | $this->errorTaint = $this->errorTaint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
183 | } |
184 | } |
185 | |
186 | /** |
187 | * @param Node $node |
188 | */ |
189 | public function visitDim( Node $node ): void { |
190 | if ( !$node->children['expr'] instanceof Node ) { |
191 | // Invalid syntax. |
192 | return; |
193 | } |
194 | $dimNode = $node->children['dim']; |
195 | if ( $dimNode === null ) { |
196 | $curOff = null; |
197 | } else { |
198 | $curOff = $this->resolveOffset( $dimNode ); |
199 | } |
200 | $this->dimDepth++; |
201 | $dimTaintWithErr = $this->getTaintedness( $dimNode ); |
202 | $dimTaintInt = $dimTaintWithErr->getTaintedness()->get(); |
203 | $this->rightTaint = $this->rightTaint->asMaybeMovedAtOffset( $curOff, $dimTaintInt ); |
204 | $dimLinks = $dimTaintWithErr->getMethodLinks()->getLinksCollapsing(); |
205 | $this->rightLinks = $this->rightLinks->asMaybeMovedAtOffset( $curOff, $dimLinks ); |
206 | $dimError = $dimTaintWithErr->getError()->asAllMovedToKeys(); |
207 | $this->rightError = $this->rightError->asAllMaybeMovedAtOffset( $curOff )->asMergedWith( $dimError ); |
208 | $this->errorTaint = $this->errorTaint->asMaybeMovedAtOffset( $curOff, $dimTaintInt ); |
209 | $this->errorLinks = $this->errorLinks->asMaybeMovedAtOffset( $curOff, $dimLinks ); |
210 | $this->maybeAddNumkeyOnAssignmentLHS( $node ); |
211 | $this( $node->children['expr'] ); |
212 | } |
213 | |
214 | /** |
215 | * @param TypedElementInterface $variableObj |
216 | */ |
217 | private function doAssignmentSingleElement( |
218 | TypedElementInterface $variableObj |
219 | ): void { |
220 | $globalVarObj = $variableObj instanceof GlobalVariable ? $variableObj->getElement() : null; |
221 | |
222 | // Make sure assigning to $this->bar doesn't kill the whole prop taint. |
223 | // Note: If there is a local variable that is a reference to another non-local variable, this will not |
224 | // affect the non-local one (Pass by reference arguments are handled separately and work as expected). |
225 | // TODO Should we also check for normal Variables in the global scope? See test setafterexec |
226 | $override = !( $variableObj instanceof Property ) && !$globalVarObj; |
227 | |
228 | $overrideTaint = $override; |
229 | if ( $this->dimDepth > 0 ) { |
230 | $curTaint = self::getTaintednessRaw( $variableObj ); |
231 | if ( $curTaint ) { |
232 | $newTaint = $override |
233 | ? $curTaint->asMergedForAssignment( $this->rightTaint, $this->dimDepth ) |
234 | : $curTaint->asMergedWith( $this->rightTaint ); |
235 | } else { |
236 | $newTaint = $this->rightTaint; |
237 | } |
238 | $overrideTaint = true; |
239 | } else { |
240 | $newTaint = $this->rightTaint; |
241 | } |
242 | $this->setTaintedness( $variableObj, $newTaint, $overrideTaint ); |
243 | |
244 | if ( $globalVarObj ) { |
245 | // Merge the taint on the "true" global object, too |
246 | if ( $this->dimDepth > 0 ) { |
247 | $curGlobalTaint = self::getTaintednessRaw( $globalVarObj ); |
248 | if ( $curGlobalTaint ) { |
249 | $newGlobalTaint = $curGlobalTaint->asMergedWith( $this->rightTaint ); |
250 | } else { |
251 | $newGlobalTaint = $this->rightTaint; |
252 | } |
253 | $overrideGlobalTaint = true; |
254 | } else { |
255 | $newGlobalTaint = $this->rightTaint; |
256 | $overrideGlobalTaint = false; |
257 | } |
258 | $this->setTaintedness( $globalVarObj, $newGlobalTaint, $overrideGlobalTaint ); |
259 | } |
260 | |
261 | if ( $this->dimDepth > 0 ) { |
262 | $curLinks = self::getMethodLinks( $variableObj ) ?? MethodLinks::emptySingleton(); |
263 | $newLinks = $override |
264 | ? $curLinks->asMergedForAssignment( $this->rightLinks, $this->dimDepth ) |
265 | : $curLinks->asMergedWith( $this->rightLinks ); |
266 | $overrideLinks = true; |
267 | } else { |
268 | $newLinks = $this->rightLinks; |
269 | $overrideLinks = $override; |
270 | } |
271 | $this->mergeTaintDependencies( $variableObj, $newLinks, $overrideLinks ); |
272 | if ( $globalVarObj ) { |
273 | // Merge dependencies on the global copy as well |
274 | if ( $this->dimDepth > 0 ) { |
275 | $curGlobalLinks = self::getMethodLinks( $globalVarObj ); |
276 | $newGlobalLinks = $curGlobalLinks |
277 | ? $curGlobalLinks->asMergedWith( $this->rightLinks ) |
278 | : $this->rightLinks; |
279 | $overrideGlobalLinks = true; |
280 | } else { |
281 | $newGlobalLinks = $this->rightLinks; |
282 | $overrideGlobalLinks = false; |
283 | } |
284 | $this->mergeTaintDependencies( $globalVarObj, $newGlobalLinks, $overrideGlobalLinks ); |
285 | } |
286 | |
287 | $curLineCausedBy = $this->getCausedByLinesToAdd( $this->errorTaint, $this->errorLinks ); |
288 | if ( $this->dimDepth > 0 ) { |
289 | $curError = self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton(); |
290 | $newError = $override |
291 | ? $curError->asMergedWith( $this->rightError, $this->dimDepth ) |
292 | : $curError->asMergedWith( $this->rightError ); |
293 | $overrideError = true; |
294 | } else { |
295 | $newError = $this->rightError; |
296 | $overrideError = $override; |
297 | } |
298 | $curError = $overrideError |
299 | ? CausedByLines::emptySingleton() |
300 | : self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton(); |
301 | $newOverallError = $curError->withAddedLines( $curLineCausedBy, $this->errorTaint, $this->errorLinks ) |
302 | ->asMergedWith( $newError ); |
303 | self::setCausedByRaw( $variableObj, $newOverallError ); |
304 | |
305 | if ( $globalVarObj ) { |
306 | if ( $this->dimDepth > 0 ) { |
307 | $curGlobalError = self::getCausedByRaw( $globalVarObj ); |
308 | $newGlobalError = $curGlobalError |
309 | ? $curGlobalError->asMergedWith( $this->rightError ) |
310 | : $this->rightError; |
311 | $overrideGlobalError = true; |
312 | } else { |
313 | $newGlobalError = $this->rightError; |
314 | $overrideGlobalError = false; |
315 | } |
316 | $curGlobalError = $overrideGlobalError |
317 | ? CausedByLines::emptySingleton() |
318 | : self::getCausedByRaw( $globalVarObj ) ?? CausedByLines::emptySingleton(); |
319 | $newOverallGlobalError = $curGlobalError |
320 | ->withAddedLines( $curLineCausedBy, $this->errorTaint, $this->errorLinks ) |
321 | ->asMergedWith( $newGlobalError ); |
322 | self::setCausedByRaw( $globalVarObj, $newOverallGlobalError ); |
323 | } |
324 | } |
325 | } |