Pimcore has Unsafe PHP Deserialization in Multiple Locations Without allowed_classes Restriction
Description
# GM-374
Summary
Multiple locations in Pimcore v11 call PHP's unserialize() on data from database columns and filesystem files without the allowed_classes restriction, enabling object injection if an attacker can control the serialized data source.
Affected
Component - Package: pimcore/pimcore and pimcore/admin-ui-classic-bundle - Files: - lib/Tool/Authentication.php (line 82) — session token deserialization - models/Site/Dao.php (line 68) — site domains from database - models/DataObject/ClassDefinition/CustomLayout/Dao.php (line 69) — layout definitions from database - models/Tool/TmpStore/Dao.php (line 64) — temporary store data from database - models/Asset/WebDAV/Service.php (line 36) — delete log from filesystem - admin-ui-classic-bundle/src/Helper/Dashboard.php (line 64) — dashboard config from filesystem
Description
Six locations in Pimcore core call unserialize() directly (bypassing Tool\Serialize) on data sourced from database columns or filesystem files without passing the allowed_classes parameter. This means any class available in the autoloader will be instantiated during deserialization.
If an attacker can write to the data source (e.g., via SQL injection targeting the tmp_store, sites, or custom_layouts tables, or via a file write vulnerability targeting the WebDAV delete log), they can inject serialized PHP gadget chains that execute arbitrary code when the data is deserialized.
This is related to but distinct from the Tool\Serialize::unserialize() issue — these calls bypass the wrapper entirely.
Impact
PHP object injection leading to Remote Code Execution when chained with a data source write vulnerability. Pimcore's dependency tree (Guzzle, Symfony, Monolog, Doctrine) provides numerous known gadget chains.
Proof of
Concept 1. Identify a writable data source (e.g., tmp_store table via SQL injection, or webdav-delete.dat via file write) 2. Write a serialized PHP gadget chain (e.g., Monolog BufferHandler chain from phpggc) 3. Trigger the deserialization (e.g., access a page that reads TmpStore, or trigger a WebDAV operation) 4. The gadget chain executes with web server privileges
Suggested
Fix Add allowed_classes parameter to all unserialize() calls. Where no objects are needed, use ['allowed_classes' => false]. Consider migrating to JSON serialization for data that doesn't require object preservation.
// Example fix for Site/Dao.php:
$siteDomains = unserialize($site['domains'], ['allowed_classes' => false]);
// Example fix for TmpStore/Dao.php:
$item['data'] = unserialize($item['data'], ['allowed_classes' => false]);
## Resources - CWE-502: Deserialization of Untrusted Data - OWASP Deserialization Cheat Sheet - phpggc: PHP Generic Gadget Chains
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Multiple locations in Pimcore v11 call PHP's unserialize() without allowed_classes restriction, enabling PHP object injection leading to RCE when an attacker can control the data source.
Vulnerability
Pimcore v11 (both pimcore/pimcore and pimcore/admin-ui-classic-bundle) contains six locations that call PHP's unserialize() directly on data sourced from database columns or filesystem files without passing the allowed_classes parameter. The affected files are: lib/Tool/Authentication.php (line 82, session token deserialization), models/Site/Dao.php (line 68, site domains), models/DataObject/ClassDefinition/CustomLayout/Dao.php (line 69, layout definitions), models/Tool/TmpStore/Dao.php (line 64, temporary store data), models/Asset/WebDAV/Service.php (line 36, delete log from filesystem), and admin-ui-classic-bundle/src/Helper/Dashboard.php (line 64, dashboard config from filesystem) [1][2][3]. These calls bypass the Tool\Serialize wrapper entirely, meaning any class available in the autoloader can be instantiated during deserialization.
Exploitation
An attacker must first gain the ability to write to one of the data sources that feeds into these unserialize() calls. This could be achieved via SQL injection into the tmp_store, sites, or custom_layouts tables, or via a file write vulnerability targeting the WebDAV delete log file (webdav-delete.dat) [2][3]. Once a writable data source is identified, the attacker writes a serialized PHP gadget chain (e.g., using known chains from Guzzle, Symfony, Monolog, or Doctrine) to that location. When Pimcore later reads and unserialize()s the poisoned data (e.g., on a page that reads TmpStore or triggers WebDAV restore logic), the gadget chain executes [3].
Impact
Successful exploitation results in PHP object injection that can be chained into remote code execution (RCE). The attacker gains full control over the server with the privileges of the web application, enabling data exfiltration, further compromise, or lateral movement [2][3]. The CVSS 3.1 score is 8.0 (High) with vector AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H, reflecting the need for prior data-source write access but the high impact of full host compromise [3].
Mitigation
The fix is implemented in commit 4788bf3a3a7f2f760a8fe61e522565941e154e1e (part of Pull Request #19119) and was merged on 2026-05-07 [1][4]. The patch changes Tool\Serialize::unserialize() to accept an allowed_classes parameter and restricts deserialization in the affected locations to only explicitly allowed classes (e.g., Asset::class in the WebDAV delete log) [1]. Users should update pimcore/pimcore and pimcore/admin-ui-classic-bundle to the latest patched version. No workaround is available if upgrading is not immediately possible; mitigating the prerequisite data-source write vulnerabilities (e.g., preventing SQL injection and unauthorized file writes) can reduce risk.
- [Security]: Harden unserializer and refine allowed classes (#19119) · pimcore/pimcore@4788bf3
- CVE-2026-45162 - GitHub Advisory Database
- Unsafe PHP Deserialization in Multiple Locations Without allowed_classes Restriction
- [Security]: Harden unserializer and refine allowed classes by kingjia90 · Pull Request #19119 · pimcore/pimcore
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
3- Range: 11
Patches
14788bf3a3a7f[Security]: Harden unserializer and refine allowed classes (#19119)
7 files changed · +24 −11
lib/Tool/Serialize.php+6 −4 modified@@ -26,13 +26,15 @@ public static function serialize(mixed $data): string return serialize($data); } - public static function unserialize(?string $data = null): mixed + public static function unserialize(?string $data = null, array|bool $allowedClasses = true): mixed { - if ($data) { - $data = unserialize($data); + if ($data === null || $data === '') { + return $data; } - return $data; + return unserialize($data, [ + 'allowed_classes' => $allowedClasses, + ]); } /**
models/Asset/WebDAV/Service.php+5 −1 modified@@ -30,7 +30,11 @@ public static function getDeleteLog(): array { $log = []; if (file_exists(self::getDeleteLogFile())) { - $log = unserialize(file_get_contents(self::getDeleteLogFile())); + $raw = file_get_contents(self::getDeleteLogFile()); + if (is_string($raw)) { + $log = unserialize($raw, ['allowed_classes' => [Asset::class]]); + } + if (!is_array($log)) { $log = []; } else {
models/Asset/WebDAV/Tree.php+2 −1 modified@@ -17,6 +17,7 @@ use Pimcore\Logger; use Pimcore\Model\Asset; use Pimcore\Model\Element; +use Pimcore\Tool\Serialize; use Sabre\DAV; /** @@ -54,7 +55,7 @@ public function move($sourcePath, $destinationPath): void // see: Asset\WebDAV\File::delete() why this is necessary $log = Asset\WebDAV\Service::getDeleteLog(); if (!$asset && array_key_exists('/' .$destinationPath, $log)) { - $asset = \Pimcore\Tool\Serialize::unserialize($log['/' .$destinationPath]['data']); + $asset = Serialize::unserialize($log['/' . $destinationPath]['data'], false); if ($asset) { $sourceAsset = Asset::getByPath('/' . $sourcePath); $asset->setData($sourceAsset->getData());
models/DataObject/ClassDefinition/CustomLayout/Dao.php+7 −1 modified@@ -63,7 +63,13 @@ public function getById(?string $id = null): void } if ($data && is_string($data['layoutDefinitions'] ?? null)) { - $data['layoutDefinitions'] = unserialize($data['layoutDefinitions']); + //Legacy fallback, see save() nested json_decode json_encode + $layoutDefinitions = \Pimcore\Tool\Serialize::unserialize($data['layoutDefinitions'], false); + if (is_array($layoutDefinitions)) { + $data['layoutDefinitions'] = Model\DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($layoutDefinitions, true); + } else { + $data['layoutDefinitions'] = $layoutDefinitions; + } } elseif (is_array($data['layoutDefinitions'] ?? null)) { $data['layoutDefinitions'] = Model\DataObject\ClassDefinition\Service::generateLayoutTreeFromArray($data['layoutDefinitions'], true); }
models/DataObject/ClassDefinition/Data/Table.php+1 −1 modified@@ -217,7 +217,7 @@ public function getDataForResource(mixed $data, ?DataObject\Concrete $object = n */ public function getDataFromResource(mixed $data, ?DataObject\Concrete $object = null, array $params = []): array { - $unserializedData = Serialize::unserialize((string) $data); + $unserializedData = Serialize::unserialize((string) $data, false); if ($data === null || empty($unserializedData)) { return [];
models/DataObject/Service.php+1 −1 modified@@ -985,7 +985,7 @@ public static function extractFieldDefinitions(ClassDefinition\Data|ClassDefinit public static function getSuperLayoutDefinition(Concrete $object): mixed { $mainLayout = $object->getClass()->getLayoutDefinitions(); - $superLayout = unserialize(serialize($mainLayout)); + $superLayout = self::cloneDefinition($mainLayout); self::createSuperLayout($superLayout);
models/Site/Dao.php+2 −2 modified@@ -61,8 +61,8 @@ public function getByDomain(string $domain): void $sitesRaw = $this->db->fetchAllAssociative('SELECT id,domains FROM sites'); $wildcardDomains = []; foreach ($sitesRaw as $site) { - if (!empty($site['domains']) && strpos($site['domains'], '*')) { - $siteDomains = unserialize($site['domains']); + if (!empty($site['domains']) && strpos($site['domains'], '*') !== false) { + $siteDomains = unserialize($site['domains'], ['allowed_classes' => false]); if (is_array($siteDomains) && count($siteDomains) > 0) { foreach ($siteDomains as $siteDomain) { if (str_contains($siteDomain, '*')) {
Vulnerability mechanics
Root cause
"Multiple locations in Pimcore call PHP's `unserialize()` on data from database columns and filesystem files without the `allowed_classes` restriction, enabling PHP object injection."
Attack vector
An attacker must first gain the ability to write to one of the data sources — for example, via SQL injection targeting the `tmp_store`, `sites`, or `custom_layouts` tables, or via a file write vulnerability targeting the WebDAV delete log (`webdav-delete.dat`) [ref_id=2]. Once a serialized PHP gadget chain (e.g., Monolog `BufferHandler` chain from phpggc) is planted, any subsequent read operation that triggers `unserialize()` on that data will instantiate the gadget objects, leading to arbitrary code execution [ref_id=2]. The attack requires a separate write primitive, but the deserialization step itself requires no special privileges beyond the ability to access the affected page or operation [CWE-502].
Affected code
The vulnerability spans six files in `pimcore/pimcore` and `pimcore/admin-ui-classic-bundle`: `lib/Tool/Authentication.php` (line 82), `models/Site/Dao.php` (line 68), `models/DataObject/ClassDefinition/CustomLayout/Dao.php` (line 69), `models/Tool/TmpStore/Dao.php` (line 64), `models/Asset/WebDAV/Service.php` (line 36), and `admin-ui-classic-bundle/src/Helper/Dashboard.php` (line 64) [ref_id=2]. Each calls PHP's `unserialize()` directly on data from database columns or filesystem files without the `allowed_classes` restriction [ref_id=2].
What the fix does
The patch [patch_id=2713663] addresses the issue by adding the `allowed_classes` parameter to every vulnerable `unserialize()` call. In `models/Site/Dao.php` and `models/DataObject/ClassDefinition/Data/Table.php`, deserialization now uses `['allowed_classes' => false]` to reject all objects [ref_id=1]. In `models/Asset/WebDAV/Service.php`, the allowed classes are restricted to only `Asset::class` [ref_id=1]. The `Tool\Serialize::unserialize()` wrapper itself was hardened to accept an `$allowedClasses` parameter and pass it through to PHP's `unserialize()` [ref_id=1]. Additionally, `models/DataObject/Service.php` replaced a `unserialize(serialize(...))` deep-copy pattern with a safe `cloneDefinition()` method [ref_id=1].
Preconditions
- inputAttacker must be able to write to a data source consumed by one of the six vulnerable unserialize() calls (e.g., via SQL injection on tmp_store/sites/custom_layouts tables, or file write on webdav-delete.dat or dashboard config)
- networkThe attacker must be able to trigger the code path that reads and deserializes the poisoned data (e.g., accessing a page that reads TmpStore, or performing a WebDAV operation)
Reproduction
1. Identify a writable data source (e.g., `tmp_store` table via SQL injection, or `webdav-delete.dat` via file write). 2. Write a serialized PHP gadget chain (e.g., Monolog `BufferHandler` chain from phpggc) into that data source. 3. Trigger the deserialization (e.g., access a page that reads TmpStore, or trigger a WebDAV operation). 4. The gadget chain executes with web server privileges [ref_id=2].
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
0No linked articles in our index yet.