Skip to content

Commit 1e151a6

Browse files
[Cache] Handle unserialize() failures gracefully
1 parent d13c424 commit 1e151a6

File tree

8 files changed

+163
-30
lines changed

8 files changed

+163
-30
lines changed

src/Symfony/Component/Cache/Adapter/AbstractAdapter.php

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,33 @@ public static function createSystemCache($namespace, $defaultLifetime, $version,
8686
return new ChainAdapter(array($apcu, $fs));
8787
}
8888

89+
/**
90+
* Like the native unserialize() function but throws an exception if anything goes wrong.
91+
*
92+
* @param string $value
93+
*
94+
* @return mixed
95+
*
96+
* @throws \Exception
97+
*/
98+
public static function unserialize($value)
99+
{
100+
if ('b:0;' === $value) {
101+
return false;
102+
}
103+
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
104+
try {
105+
if (false !== $value = unserialize($value)) {
106+
return $value;
107+
}
108+
throw new \DomainException('Failed to unserialize cached value');
109+
} catch (\Error $e) {
110+
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
111+
} finally {
112+
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
113+
}
114+
}
115+
89116
/**
90117
* Fetches several cache items.
91118
*
@@ -361,13 +388,26 @@ private function generateItems($items, &$keys)
361388
{
362389
$f = $this->createCacheItem;
363390

364-
foreach ($items as $id => $value) {
365-
yield $keys[$id] => $f($keys[$id], $value, true);
366-
unset($keys[$id]);
391+
try {
392+
foreach ($items as $id => $value) {
393+
$key = $keys[$id];
394+
unset($keys[$id]);
395+
yield $key => $f($key, $value, true);
396+
}
397+
} catch (\Exception $e) {
398+
CacheItem::log($this->logger, 'Failed to fetch requested items', array('keys' => array_values($keys), 'exception' => $e));
367399
}
368400

369401
foreach ($keys as $key) {
370402
yield $key => $f($key, null, false);
371403
}
372404
}
405+
406+
/**
407+
* @internal
408+
*/
409+
public static function handleUnserializeCallback($class)
410+
{
411+
throw new \DomainException('Class not found: '.$class);
412+
}
373413
}

src/Symfony/Component/Cache/Adapter/ApcuAdapter.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,16 @@ public function __construct($namespace = '', $defaultLifetime = 0, $version = nu
4949
*/
5050
protected function doFetch(array $ids)
5151
{
52-
return apcu_fetch($ids);
52+
$unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback');
53+
try {
54+
$values = apcu_fetch($ids);
55+
} catch (\Error $e) {
56+
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
57+
} finally {
58+
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
59+
}
60+
61+
return $values;
5362
}
5463

5564
/**

src/Symfony/Component/Cache/Adapter/ArrayAdapter.php

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ public function getItem($key)
5858
if (!$isHit = $this->hasItem($key)) {
5959
$value = null;
6060
} elseif ($this->storeSerialized) {
61-
$value = unserialize($this->values[$key]);
61+
try {
62+
$value = AbstractAdapter::unserialize($this->values[$key]);
63+
} catch (\Exception $e) {
64+
CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e));
65+
$value = null;
66+
$isHit = false;
67+
}
6268
} else {
6369
$value = $this->values[$key];
6470
}
@@ -76,7 +82,50 @@ public function getItems(array $keys = array())
7682
CacheItem::validateKey($key);
7783
}
7884

79-
return $this->generateItems($keys, time());
85+
$e = null;
86+
$now = time();
87+
$values = array();
88+
$f = $this->createCacheItem;
89+
90+
if ($this->storeSerialized) {
91+
$unserializeCallbackHandler = ini_set('unserialize_callback_func', AbstractAdapter::class.'::handleUnserializeCallback');
92+
}
93+
94+
try {
95+
foreach ($keys as $key) {
96+
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) {
97+
$value = null;
98+
} elseif ($this->storeSerialized) {
99+
if ('b:0;' === $value = $this->values[$key]) {
100+
$value = false;
101+
} elseif (false === $value = unserialize($value)) {
102+
$value = null;
103+
$isHit = false;
104+
}
105+
} else {
106+
$value = $this->values[$key];
107+
}
108+
109+
$values[$key] = $f($key, $value, $isHit);
110+
}
111+
} catch (\Throwable $e) {
112+
} catch (\Exception $e) {
113+
}
114+
115+
if ($this->storeSerialized) {
116+
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
117+
}
118+
if (null !== $e) {
119+
CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e));
120+
121+
foreach ($keys as $key) {
122+
if (!isset($values[$key])) {
123+
$values[$key] = $f($key, null, false);
124+
}
125+
}
126+
}
127+
128+
return $values;
80129
}
81130

82131
/**
@@ -176,21 +225,4 @@ public function commit()
176225
{
177226
return true;
178227
}
179-
180-
private function generateItems(array $keys, $now)
181-
{
182-
$f = $this->createCacheItem;
183-
184-
foreach ($keys as $key) {
185-
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) {
186-
$value = null;
187-
} elseif ($this->storeSerialized) {
188-
$value = unserialize($this->values[$key]);
189-
} else {
190-
$value = $this->values[$key];
191-
}
192-
193-
yield $key => $f($key, $value, $isHit);
194-
}
195-
}
196228
}

src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,24 @@ public function __construct(CacheProvider $provider, $namespace = '', $defaultLi
3232
*/
3333
protected function doFetch(array $ids)
3434
{
35-
return $this->provider->fetchMultiple($ids);
35+
$unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback');
36+
try {
37+
return $this->provider->fetchMultiple($ids);
38+
} catch (\Error $e) {
39+
$trace = $e->getTrace();
40+
41+
if (isset($trace[0]['function']) && !isset($trace[0]['class'])) {
42+
switch ($trace[0]['function']) {
43+
case 'unserialize':
44+
case 'apcu_fetch':
45+
case 'apc_fetch':
46+
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
47+
}
48+
}
49+
throw $e;
50+
} finally {
51+
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
52+
}
3653
}
3754

3855
/**

src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ protected function doFetch(array $ids)
7373
$value = stream_get_contents($h);
7474
fclose($h);
7575
if ($i === $id) {
76-
$values[$id] = unserialize($value);
76+
$values[$id] = parent::unserialize($value);
7777
}
7878
}
7979
}

src/Symfony/Component/Cache/Adapter/RedisAdapter.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,19 +134,15 @@ public static function createConnection($dsn, array $options = array())
134134
*/
135135
protected function doFetch(array $ids)
136136
{
137-
$result = array();
138-
139137
if ($ids) {
140138
$values = $this->redis->mGet($ids);
141139
$index = 0;
142140
foreach ($ids as $id) {
143141
if ($value = $values[$index++]) {
144-
$result[$id] = unserialize($value);
142+
yield $id => parent::unserialize($value);
145143
}
146144
}
147145
}
148-
149-
return $result;
150146
}
151147

152148
/**

src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,42 @@ public function testDefaultLifeTime()
4646
$item = $cache->getItem('key.dlt');
4747
$this->assertFalse($item->isHit());
4848
}
49+
50+
public function testNotUnserializable()
51+
{
52+
if (isset($this->skippedTests[__FUNCTION__])) {
53+
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
54+
55+
return;
56+
}
57+
58+
$cache = $this->createCachePool();
59+
60+
$item = $cache->getItem('foo');
61+
$cache->save($item->set(new NotUnserializable()));
62+
63+
$item = $cache->getItem('foo');
64+
$this->assertFalse($item->isHit());
65+
66+
foreach ($cache->getItems(array('foo')) as $item) {
67+
}
68+
$cache->save($item->set(new NotUnserializable()));
69+
70+
foreach ($cache->getItems(array('foo')) as $item) {
71+
}
72+
$this->assertFalse($item->isHit());
73+
}
74+
}
75+
76+
class NotUnserializable implements \Serializable
77+
{
78+
public function serialize()
79+
{
80+
return serialize(123);
81+
}
82+
83+
public function unserialize($ser)
84+
{
85+
throw new \Exception(__CLASS__);
86+
}
4987
}

src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class DoctrineAdapterTest extends AdapterTestCase
2222
protected $skippedTests = array(
2323
'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayCache is not.',
2424
'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayCache is not.',
25+
'testNotUnserializable' => 'ArrayCache does not use serialize/unserialize',
2526
);
2627

2728
public function createCachePool($defaultLifetime = 0)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy