Skip to content

Commit 2e0e020

Browse files
authored
Merge pull request #4637 from MGatner/cache-hash
Cache Key Validation
2 parents 2b5e848 + cf52dda commit 2e0e020

File tree

10 files changed

+233
-75
lines changed

10 files changed

+233
-75
lines changed

system/Cache/Handlers/BaseHandler.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,63 @@
1414
use Closure;
1515
use CodeIgniter\Cache\CacheInterface;
1616
use Exception;
17+
use InvalidArgumentException;
1718

1819
/**
1920
* Base class for cache handling
2021
*/
2122
abstract class BaseHandler implements CacheInterface
2223
{
24+
/**
25+
* Reserved characters that cannot be used in a key or tag.
26+
* From https://github.com/symfony/cache-contracts/blob/c0446463729b89dd4fa62e9aeecc80287323615d/ItemInterface.php#L43
27+
*/
28+
public const RESERVED_CHARACTERS = '{}()/\@:';
29+
30+
/**
31+
* Maximum key length.
32+
*/
33+
public const MAX_KEY_LENGTH = PHP_INT_MAX;
34+
35+
/**
36+
* Prefix to apply to cache keys.
37+
* May not be used by all handlers.
38+
*
39+
* @var string
40+
*/
41+
protected $prefix;
42+
43+
/**
44+
* Validates a cache key according to PSR-6.
45+
* Keys that exceed MAX_KEY_LENGTH are hashed.
46+
* From https://github.com/symfony/cache/blob/7b024c6726af21fd4984ac8d1eae2b9f3d90de88/CacheItem.php#L158
47+
*
48+
* @param string $key The key to validate
49+
* @param string $prefix Optional prefix to include in length calculations
50+
*
51+
* @throws InvalidArgumentException When $key is not valid
52+
*/
53+
public static function validateKey($key, $prefix = ''): string
54+
{
55+
if (! is_string($key))
56+
{
57+
throw new InvalidArgumentException('Cache key must be a string');
58+
}
59+
if ($key === '')
60+
{
61+
throw new InvalidArgumentException('Cache key cannot be empty.');
62+
}
63+
if (strpbrk($key, self::RESERVED_CHARACTERS) !== false)
64+
{
65+
throw new InvalidArgumentException('Cache key contains reserved characters ' . self::RESERVED_CHARACTERS);
66+
}
67+
68+
// If the key with prefix exceeds the length then return the hashed version
69+
return strlen($prefix . $key) > static::MAX_KEY_LENGTH ? $prefix . md5($key) : $prefix . $key;
70+
}
71+
72+
//--------------------------------------------------------------------
73+
2374
/**
2475
* Get an item from the cache, or execute the given Closure and store the result.
2576
*

system/Cache/Handlers/FileHandler.php

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@
2121
class FileHandler extends BaseHandler
2222
{
2323
/**
24-
* Prefixed to all cache names.
25-
*
26-
* @var string
24+
* Maximum key length.
2725
*/
28-
protected $prefix;
26+
public const MAX_KEY_LENGTH = PHP_MAXPATHLEN;
2927

3028
/**
3129
* Where to store cached files on the disk.
@@ -94,8 +92,7 @@ public function initialize()
9492
*/
9593
public function get(string $key)
9694
{
97-
$key = $this->prefix . $key;
98-
95+
$key = static::validateKey($key, $this->prefix);
9996
$data = $this->getItem($key);
10097

10198
return is_array($data) ? $data['data'] : null;
@@ -114,7 +111,7 @@ public function get(string $key)
114111
*/
115112
public function save(string $key, $value, int $ttl = 60)
116113
{
117-
$key = $this->prefix . $key;
114+
$key = static::validateKey($key, $this->prefix);
118115

119116
$contents = [
120117
'time' => time(),
@@ -152,7 +149,7 @@ public function save(string $key, $value, int $ttl = 60)
152149
*/
153150
public function delete(string $key)
154151
{
155-
$key = $this->prefix . $key;
152+
$key = static::validateKey($key, $this->prefix);
156153

157154
return is_file($this->path . $key) && unlink($this->path . $key);
158155
}
@@ -170,7 +167,7 @@ public function deleteMatching(string $pattern)
170167
{
171168
$deleted = 0;
172169

173-
foreach (glob($this->path . $pattern, GLOB_NOSORT) as $filename)
170+
foreach (glob($this->path . $pattern, GLOB_NOSORT) as $filename)
174171
{
175172
if (is_file($filename) && @unlink($filename))
176173
{
@@ -193,8 +190,7 @@ public function deleteMatching(string $pattern)
193190
*/
194191
public function increment(string $key, int $offset = 1)
195192
{
196-
$key = $this->prefix . $key;
197-
193+
$key = static::validateKey($key, $this->prefix);
198194
$data = $this->getItem($key);
199195

200196
if ($data === false)
@@ -222,12 +218,11 @@ public function increment(string $key, int $offset = 1)
222218
* @param string $key Cache ID
223219
* @param integer $offset Step/value to increase by
224220
*
225-
* @return bool
221+
* @return boolean
226222
*/
227223
public function decrement(string $key, int $offset = 1)
228224
{
229-
$key = $this->prefix . $key;
230-
225+
$key = static::validateKey($key, $this->prefix);
231226
$data = $this->getItem($key);
232227

233228
if ($data === false)
@@ -288,7 +283,7 @@ public function getCacheInfo()
288283
*/
289284
public function getMetaData(string $key)
290285
{
291-
$key = $this->prefix . $key;
286+
$key = static::validateKey($key, $this->prefix);
292287

293288
if (! is_file($this->path . $key))
294289
{
@@ -342,26 +337,26 @@ public function isSupported(): bool
342337
* Does the heavy lifting of actually retrieving the file and
343338
* verifying it's age.
344339
*
345-
* @param string $key
340+
* @param string $filename
346341
*
347342
* @return boolean|mixed
348343
*/
349-
protected function getItem(string $key)
344+
protected function getItem(string $filename)
350345
{
351-
if (! is_file($this->path . $key))
346+
if (! is_file($this->path . $filename))
352347
{
353348
return false;
354349
}
355350

356-
$data = unserialize(file_get_contents($this->path . $key));
351+
$data = unserialize(file_get_contents($this->path . $filename));
357352

358353
// @phpstan-ignore-next-line
359354
if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl'])
360355
{
361356
// If the file is still there then remove it
362-
if (is_file($this->path . $key))
357+
if (is_file($this->path . $filename))
363358
{
364-
unlink($this->path . $key);
359+
unlink($this->path . $filename);
365360
}
366361

367362
return false;

system/Cache/Handlers/MemcachedHandler.php

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,6 @@
2222
*/
2323
class MemcachedHandler extends BaseHandler
2424
{
25-
/**
26-
* Prefixed to all cache names.
27-
*
28-
* @var string
29-
*/
30-
protected $prefix;
31-
3225
/**
3326
* The memcached object
3427
*
@@ -166,7 +159,7 @@ public function initialize()
166159
*/
167160
public function get(string $key)
168161
{
169-
$key = $this->prefix . $key;
162+
$key = static::validateKey($key, $this->prefix);
170163

171164
if ($this->memcached instanceof Memcached)
172165
{
@@ -206,7 +199,7 @@ public function get(string $key)
206199
*/
207200
public function save(string $key, $value, int $ttl = 60)
208201
{
209-
$key = $this->prefix . $key;
202+
$key = static::validateKey($key, $this->prefix);
210203

211204
if (! $this->config['raw'])
212205
{
@@ -242,7 +235,7 @@ public function save(string $key, $value, int $ttl = 60)
242235
*/
243236
public function delete(string $key)
244237
{
245-
$key = $this->prefix . $key;
238+
$key = static::validateKey($key, $this->prefix);
246239

247240
return $this->memcached->delete($key);
248241
}
@@ -278,7 +271,7 @@ public function increment(string $key, int $offset = 1)
278271
return false;
279272
}
280273

281-
$key = $this->prefix . $key;
274+
$key = static::validateKey($key, $this->prefix);
282275

283276
// @phpstan-ignore-next-line
284277
return $this->memcached->increment($key, $offset, $offset, 60);
@@ -301,7 +294,7 @@ public function decrement(string $key, int $offset = 1)
301294
return false;
302295
}
303296

304-
$key = $this->prefix . $key;
297+
$key = static::validateKey($key, $this->prefix);
305298

306299
//FIXME: third parameter isn't other handler actions.
307300
// @phpstan-ignore-next-line
@@ -349,8 +342,7 @@ public function getCacheInfo()
349342
*/
350343
public function getMetaData(string $key)
351344
{
352-
$key = $this->prefix . $key;
353-
345+
$key = static::validateKey($key, $this->prefix);
354346
$stored = $this->memcached->get($key);
355347

356348
// if not an array, don't try to count for PHP7.2

system/Cache/Handlers/PredisHandler.php

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,6 @@
2222
*/
2323
class PredisHandler extends BaseHandler
2424
{
25-
/**
26-
* Prefixed to all cache names.
27-
*
28-
* @var string
29-
*/
30-
protected $prefix;
31-
3225
/**
3326
* Default config
3427
*
@@ -101,8 +94,12 @@ public function initialize()
10194
*/
10295
public function get(string $key)
10396
{
104-
$data = array_combine(
105-
['__ci_type', '__ci_value'],
97+
$key = static::validateKey($key);
98+
99+
$data = array_combine([
100+
'__ci_type',
101+
'__ci_value',
102+
],
106103
$this->redis->hmget($key, ['__ci_type', '__ci_value'])
107104
);
108105

@@ -141,6 +138,8 @@ public function get(string $key)
141138
*/
142139
public function save(string $key, $value, int $ttl = 60)
143140
{
141+
$key = static::validateKey($key);
142+
144143
switch ($dataType = gettype($value))
145144
{
146145
case 'array':
@@ -179,6 +178,8 @@ public function save(string $key, $value, int $ttl = 60)
179178
*/
180179
public function delete(string $key)
181180
{
181+
$key = static::validateKey($key);
182+
182183
return $this->redis->del($key) === 1;
183184
}
184185

@@ -215,6 +216,8 @@ public function deleteMatching(string $pattern)
215216
*/
216217
public function increment(string $key, int $offset = 1)
217218
{
219+
$key = static::validateKey($key);
220+
218221
return $this->redis->hincrby($key, 'data', $offset);
219222
}
220223

@@ -230,6 +233,8 @@ public function increment(string $key, int $offset = 1)
230233
*/
231234
public function decrement(string $key, int $offset = 1)
232235
{
236+
$key = static::validateKey($key);
237+
233238
return $this->redis->hincrby($key, 'data', -$offset);
234239
}
235240

@@ -273,6 +278,8 @@ public function getCacheInfo()
273278
*/
274279
public function getMetaData(string $key)
275280
{
281+
$key = static::validateKey($key);
282+
276283
$data = array_combine(['__ci_value'], $this->redis->hmget($key, ['__ci_value']));
277284

278285
if (isset($data['__ci_value']) && $data['__ci_value'] !== false)

0 commit comments

Comments
 (0)