Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.94% |
248 / 282 |
|
84.44% |
38 / 45 |
CRAP | |
0.00% |
0 / 1 |
Taintedness | |
87.94% |
248 / 282 |
|
84.44% |
38 / 45 |
158.17 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newSafe | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newUnknown | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newTainted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromArray | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
get | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
asCollapsed | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
asKnownKeysMadeUnknown | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getAllKeysTaint | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
add | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
with | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
remove | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
without | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
has | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
keepOnly | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
withOnly | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
intersectForSink | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
7 | |||
removeKnownKeysFrom | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
mergeWith | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
6 | |||
asMergedWith | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setOffsetTaintedness | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addKeysTaintedness | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
asMergedForAssignment | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
arrayPlus | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
asArrayPlusWith | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTaintednessForOffsetOrWhole | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
asMaybeMovedAtOffset | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
asValueFirstLevel | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
withoutKey | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
withoutKeys | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
asKeyForForeach | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
arrayReplace | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
arrayMerge | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
isSafe | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
asExecToYesTaint | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
asYesToExecTaint | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
asMovedAtRelevantOffsetsForBackprop | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
6.05 | |||
flagsAsExecToYesTaint | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
flagsAsYesToExecTaint | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
asPreservedTaintedness | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
decomposeForLinks | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
toString | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
toShortString | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
__clone | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
__toString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php declare( strict_types=1 ); |
2 | |
3 | namespace SecurityCheckPlugin; |
4 | |
5 | use ast\Node; |
6 | |
7 | /** |
8 | * Value object used to store taintedness. This should always be used to manipulate taintedness values, |
9 | * instead of directly using taint constants directly (except for comparisons etc.). |
10 | * |
11 | * Note that this class should be used as copy-on-write (like phan's UnionType), so in-place |
12 | * manipulation should never be done on phan objects. |
13 | */ |
14 | class Taintedness { |
15 | /** @var int Combination of the class constants */ |
16 | private $flags; |
17 | |
18 | /** @var self[] Taintedness for each possible array element */ |
19 | private $dimTaint = []; |
20 | |
21 | /** @var int Taintedness of the array keys */ |
22 | private $keysTaint = SecurityCheckPlugin::NO_TAINT; |
23 | |
24 | /** |
25 | * @var self|null Taintedness for array elements that we couldn't attribute to any key |
26 | */ |
27 | private $unknownDimsTaint; |
28 | |
29 | /** |
30 | * @param int $val One of the class constants |
31 | */ |
32 | public function __construct( int $val ) { |
33 | $this->flags = $val; |
34 | } |
35 | |
36 | // Common creation shortcuts |
37 | |
38 | /** |
39 | * @return self |
40 | */ |
41 | public static function newSafe(): self { |
42 | return new self( SecurityCheckPlugin::NO_TAINT ); |
43 | } |
44 | |
45 | /** |
46 | * @return self |
47 | */ |
48 | public static function newUnknown(): self { |
49 | return new self( SecurityCheckPlugin::UNKNOWN_TAINT ); |
50 | } |
51 | |
52 | /** |
53 | * @return self |
54 | */ |
55 | public static function newTainted(): self { |
56 | return new self( SecurityCheckPlugin::YES_TAINT ); |
57 | } |
58 | |
59 | /** |
60 | * @param Taintedness[] $values |
61 | * @return self |
62 | */ |
63 | public static function newFromArray( array $values ): self { |
64 | $ret = self::newSafe(); |
65 | foreach ( $values as $key => $value ) { |
66 | assert( $value instanceof self ); |
67 | $ret->setOffsetTaintedness( $key, $value ); |
68 | } |
69 | return $ret; |
70 | } |
71 | |
72 | /** |
73 | * Get a numeric representation of the taint stored in this object. This includes own taint, |
74 | * array keys and whatnot. |
75 | * @note This should almost NEVER be used outside of this class! Use accessors as much as possible! |
76 | * |
77 | * @return int |
78 | */ |
79 | public function get(): int { |
80 | $ret = $this->flags | $this->getAllKeysTaint() | $this->keysTaint; |
81 | return $this->unknownDimsTaint ? ( $ret | $this->unknownDimsTaint->get() ) : $ret; |
82 | } |
83 | |
84 | /** |
85 | * Get a flattened version of this object, with any taint from keys etc. collapsed into flags |
86 | * @return $this |
87 | */ |
88 | public function asCollapsed(): self { |
89 | return new self( $this->get() ); |
90 | } |
91 | |
92 | /** |
93 | * Returns a copy of this object where the taintedness of every known key has been reassigned |
94 | * to unknown keys. |
95 | * @return self |
96 | */ |
97 | public function asKnownKeysMadeUnknown(): self { |
98 | $ret = new self( $this->flags ); |
99 | $ret->keysTaint = $this->keysTaint; |
100 | $ret->unknownDimsTaint = $this->unknownDimsTaint; |
101 | if ( $this->dimTaint ) { |
102 | $ret->unknownDimsTaint ??= self::newSafe(); |
103 | foreach ( $this->dimTaint as $keyTaint ) { |
104 | $ret->unknownDimsTaint->mergeWith( $keyTaint ); |
105 | } |
106 | } |
107 | return $ret; |
108 | } |
109 | |
110 | /** |
111 | * Recursively extract the taintedness from each key. |
112 | * |
113 | * @return int |
114 | */ |
115 | private function getAllKeysTaint(): int { |
116 | $ret = SecurityCheckPlugin::NO_TAINT; |
117 | foreach ( $this->dimTaint as $val ) { |
118 | $ret |= $val->get(); |
119 | } |
120 | return $ret; |
121 | } |
122 | |
123 | // Value manipulation |
124 | |
125 | /** |
126 | * Add the given taint to this object's flags, *without* creating a clone |
127 | * @see Taintedness::with() if you need a clone |
128 | * @see Taintedness::mergeWith() if you want to preserve the whole shape |
129 | * |
130 | * @param int $taint |
131 | */ |
132 | public function add( int $taint ): void { |
133 | // TODO: Should this clear UNKNOWN_TAINT if its present only in one of the args? |
134 | $this->flags |= $taint; |
135 | } |
136 | |
137 | /** |
138 | * Returns a copy of this object, with the bits in $other added to flags. |
139 | * @see Taintedness::add() for the in-place version |
140 | * @see Taintedness::asMergedWith() if you want to preserve the whole shape |
141 | * |
142 | * @param int $other |
143 | * @return $this |
144 | */ |
145 | public function with( int $other ): self { |
146 | $ret = clone $this; |
147 | $ret->add( $other ); |
148 | return $ret; |
149 | } |
150 | |
151 | /** |
152 | * Recursively remove the given taint from this object, *without* creating a clone |
153 | * @see Taintedness::without() if you need a clone |
154 | * |
155 | * @param int $other |
156 | */ |
157 | public function remove( int $other ): void { |
158 | $this->keepOnly( ~$other ); |
159 | } |
160 | |
161 | /** |
162 | * Returns a copy of this object, with the bits in $other removed recursively. |
163 | * @see Taintedness::remove() for the in-place version |
164 | * |
165 | * @param int $other |
166 | * @return $this |
167 | */ |
168 | public function without( int $other ): self { |
169 | $ret = clone $this; |
170 | $ret->remove( $other ); |
171 | return $ret; |
172 | } |
173 | |
174 | /** |
175 | * Check whether this object has the given flag, recursively. |
176 | * @note If $taint has more than one flag, this will check for at least one, not all. |
177 | * |
178 | * @param int $taint |
179 | * @return bool |
180 | */ |
181 | public function has( int $taint ): bool { |
182 | // Avoid using get() for performance |
183 | if ( ( $this->flags & $taint ) !== SecurityCheckPlugin::NO_TAINT ) { |
184 | return true; |
185 | } |
186 | if ( ( $this->keysTaint & $taint ) !== SecurityCheckPlugin::NO_TAINT ) { |
187 | return true; |
188 | } |
189 | if ( $this->unknownDimsTaint && $this->unknownDimsTaint->has( $taint ) ) { |
190 | return true; |
191 | } |
192 | foreach ( $this->dimTaint as $val ) { |
193 | if ( $val->has( $taint ) ) { |
194 | return true; |
195 | } |
196 | } |
197 | return false; |
198 | } |
199 | |
200 | /** |
201 | * Keep only the taint in $taint, recursively, preserving the shape and without creating a copy. |
202 | * @see Taintedness::withOnly if you need a clone |
203 | * |
204 | * @param int $taint |
205 | */ |
206 | public function keepOnly( int $taint ): void { |
207 | $this->flags &= $taint; |
208 | if ( $this->unknownDimsTaint ) { |
209 | $this->unknownDimsTaint->keepOnly( $taint ); |
210 | } |
211 | $this->keysTaint &= $taint; |
212 | foreach ( $this->dimTaint as $val ) { |
213 | $val->keepOnly( $taint ); |
214 | } |
215 | } |
216 | |
217 | /** |
218 | * Returns a copy of this object, with only the taint in $taint kept (recursively, preserving the shape) |
219 | * @see Taintedness::keepOnly() for the in-place version |
220 | * |
221 | * @param int $other |
222 | * @return $this |
223 | */ |
224 | public function withOnly( int $other ): self { |
225 | $ret = clone $this; |
226 | $ret->keepOnly( $other ); |
227 | return $ret; |
228 | } |
229 | |
230 | /** |
231 | * Intersect the taintedness of a value against that of a sink, to later determine whether the |
232 | * expression is safe. In case of function calls, $sink is the param taint and $value is the arg taint. |
233 | * |
234 | * @note The order of the arguments is important! This method preserves the shape of $sink, not $value. |
235 | * |
236 | * @note The order of the arguments is important! This method preserves the shape of $sink, not $value. |
237 | * |
238 | * @param Taintedness $sink |
239 | * @param Taintedness $value |
240 | * @return self |
241 | */ |
242 | public static function intersectForSink( self $sink, self $value ): self { |
243 | $intersect = new self( SecurityCheckPlugin::NO_TAINT ); |
244 | // If the sink has non-zero flags, intersect it with the whole other side. This particularly preserves |
245 | // the shape of $sink, discarding anything from $value if the sink has a NO_TAINT in that position. |
246 | if ( $sink->flags & SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT ) { |
247 | // Special case: NUMKEY is only for the outer array |
248 | $rightFlags = $value->flags | $value->keysTaint; |
249 | if ( $value->keysTaint & SecurityCheckPlugin::SQL_TAINT ) { |
250 | // FIXME HACK: If keys are tainted, add numkey. This assumes that numkey is really only used for |
251 | // Database methods, where keys are never escaped. |
252 | $rightFlags |= SecurityCheckPlugin::SQL_NUMKEY_TAINT; |
253 | } |
254 | $rightFlags |= ( $value->getAllKeysTaint() & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
255 | if ( $value->unknownDimsTaint ) { |
256 | $rightFlags |= $value->unknownDimsTaint->get() & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT; |
257 | } |
258 | $intersect->flags = $sink->flags & ( ( $rightFlags & SecurityCheckPlugin::ALL_TAINT ) << 1 ); |
259 | } elseif ( $sink->flags ) { |
260 | $intersect->flags = $sink->flags & ( ( $value->get() & SecurityCheckPlugin::ALL_TAINT ) << 1 ); |
261 | } |
262 | if ( $sink->unknownDimsTaint ) { |
263 | $intersect->unknownDimsTaint = self::intersectForSink( |
264 | $sink->unknownDimsTaint, |
265 | $value->asValueFirstLevel() |
266 | ); |
267 | } |
268 | $valueKeysAsExec = ( ( $value->keysTaint | $value->flags ) & SecurityCheckPlugin::ALL_TAINT ) << 1; |
269 | $intersect->keysTaint = $sink->keysTaint & $valueKeysAsExec; |
270 | foreach ( $sink->dimTaint as $key => $dTaint ) { |
271 | $intersect->dimTaint[$key] = self::intersectForSink( |
272 | $dTaint, |
273 | $value->getTaintednessForOffsetOrWhole( $key ) |
274 | ); |
275 | } |
276 | return $intersect; |
277 | } |
278 | |
279 | /** |
280 | * Removes offset data from $this for all known offsets of $other, in place. |
281 | * |
282 | * @param Taintedness $other |
283 | * @return void |
284 | */ |
285 | public function removeKnownKeysFrom( self $other ): void { |
286 | foreach ( $other->dimTaint as $key => $_ ) { |
287 | unset( $this->dimTaint[$key] ); |
288 | } |
289 | if ( |
290 | ( $this->flags & SecurityCheckPlugin::SQL_NUMKEY_TAINT ) && |
291 | !$this->has( SecurityCheckPlugin::SQL_TAINT ) |
292 | ) { |
293 | // Note that this adjustment is not guaranteed to happen immediately after the removal of the last |
294 | // integer key. For instance, in [ 0 => unsafe, 'foo' => unsafe ], if only the element 0 is removed, |
295 | // this branch will not run because 'foo' still contributes sql taint. |
296 | $this->flags &= ~SecurityCheckPlugin::SQL_NUMKEY_TAINT; |
297 | } |
298 | } |
299 | |
300 | /** |
301 | * Merge this object with $other, recursively and without creating a copy. |
302 | * @see Taintedness::asMergedWith() if you need a copy |
303 | * |
304 | * @param Taintedness $other |
305 | */ |
306 | public function mergeWith( self $other ): void { |
307 | $this->flags |= $other->flags; |
308 | if ( $other->unknownDimsTaint && !$this->unknownDimsTaint ) { |
309 | $this->unknownDimsTaint = $other->unknownDimsTaint; |
310 | } elseif ( $other->unknownDimsTaint ) { |
311 | $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint ); |
312 | } |
313 | $this->keysTaint |= $other->keysTaint; |
314 | foreach ( $other->dimTaint as $key => $val ) { |
315 | if ( !isset( $this->dimTaint[$key] ) ) { |
316 | $this->dimTaint[$key] = clone $val; |
317 | } else { |
318 | $this->dimTaint[$key]->mergeWith( $val ); |
319 | } |
320 | } |
321 | } |
322 | |
323 | /** |
324 | * Merge this object with $other, recursively, creating a copy. |
325 | * @see Taintedness::mergeWith() for in-place merge |
326 | * |
327 | * @param Taintedness $other |
328 | * @return $this |
329 | */ |
330 | public function asMergedWith( self $other ): self { |
331 | $ret = clone $this; |
332 | $ret->mergeWith( $other ); |
333 | return $ret; |
334 | } |
335 | |
336 | // Offsets taintedness |
337 | |
338 | /** |
339 | * Set the taintedness for $offset to $value, in place |
340 | * |
341 | * @param Node|mixed $offset Node or a scalar value, already resolved |
342 | * @param Taintedness $value |
343 | */ |
344 | public function setOffsetTaintedness( $offset, self $value ): void { |
345 | if ( is_scalar( $offset ) ) { |
346 | $this->dimTaint[$offset] = $value; |
347 | } else { |
348 | $this->unknownDimsTaint ??= self::newSafe(); |
349 | $this->unknownDimsTaint->mergeWith( $value ); |
350 | } |
351 | } |
352 | |
353 | /** |
354 | * Adds the bits in $value to the taintedness of the keys |
355 | * @param int $value |
356 | */ |
357 | public function addKeysTaintedness( int $value ): void { |
358 | $this->keysTaint |= $value; |
359 | } |
360 | |
361 | /** |
362 | * @param self $other |
363 | * @param int $depth |
364 | * @return self |
365 | */ |
366 | public function asMergedForAssignment( self $other, int $depth ): self { |
367 | if ( $depth === 0 ) { |
368 | return $other; |
369 | } |
370 | $ret = clone $this; |
371 | $ret->flags |= $other->flags; |
372 | $ret->keysTaint |= $other->keysTaint; |
373 | if ( !$ret->unknownDimsTaint ) { |
374 | $ret->unknownDimsTaint = $other->unknownDimsTaint; |
375 | } elseif ( $other->unknownDimsTaint ) { |
376 | $ret->unknownDimsTaint->mergeWith( $other->unknownDimsTaint ); |
377 | } |
378 | foreach ( $other->dimTaint as $k => $v ) { |
379 | $ret->dimTaint[$k] = isset( $ret->dimTaint[$k] ) |
380 | ? $ret->dimTaint[$k]->asMergedForAssignment( $v, $depth - 1 ) |
381 | : $v; |
382 | } |
383 | return $ret; |
384 | } |
385 | |
386 | /** |
387 | * Apply an array addition with $other |
388 | * |
389 | * @param Taintedness $other |
390 | */ |
391 | public function arrayPlus( self $other ): void { |
392 | $this->flags |= $other->flags; |
393 | if ( $other->unknownDimsTaint && !$this->unknownDimsTaint ) { |
394 | $this->unknownDimsTaint = $other->unknownDimsTaint; |
395 | } elseif ( $other->unknownDimsTaint ) { |
396 | $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint ); |
397 | } |
398 | $this->keysTaint |= $other->keysTaint; |
399 | // This is not recursive because array addition isn't |
400 | $this->dimTaint += $other->dimTaint; |
401 | } |
402 | |
403 | /** |
404 | * Apply the effect of array addition and return a clone of $this |
405 | * |
406 | * @param Taintedness $other |
407 | * @return $this |
408 | */ |
409 | public function asArrayPlusWith( self $other ): self { |
410 | $ret = clone $this; |
411 | $ret->arrayPlus( $other ); |
412 | return $ret; |
413 | } |
414 | |
415 | /** |
416 | * Get the taintedness for the given offset, if set. If $offset could not be resolved, this |
417 | * will return the whole object, with taint from unknown keys added. If the offset is not known, |
418 | * it will return a new Taintedness object without the original shape, and with taint from |
419 | * unknown keys added. |
420 | * |
421 | * @param Node|string|int|bool|float|null $offset |
422 | * @return self Always a copy |
423 | */ |
424 | public function getTaintednessForOffsetOrWhole( $offset ): self { |
425 | if ( !is_scalar( $offset ) ) { |
426 | return $this->asValueFirstLevel(); |
427 | } |
428 | if ( isset( $this->dimTaint[$offset] ) ) { |
429 | if ( $this->unknownDimsTaint ) { |
430 | $ret = $this->dimTaint[$offset]->asMergedWith( $this->unknownDimsTaint ); |
431 | } else { |
432 | $ret = $this->dimTaint[$offset]; |
433 | } |
434 | } elseif ( $this->unknownDimsTaint ) { |
435 | $ret = $this->unknownDimsTaint; |
436 | } else { |
437 | return new self( $this->flags ); |
438 | } |
439 | $ret->flags |= $this->flags; |
440 | return $ret; |
441 | } |
442 | |
443 | /** |
444 | * Create a new object with $this at the given $offset (if scalar) or as unknown object. |
445 | * |
446 | * @param Node|string|int|bool|float|null $offset |
447 | * @param int|null $offsetTaint If available, will be used as key taint |
448 | * @return self Always a copy |
449 | */ |
450 | public function asMaybeMovedAtOffset( $offset, int $offsetTaint = null ): self { |
451 | $ret = self::newSafe(); |
452 | if ( $offsetTaint !== null ) { |
453 | $ret->keysTaint = $offsetTaint; |
454 | } |
455 | if ( $offset instanceof Node || $offset === null ) { |
456 | $ret->unknownDimsTaint = clone $this; |
457 | } else { |
458 | $ret->dimTaint[$offset] = clone $this; |
459 | } |
460 | return $ret; |
461 | } |
462 | |
463 | /** |
464 | * Get a representation of this taint at the first depth level. For instance, this can be used in a foreach |
465 | * assignment for the value. Own taint and unknown keys taint are preserved, and then we merge in recursively |
466 | * all the current keys. |
467 | * |
468 | * @return $this |
469 | */ |
470 | public function asValueFirstLevel(): self { |
471 | $ret = new self( $this->flags & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
472 | if ( $this->unknownDimsTaint ) { |
473 | $ret->mergeWith( $this->unknownDimsTaint ); |
474 | } |
475 | foreach ( $this->dimTaint as $val ) { |
476 | $ret->mergeWith( $val ); |
477 | } |
478 | return $ret; |
479 | } |
480 | |
481 | /** |
482 | * Creates a copy of this object without the given key |
483 | * @param string|int|bool|float $key |
484 | * @return $this |
485 | */ |
486 | public function withoutKey( $key ): self { |
487 | $ret = clone $this; |
488 | unset( $ret->dimTaint[$key] ); |
489 | if ( |
490 | ( $ret->flags & SecurityCheckPlugin::SQL_NUMKEY_TAINT ) && |
491 | !$ret->has( SecurityCheckPlugin::SQL_TAINT ) |
492 | ) { |
493 | // Note that this adjustment is not guaranteed to happen immediately after the removal of the last |
494 | // integer key. For instance, in [ 0 => unsafe, 'foo' => unsafe ], if the element 0 is removed, |
495 | // this branch will not run because 'foo' still contributes sql taint. |
496 | $ret->flags &= ~SecurityCheckPlugin::SQL_NUMKEY_TAINT; |
497 | } |
498 | return $ret; |
499 | } |
500 | |
501 | /** |
502 | * Creates a copy of this object without known offsets, and without keysTaint |
503 | * @return $this |
504 | */ |
505 | public function withoutKeys(): self { |
506 | $ret = clone $this; |
507 | $ret->keysTaint = SecurityCheckPlugin::NO_TAINT; |
508 | if ( !$ret->dimTaint ) { |
509 | return $ret; |
510 | } |
511 | $ret->unknownDimsTaint ??= self::newSafe(); |
512 | foreach ( $ret->dimTaint as $dim => $taint ) { |
513 | $ret->unknownDimsTaint->mergeWith( $taint ); |
514 | unset( $ret->dimTaint[$dim] ); |
515 | } |
516 | return $ret; |
517 | } |
518 | |
519 | /** |
520 | * Get a representation of this taint to be used in a foreach assignment for the key |
521 | * |
522 | * @return $this |
523 | */ |
524 | public function asKeyForForeach(): self { |
525 | return new self( ( $this->keysTaint | $this->flags ) & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
526 | } |
527 | |
528 | /** |
529 | * Applies an array_replace operations to $this, in place. |
530 | * |
531 | * @param Taintedness $other |
532 | * @return void |
533 | */ |
534 | public function arrayReplace( self $other ): void { |
535 | $this->flags |= $other->flags; |
536 | $this->dimTaint = array_replace( $this->dimTaint, $other->dimTaint ); |
537 | if ( $other->unknownDimsTaint ) { |
538 | if ( $this->unknownDimsTaint ) { |
539 | $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint ); |
540 | } else { |
541 | $this->unknownDimsTaint = $other->unknownDimsTaint; |
542 | } |
543 | } |
544 | } |
545 | |
546 | /** |
547 | * Applies an array_merge operations to $this, in place. |
548 | * |
549 | * @param Taintedness $other |
550 | * @return void |
551 | */ |
552 | public function arrayMerge( self $other ): void { |
553 | // First merge the known elements |
554 | $this->dimTaint = array_merge( $this->dimTaint, $other->dimTaint ); |
555 | // Then merge general flags, key flags, and any unknown keys |
556 | $this->flags |= $other->flags; |
557 | $this->keysTaint |= $other->keysTaint; |
558 | $this->unknownDimsTaint ??= new self( SecurityCheckPlugin::NO_TAINT ); |
559 | if ( $other->unknownDimsTaint ) { |
560 | $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint ); |
561 | } |
562 | // Finally, move taintedness from int keys to unknown |
563 | foreach ( $this->dimTaint as $k => $val ) { |
564 | if ( is_int( $k ) ) { |
565 | $this->unknownDimsTaint->mergeWith( $val ); |
566 | unset( $this->dimTaint[$k] ); |
567 | } |
568 | } |
569 | } |
570 | |
571 | // Conversion/checks shortcuts |
572 | |
573 | /** |
574 | * Check whether this object has no taintedness. |
575 | * |
576 | * @return bool |
577 | */ |
578 | public function isSafe(): bool { |
579 | // Don't use get() for performance |
580 | if ( $this->flags !== SecurityCheckPlugin::NO_TAINT ) { |
581 | return false; |
582 | } |
583 | if ( $this->keysTaint !== SecurityCheckPlugin::NO_TAINT ) { |
584 | return false; |
585 | } |
586 | if ( $this->unknownDimsTaint && !$this->unknownDimsTaint->isSafe() ) { |
587 | return false; |
588 | } |
589 | foreach ( $this->dimTaint as $val ) { |
590 | if ( !$val->isSafe() ) { |
591 | return false; |
592 | } |
593 | } |
594 | return true; |
595 | } |
596 | |
597 | /** |
598 | * Convert exec to yes taint recursively. Special flags like UNKNOWN or INAPPLICABLE are discarded. |
599 | * Any YES flags are also discarded. Note that this returns a copy of the |
600 | * original object. The shape is preserved. |
601 | * |
602 | * @warning This function is nilpotent: f^2(x) = 0 |
603 | * |
604 | * @return self |
605 | */ |
606 | public function asExecToYesTaint(): self { |
607 | $ret = new self( ( $this->flags & SecurityCheckPlugin::ALL_EXEC_TAINT ) >> 1 ); |
608 | if ( $this->unknownDimsTaint ) { |
609 | $ret->unknownDimsTaint = $this->unknownDimsTaint->asExecToYesTaint(); |
610 | } |
611 | $ret->keysTaint = ( $this->keysTaint & SecurityCheckPlugin::ALL_EXEC_TAINT ) >> 1; |
612 | foreach ( $this->dimTaint as $k => $val ) { |
613 | $ret->dimTaint[$k] = $val->asExecToYesTaint(); |
614 | } |
615 | return $ret; |
616 | } |
617 | |
618 | /** |
619 | * Convert the yes taint bits to corresponding exec taint bits recursively. |
620 | * Any UNKNOWN_TAINT or INAPPLICABLE_TAINT is discarded. Note that this returns a copy of the |
621 | * original object. The shape is preserved. |
622 | * |
623 | * @warning This function is nilpotent: f^2(x) = 0 |
624 | * |
625 | * @return self |
626 | * @suppress PhanUnreferencedPublicMethod For consistency |
627 | */ |
628 | public function asYesToExecTaint(): self { |
629 | $ret = new self( ( $this->flags & SecurityCheckPlugin::ALL_TAINT ) << 1 ); |
630 | if ( $this->unknownDimsTaint ) { |
631 | $ret->unknownDimsTaint = $this->unknownDimsTaint->asYesToExecTaint(); |
632 | } |
633 | $ret->keysTaint = ( $this->keysTaint & SecurityCheckPlugin::ALL_TAINT ) << 1; |
634 | foreach ( $this->dimTaint as $k => $val ) { |
635 | $ret->dimTaint[$k] = $val->asYesToExecTaint(); |
636 | } |
637 | return $ret; |
638 | } |
639 | |
640 | /** |
641 | * @param ParamLinksOffsets $offsets |
642 | * @return self |
643 | */ |
644 | public function asMovedAtRelevantOffsetsForBackprop( ParamLinksOffsets $offsets ): self { |
645 | $offsetsFlags = $offsets->getFlags(); |
646 | $ret = $offsetsFlags ? |
647 | $this->withOnly( ( $offsetsFlags & SecurityCheckPlugin::ALL_TAINT ) << 1 ) |
648 | : new self( SecurityCheckPlugin::NO_TAINT ); |
649 | foreach ( $offsets->getDims() as $k => $val ) { |
650 | $newVal = $this->asMovedAtRelevantOffsetsForBackprop( $val ); |
651 | if ( isset( $ret->dimTaint[$k] ) ) { |
652 | $ret->dimTaint[$k]->mergeWith( $newVal ); |
653 | } else { |
654 | $ret->dimTaint[$k] = $newVal; |
655 | } |
656 | } |
657 | $unknownOffs = $offsets->getUnknown(); |
658 | if ( $unknownOffs ) { |
659 | $newVal = $this->asMovedAtRelevantOffsetsForBackprop( $unknownOffs ); |
660 | if ( $ret->unknownDimsTaint ) { |
661 | $ret->unknownDimsTaint->mergeWith( $newVal ); |
662 | } else { |
663 | $ret->unknownDimsTaint = $newVal; |
664 | } |
665 | } |
666 | $ret->keysTaint |= ( $this->flags | $this->keysTaint ) & |
667 | ( ( $offsets->getKeysFlags() & SecurityCheckPlugin::ALL_TAINT ) << 1 ); |
668 | return $ret; |
669 | } |
670 | |
671 | /** |
672 | * Utility method to convert some flags from EXEC to YES. Note that this is not used internally |
673 | * to avoid the unnecessary overhead of a function call in hot code. |
674 | * |
675 | * @param int $flags |
676 | * @return int |
677 | */ |
678 | public static function flagsAsExecToYesTaint( int $flags ): int { |
679 | return ( $flags & SecurityCheckPlugin::ALL_EXEC_TAINT ) >> 1; |
680 | } |
681 | |
682 | /** |
683 | * Utility method to convert some flags from YES to EXEC. Note that this is not used internally |
684 | * to avoid the unnecessary overhead of a function call in hot code. |
685 | * |
686 | * @param int $flags |
687 | * @return int |
688 | */ |
689 | public static function flagsAsYesToExecTaint( int $flags ): int { |
690 | return ( $flags & SecurityCheckPlugin::ALL_TAINT ) << 1; |
691 | } |
692 | |
693 | /** |
694 | * @todo This method shouldn't be necessary (ideally) |
695 | * @return PreservedTaintedness |
696 | */ |
697 | public function asPreservedTaintedness(): PreservedTaintedness { |
698 | $ret = new PreservedTaintedness( new ParamLinksOffsets( $this->flags ) ); |
699 | foreach ( $this->dimTaint as $k => $val ) { |
700 | $ret->setOffsetTaintedness( $k, $val->asPreservedTaintedness() ); |
701 | } |
702 | if ( $this->unknownDimsTaint ) { |
703 | $ret->setOffsetTaintedness( null, $this->unknownDimsTaint->asPreservedTaintedness() ); |
704 | } |
705 | return $ret; |
706 | } |
707 | |
708 | /** |
709 | * Given some method links, returns a list of pairs of LinksSet and Taintedness objects, where the taintedness |
710 | * in each pair should be backpropagated to the links in the LinksSet. |
711 | * |
712 | * @param MethodLinks $links |
713 | * @return array<array<LinksSet|Taintedness>> |
714 | * @phan-return array<array{0:LinksSet,1:Taintedness}> |
715 | */ |
716 | public function decomposeForLinks( MethodLinks $links ): array { |
717 | $pairs = []; |
718 | |
719 | if ( $this->flags !== SecurityCheckPlugin::NO_TAINT ) { |
720 | $pairs[] = [ $links->getLinksCollapsing(), new self( $this->flags ) ]; |
721 | } |
722 | |
723 | if ( $this->keysTaint !== SecurityCheckPlugin::NO_TAINT ) { |
724 | $pairs[] = [ $links->asKeyForForeach()->getLinksCollapsing(), $this->asKeyForForeach() ]; |
725 | } |
726 | |
727 | foreach ( $this->dimTaint as $k => $dimTaint ) { |
728 | $pairs = array_merge( |
729 | $pairs, |
730 | $dimTaint->decomposeForLinks( $links->getForDim( $k ) ) |
731 | ); |
732 | } |
733 | |
734 | if ( $this->unknownDimsTaint ) { |
735 | $pairs = array_merge( |
736 | $pairs, |
737 | $this->unknownDimsTaint->decomposeForLinks( $links->getForDim( null ) ) |
738 | ); |
739 | } |
740 | return $pairs; |
741 | } |
742 | |
743 | /** |
744 | * Get a stringified representation of this taintedness, useful for debugging etc. |
745 | * |
746 | * @param string $indent |
747 | * @return string |
748 | * @suppress PhanUnreferencedPublicMethod |
749 | */ |
750 | public function toString( $indent = '' ): string { |
751 | $flags = SecurityCheckPlugin::taintToString( $this->flags ); |
752 | $keys = SecurityCheckPlugin::taintToString( $this->keysTaint ); |
753 | $ret = <<<EOT |
754 | { |
755 | $indent Own taint: $flags |
756 | $indent Keys: $keys |
757 | $indent Elements: { |
758 | EOT; |
759 | |
760 | $kIndent = "$indent "; |
761 | $first = "\n"; |
762 | $last = ''; |
763 | foreach ( $this->dimTaint as $key => $taint ) { |
764 | $ret .= "$first$kIndent $key => " . $taint->toString( "$kIndent " ) . "\n"; |
765 | $first = ''; |
766 | $last = $kIndent; |
767 | } |
768 | if ( $this->unknownDimsTaint ) { |
769 | $ret .= "$first$kIndent UNKNOWN => " . $this->unknownDimsTaint->toString( "$kIndent " ) . "\n"; |
770 | $last = $kIndent; |
771 | } |
772 | $ret .= "$last}\n$indent}"; |
773 | return $ret; |
774 | } |
775 | |
776 | /** |
777 | * Get a stringified representation of this taintedness suitable for the debug annotation |
778 | * |
779 | * @return string |
780 | */ |
781 | public function toShortString(): string { |
782 | $flags = SecurityCheckPlugin::taintToString( $this->flags ); |
783 | $ret = "{Own: $flags"; |
784 | if ( $this->keysTaint ) { |
785 | $ret .= '; Keys: ' . SecurityCheckPlugin::taintToString( $this->keysTaint ); |
786 | } |
787 | $keyParts = []; |
788 | if ( $this->dimTaint ) { |
789 | foreach ( $this->dimTaint as $key => $taint ) { |
790 | $keyParts[] = "$key => " . $taint->toShortString(); |
791 | } |
792 | } |
793 | if ( $this->unknownDimsTaint ) { |
794 | $keyParts[] = 'UNKNOWN => ' . $this->unknownDimsTaint->toShortString(); |
795 | } |
796 | if ( $keyParts ) { |
797 | $ret .= '; Elements: {' . implode( '; ', $keyParts ) . '}'; |
798 | } |
799 | $ret .= '}'; |
800 | return $ret; |
801 | } |
802 | |
803 | /** |
804 | * Make sure to clone member variables, too. |
805 | */ |
806 | public function __clone() { |
807 | if ( $this->unknownDimsTaint ) { |
808 | $this->unknownDimsTaint = clone $this->unknownDimsTaint; |
809 | } |
810 | foreach ( $this->dimTaint as $k => $v ) { |
811 | $this->dimTaint[$k] = clone $v; |
812 | } |
813 | } |
814 | |
815 | /** |
816 | * @return string |
817 | */ |
818 | public function __toString(): string { |
819 | return $this->toShortString(); |
820 | } |
821 | } |