This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | declare( strict_types = 1 ); |
||
3 | |||
4 | namespace Wikibase\Lib; |
||
5 | |||
6 | use BagOStuff; |
||
7 | use DateInterval; |
||
8 | use DateTime; |
||
9 | use Psr\Log\LoggerAwareTrait; |
||
10 | use Psr\Log\NullLogger; |
||
11 | use Psr\SimpleCache\CacheInterface; |
||
12 | use Traversable; |
||
13 | |||
14 | /** |
||
15 | * @license GPL-2.0-or-later |
||
16 | */ |
||
17 | class SimpleCacheWithBagOStuff implements CacheInterface { |
||
18 | |||
19 | use LoggerAwareTrait; |
||
20 | |||
21 | private const INVALID_KEY_REGEX = '/[\{\}\(\)\/\\\\@:]/'; |
||
22 | |||
23 | /** |
||
24 | * @var BagOStuff |
||
25 | */ |
||
26 | private $inner; |
||
27 | |||
28 | /** |
||
29 | * @var string |
||
30 | */ |
||
31 | private $prefix; |
||
32 | |||
33 | /** |
||
34 | * @var string |
||
35 | */ |
||
36 | private $secret; |
||
37 | |||
38 | /** |
||
39 | * @param BagOStuff $inner |
||
40 | * @param string $prefix While setting and getting all keys will be prefixed with this string |
||
41 | * @param string $secret Will be used to create a signature for stored values |
||
42 | * |
||
43 | * @throws \InvalidArgumentException If prefix has wrong format or secret is not a string or empty |
||
44 | */ |
||
45 | public function __construct( BagOStuff $inner, string $prefix, string $secret ) { |
||
46 | $this->assertKeyIsValid( $prefix ); |
||
47 | |||
48 | if ( empty( $secret ) ) { |
||
49 | throw new \InvalidArgumentException( "Secret is required to be a nonempty string" ); |
||
50 | } |
||
51 | |||
52 | $this->inner = $inner; |
||
53 | $this->prefix = $prefix; |
||
54 | $this->secret = $secret; |
||
55 | $this->logger = new NullLogger(); |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * Fetches a value from the cache. |
||
60 | * |
||
61 | * @param string $key The unique key of this item in the cache. |
||
62 | * @param mixed $default Default value to return if the key does not exist. |
||
63 | * |
||
64 | * @return mixed The value of the item from the cache, or $default in case of cache miss. |
||
65 | * |
||
66 | * @throws CacheInvalidArgumentException |
||
67 | * MUST be thrown if the $key string is not a legal value. |
||
68 | */ |
||
69 | public function get( $key, $default = null ) { |
||
70 | $this->assertKeyIsValid( $key ); |
||
71 | $key = $this->inner->makeKey( $this->prefix, $key ); |
||
72 | |||
73 | $result = $this->inner->get( $key ); |
||
74 | if ( $result === false ) { |
||
75 | return $default; |
||
76 | } |
||
77 | |||
78 | return $this->unserialize( $result, $default, [ 'key' => $key, 'prefix' => $this->prefix ] ); |
||
79 | } |
||
80 | |||
81 | /** |
||
82 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. |
||
83 | * |
||
84 | * @param string $key The key of the item to store. |
||
85 | * @param mixed $value The value of the item to store, must be serializable. |
||
86 | * @param null|int|DateInterval $ttl Optional. The TTL value of this item. If no value is sent and |
||
87 | * the driver supports TTL then the library may set a default value |
||
88 | * for it or let the driver take care of that. |
||
89 | * |
||
90 | * @return bool True on success and false on failure. |
||
91 | * |
||
92 | * @throws CacheInvalidArgumentException |
||
93 | * MUST be thrown if the $key string is not a legal value. |
||
94 | */ |
||
95 | public function set( $key, $value, $ttl = null ) { |
||
96 | $this->assertKeyIsValid( $key ); |
||
97 | $key = $this->inner->makeKey( $this->prefix, $key ); |
||
98 | $ttl = $this->normalizeTtl( $ttl ); |
||
99 | |||
100 | $value = $this->serialize( $value ); |
||
101 | |||
102 | return $this->inner->set( $key, $value, $ttl ); |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * Delete an item from the cache by its unique key. |
||
107 | * |
||
108 | * @param string $key The unique cache key of the item to delete. |
||
109 | * |
||
110 | * @return bool True if the item was successfully removed. False if there was an error. |
||
111 | * |
||
112 | * @throws CacheInvalidArgumentException |
||
113 | * MUST be thrown if the $key string is not a legal value. |
||
114 | */ |
||
115 | public function delete( $key ) { |
||
116 | $this->assertKeyIsValid( $key ); |
||
117 | $key = $this->inner->makeKey( $this->prefix, $key ); |
||
118 | |||
119 | return $this->inner->delete( $key ); |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * Wipes clean the entire cache's keys. |
||
124 | * |
||
125 | * @return bool True on success and false on failure. |
||
126 | */ |
||
127 | public function clear() { |
||
128 | //Cannot be implemented |
||
129 | return false; |
||
130 | } |
||
131 | |||
132 | /** |
||
133 | * Obtains multiple cache items by their unique keys. |
||
134 | * |
||
135 | * @param iterable $keys A list of keys that can obtained in a single operation. |
||
136 | * @param mixed $default Default value to return for keys that do not exist. |
||
137 | * |
||
138 | * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value. |
||
139 | * |
||
140 | * @throws CacheInvalidArgumentException |
||
141 | * MUST be thrown if $keys is neither an array nor a Traversable, |
||
142 | * or if any of the $keys are not a legal value. |
||
143 | */ |
||
144 | public function getMultiple( $keys, $default = null ) { |
||
145 | $keys = $this->toArray( $keys ); |
||
146 | $this->assertKeysAreValid( $keys ); |
||
147 | $prefixedKeys = array_map( |
||
148 | function ( $k ) { |
||
149 | return $this->inner->makeKey( $this->prefix, $k ); |
||
150 | }, |
||
151 | $keys |
||
152 | ); |
||
153 | |||
154 | $innerResult = $this->inner->getMulti( $prefixedKeys ); |
||
155 | $result = []; |
||
156 | foreach ( $keys as $key ) { |
||
157 | $prefixedKey = $this->inner->makeKey( $this->prefix, $key ); |
||
158 | if ( !array_key_exists( $prefixedKey, $innerResult ) ) { |
||
159 | $result[ $key ] = $default; |
||
160 | } else { |
||
161 | $result[ $key ] = $this->unserialize( |
||
162 | $innerResult[ $prefixedKey ], |
||
163 | $default, |
||
164 | [ 'key' => $key, 'prefix' => $this->prefix ] |
||
165 | ); |
||
166 | } |
||
167 | } |
||
168 | |||
169 | return $result; |
||
0 ignored issues
–
show
|
|||
170 | } |
||
171 | |||
172 | /** |
||
173 | * Persists a set of key => value pairs in the cache, with an optional TTL. |
||
174 | * |
||
175 | * @param iterable $values A list of key => value pairs for a multiple-set operation. |
||
176 | * @param null|int|DateInterval $ttl Optional. The TTL value of this item. If no value is sent and |
||
177 | * the driver supports TTL then the library may set a default value |
||
178 | * for it or let the driver take care of that. |
||
179 | * |
||
180 | * @return bool True on success and false on failure. |
||
181 | * |
||
182 | * @throws CacheInvalidArgumentException |
||
183 | * MUST be thrown if $values is neither an array nor a Traversable, |
||
184 | * or if any of the $values are not a legal value. |
||
185 | */ |
||
186 | public function setMultiple( $values, $ttl = null ) { |
||
187 | $values = $this->toAssociativeArray( $values ); |
||
188 | $this->assertKeysAreValidAllowIntegers( array_keys( $values ) ); |
||
189 | |||
190 | $ttl = $this->normalizeTtl( $ttl ); |
||
191 | |||
192 | foreach ( $values as $key => $value ) { |
||
193 | $key = $this->inner->makeKey( $this->prefix, $key ); |
||
194 | $values[ $key ] = $this->serialize( $value ); |
||
195 | } |
||
196 | |||
197 | return $this->inner->setMulti( $values, $ttl ?: 0 ); |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * Deletes multiple cache items in a single operation. |
||
202 | * |
||
203 | * @param iterable $keys A list of string-based keys to be deleted. |
||
204 | * |
||
205 | * @return bool True if the items were successfully removed. False if there was an error. |
||
206 | * |
||
207 | * @throws CacheInvalidArgumentException |
||
208 | * MUST be thrown if $keys is neither an array nor a Traversable, |
||
209 | * or if any of the $keys are not a legal value. |
||
210 | */ |
||
211 | public function deleteMultiple( $keys ) { |
||
212 | $keys = $this->toArray( $keys ); |
||
213 | $this->assertKeysAreValid( $keys ); |
||
214 | $result = true; |
||
215 | foreach ( $keys as $key ) { |
||
216 | $result &= $this->delete( $key ); |
||
217 | } |
||
218 | return (bool)$result; |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * Determines whether an item is present in the cache. |
||
223 | * |
||
224 | * NOTE: It is recommended that has() is only to be used for cache warming type purposes |
||
225 | * and not to be used within your live applications operations for get/set, as this method |
||
226 | * is subject to a race condition where your has() will return true and immediately after, |
||
227 | * another script can remove it making the state of your app out of date. |
||
228 | * |
||
229 | * @param string $key The cache item key. |
||
230 | * |
||
231 | * @return bool |
||
232 | * |
||
233 | * @throws CacheInvalidArgumentException |
||
234 | * MUST be thrown if the $key string is not a legal value. |
||
235 | */ |
||
236 | public function has( $key ) { |
||
237 | $this->assertKeyIsValid( $key ); |
||
238 | $key = $this->inner->makeKey( $this->prefix, $key ); |
||
239 | $result = $this->inner->get( $key ); |
||
240 | return $result !== false; |
||
241 | } |
||
242 | |||
243 | /* |
||
244 | * Due to the fact that in PHP array indices are automatically casted |
||
245 | * to integers if possible, e.g. `['0' => ''] === [0 => '']`, we have to |
||
246 | * allow integers to be present as keys in $values in `setMultiple()` |
||
247 | */ |
||
248 | private function assertKeysAreValidAllowIntegers( $keys ): void { |
||
249 | $this->assertKeysAreValid( |
||
250 | array_map( |
||
251 | function ( $key ) { |
||
252 | if ( is_int( $key ) ) { |
||
253 | return (string)$key; |
||
254 | } |
||
255 | return $key; |
||
256 | }, |
||
257 | $keys |
||
258 | ) |
||
259 | ); |
||
260 | } |
||
261 | |||
262 | private function assertKeysAreValid( $keys ): void { |
||
263 | foreach ( $keys as $key ) { |
||
264 | $this->assertKeyIsValid( $key ); |
||
265 | } |
||
266 | } |
||
267 | |||
268 | /** |
||
269 | * @throws CacheInvalidArgumentException |
||
270 | */ |
||
271 | private function assertKeyIsValid( $key ): void { |
||
272 | if ( !is_string( $key ) ) { |
||
273 | $type = gettype( $key ); |
||
274 | throw new CacheInvalidArgumentException( "Cache key should be string or integer, `{$type}` is given" ); |
||
275 | } |
||
276 | |||
277 | if ( $key === '' ) { |
||
278 | throw new CacheInvalidArgumentException( "Cache key cannot be an empty string" ); |
||
279 | } |
||
280 | |||
281 | if ( preg_match( self::INVALID_KEY_REGEX, $key ) ) { |
||
282 | throw new CacheInvalidArgumentException( "Cache key contains characters that are not allowed: `{$key}`" ); |
||
283 | } |
||
284 | } |
||
285 | |||
286 | /** |
||
287 | * @param mixed $var the object to turn to array |
||
288 | * @return array |
||
289 | * @throws CacheInvalidArgumentException |
||
290 | */ |
||
291 | private function toArray( $var ) { |
||
292 | if ( !$this->isIterable( $var ) ) { |
||
293 | $type = gettype( $var ); |
||
294 | throw new CacheInvalidArgumentException( "Expected iterable, `{$type}` given" ); |
||
295 | } |
||
296 | |||
297 | if ( $var instanceof Traversable ) { |
||
298 | $result = []; |
||
299 | foreach ( $var as $value ) { |
||
300 | $result[] = $value; |
||
301 | } |
||
302 | } else { |
||
303 | $result = $var; |
||
304 | } |
||
305 | |||
306 | return $result; |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | * @param mixed $var the object to turn to associative array |
||
311 | * @return array |
||
312 | * @throws CacheInvalidArgumentException |
||
313 | */ |
||
314 | private function toAssociativeArray( $var ) { |
||
315 | if ( !$this->isIterable( $var ) ) { |
||
316 | $type = gettype( $var ); |
||
317 | throw new CacheInvalidArgumentException( "Expected iterable, `{$type}` given" ); |
||
318 | } |
||
319 | |||
320 | if ( $var instanceof Traversable ) { |
||
321 | $result = []; |
||
322 | foreach ( $var as $key => $value ) { |
||
323 | if ( !( is_string( $key ) || is_int( $key ) ) ) { |
||
324 | $type = gettype( $key ); |
||
325 | throw new CacheInvalidArgumentException( "Cache key should be string or integer, `{$type}` is given" ); |
||
326 | } |
||
327 | $result[ $key ] = $value; |
||
328 | } |
||
329 | } else { |
||
330 | $result = $var; |
||
331 | } |
||
332 | |||
333 | return $result; |
||
334 | } |
||
335 | |||
336 | private function isIterable( $var ) { |
||
337 | return is_array( $var ) || ( is_object( $var ) && ( $var instanceof Traversable ) ); |
||
338 | } |
||
339 | |||
340 | /** |
||
341 | * @param null|int|DateInterval $ttl The TTL value of this item. If no value is sent and |
||
342 | * the driver supports TTL then the library may set a default value |
||
343 | * for it or let the driver take care of that. |
||
344 | * |
||
345 | * @return int UNIX timestamp when the item should expire or \BagOStuff::TTL_INDEFINITE |
||
346 | * @throws CacheInvalidArgumentException |
||
347 | */ |
||
348 | private function normalizeTtl( $ttl ) { |
||
349 | // Addition of `1` to timestamp is required to avoid the issue when we read timestamp in |
||
350 | // the very end of the pending second (lets say 57.999) so that effective TTL becomes |
||
351 | // very small (in former example it will be 0.001). This issue makes tests flaky. |
||
352 | // @see https://phabricator.wikimedia.org/T201453 |
||
353 | if ( $ttl instanceof DateInterval ) { |
||
354 | $date = new DateTime(); |
||
355 | $date->add( $ttl ); |
||
356 | return $date->getTimestamp() + 1; |
||
357 | } elseif ( $ttl === 0 ) { |
||
358 | return time(); |
||
359 | } elseif ( is_int( $ttl ) ) { |
||
360 | return $ttl + time() + 1; |
||
361 | } elseif ( $ttl === null ) { |
||
362 | return BagOStuff::TTL_INDEFINITE; |
||
363 | } else { |
||
364 | $type = gettype( $ttl ); |
||
365 | throw new CacheInvalidArgumentException( "Invalid TTL: `null|int|\DateInterval` expected, `$type` given" ); |
||
366 | } |
||
367 | } |
||
368 | |||
369 | private function serialize( $value ) { |
||
370 | $serializedValue = serialize( $value ); |
||
371 | $dataToStore = utf8_encode( $serializedValue ); |
||
372 | |||
373 | $signature = hash_hmac( 'sha256', $dataToStore, $this->secret ); |
||
374 | return json_encode( [ $signature, $dataToStore ] ); |
||
375 | } |
||
376 | |||
377 | /** |
||
378 | * @param string $string |
||
379 | * @param mixed $default |
||
380 | * @param array $loggingContext |
||
381 | * @return mixed |
||
382 | * @throws \Exception |
||
383 | * |
||
384 | * @note This implementation is so complicated because we cannot use PHP serialization due to |
||
385 | * the possibility of Object Injection attack. |
||
386 | * |
||
387 | * @see https://phabricator.wikimedia.org/T161647 |
||
388 | * @see https://secure.php.net/manual/en/function.unserialize.php |
||
389 | * @see https://www.owasp.org/index.php/PHP_Object_Injection |
||
390 | */ |
||
391 | private function unserialize( $string, $default, array $loggingContext ) { |
||
392 | $result = json_decode( $string ); |
||
393 | |||
394 | list( $signatureToCheck, $data ) = $result; |
||
395 | $correctSignature = hash_hmac( 'sha256', $data, $this->secret ); |
||
396 | $hashEquals = hash_equals( $correctSignature, $signatureToCheck ); |
||
397 | if ( !$hashEquals ) { |
||
398 | $this->logger->alert( "Incorrect signature", $loggingContext ); |
||
399 | |||
400 | return $default; |
||
401 | } |
||
402 | $decodedData = utf8_decode( $data ); |
||
403 | |||
404 | if ( $decodedData === serialize( false ) ) { |
||
405 | return false; |
||
406 | } |
||
407 | |||
408 | // phpcs:disable Generic.PHP.NoSilencedErrors.Discouraged |
||
409 | $value = @unserialize( |
||
410 | $decodedData, |
||
411 | [ |
||
412 | 'allowed_classes' => [ \stdClass::class ] |
||
413 | ] |
||
414 | ); |
||
415 | |||
416 | if ( $value === false ) { |
||
417 | $this->logger->alert( "Cannot deserialize stored value", $loggingContext ); |
||
418 | |||
419 | return $default; |
||
420 | } |
||
421 | |||
422 | return $value; |
||
423 | } |
||
424 | |||
425 | } |
||
426 |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.