8000 [Cache] Handle unserialize() failures gracefully · symfony/symfony@2915a08 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2915a08

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

File tree

8 files changed

+163
-30
lines changed
  • Tests/Adapter
  • 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+
    function serialize()
    79+
    {
    80+
    return serialize(123);
    81+
    }
    82+
    83+
    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)
    0