Safeguard
Open Source Security

PHP object injection vulnerability landscape

Industry analysis of PHP object injection vulnerability trends, gadget chains, and real-world CVEs, plus how to find exploitable deserialization paths.

Safeguard Research Team
Research
8 min read

PHP powers an estimated three-quarters of the server-side web, and one of its oldest and most persistent vulnerability classes is still turning up in production code in 2026: PHP Object Injection. Security teams tracking CWE-502 ("Deserialization of Untrusted Data") in PHP ecosystems continue to see the pattern repeat across CMS plugins, e-commerce platforms, and internal line-of-business apps built on frameworks like Laravel, Symfony, and CodeIgniter. The underlying flaw is almost two decades old — PHP's unserialize() function has been a known danger zone since the mid-2000s — but the exploitation technique has only gotten more efficient, thanks to public gadget-chain tooling and a Composer dependency graph that quietly ships exploitable classes into thousands of applications that never call unserialize() themselves.

This piece walks through what PHP Object Injection actually is, why it keeps resurfacing at scale, the historical incidents that show its real-world blast radius, and what a modern application security program needs to do differently to find these bugs before an attacker does.

What PHP Object Injection Actually Is

PHP Object Injection (PHPOI, sometimes shortened to POI) occurs when an application passes attacker-controlled data into unserialize() (or, in some cases, functions that wrap it, like unserialize()-based session handlers, phar:// stream deserialization, or certain YAML/XML parsers configured to instantiate arbitrary classes). PHP's serialization format encodes not just data but class names, so an attacker who controls the serialized string can force the application to instantiate objects of classes that were never intended to be attacker-controlled.

By itself, instantiating an unexpected object usually does nothing dangerous. The real risk comes from PHP's "magic methods" — __wakeup(), __destruct(), __toString(), __call(), and others — which execute automatically at specific points in an object's lifecycle. If any class reachable from the application's autoloader has a magic method that performs a sensitive operation (writing a file, running a query, calling eval(), invoking system()), an attacker can chain multiple objects together so that the deserialization of one attacker-supplied payload triggers a cascade of magic-method calls that ends in remote code execution, arbitrary file write, or SQL injection.

This is fundamentally different from most vulnerability classes because the exploitable code often has nothing to do with the application logic the developer wrote. The vulnerable "gadget" might live three dependency layers down, in a logging library or an ORM the developer has never directly interacted with. The vulnerability is in the reachable class graph, not in a single line of business logic.

The Gadget Chain Ecosystem

What makes PHP Object Injection distinct from a generic deserialization bug in, say, Java or .NET is the maturity of public tooling for chaining PHP gadgets. PHPGGC (PHP Generic Gadget Chains), an open-source project maintained since 2016, catalogs pre-built exploitation chains for dozens of widely used PHP components — including Monolog, Guzzle, Doctrine, SwiftMailer, Symfony, Laravel, CodeIgniter 4, Zend Framework, CakePHP, and WordPress core itself. An attacker who identifies that a target application merely includes one of these libraries in its Composer dependency tree — regardless of whether the app's own code was written carelessly — can often generate a working exploit payload in seconds with a single command-line invocation.

This is the crux of why PHP Object Injection remains dangerous at scale: the vulnerable "sink" (the unserialize() call) is frequently a single, easily fixed line, but the "gadget" that makes it exploitable can be an incidental transitive dependency the security team doesn't even know is present. Traditional dependency scanning that only flags a package for a known CVE misses this entirely — the gadget classes are not, on their own, "vulnerable" packages. They only become dangerous in combination with a reachable deserialization sink elsewhere in the same application.

Historical Incidents That Show the Blast Radius

PHP Object Injection is not a theoretical concern; it has produced some of the most widely exploited PHP vulnerabilities of the last decade.

Joomla, CVE-2015-8562 (December 2015). A PHP Object Injection flaw in Joomla's com_fields/com_contenthistory handling allowed an attacker to inject a crafted User-Agent HTTP header that was later deserialized by the application. Within days of disclosure, mass-scanning and automated exploitation against unpatched Joomla installs was observed across the internet, compromising sites that had not yet applied the emergency 3.4.6 release. The incident remains a textbook example of how quickly a PHPOI disclosure moves from advisory to internet-wide exploitation once a working gadget chain is public.

vBulletin, CVE-2019-16759 ("vBulletin 0-day," September 2019). A pre-authentication PHP Object Injection vulnerability in vBulletin 5.x's ajax/api/hook/decodeArguments and widgetConfig handling allowed unauthenticated attackers to achieve remote code execution by submitting a crafted serialized payload. A full working exploit and Metasploit module appeared publicly within 24 hours of disclosure, and mass exploitation against internet-facing vBulletin forums followed almost immediately — a stark illustration of how little time defenders get once a PHPOI gadget chain is weaponized.

The WordPress plugin ecosystem, ongoing. WordPress core and its plugin marketplace remain one of the most consistent sources of new PHP Object Injection disclosures. Because so many plugins implement custom caching, import/export, or "settings restore" features that call unserialize() on data pulled from the database, request parameters, or uploaded files, security researchers and bug bounty programs continue to report new PHPOI findings across the plugin ecosystem every year. Given that WordPress powers a substantial share of all websites, a single popular plugin's PHPOI disclosure can put hundreds of thousands of individual sites at simultaneous risk — and because plugin update rates lag far behind core WordPress updates, the exploitation window frequently stays open for months after a patch ships.

Framework-level ecosystems are not immune either. Composer-based PHP applications built on Laravel, Symfony, and similar frameworks have repeatedly been shown — through both academic research and independent security testing — to contain exploitable gadget chains in commonly bundled dependencies, even when the applications themselves never explicitly call unserialize() on obviously untrusted input. Serialized data can enter an application through less obvious channels: cache backends, message queues, cookie-based session storage, and even third-party API responses that get persisted and later reconstituted.

Why This Class Keeps Recurring

A few structural factors keep PHP Object Injection near the top of the PHP vulnerability landscape year after year:

  • Legacy defaults. A large share of production PHP was written before json_encode()/json_decode() became the idiomatic choice for data interchange, so serialized-object patterns are baked into caching layers, session handlers, and "save state" features across older codebases that are expensive to refactor.
  • Transitive gadget exposure. Modern Composer-based dependency trees routinely pull in libraries with known gadget-worthy magic methods, even when the application team never chose those libraries for that purpose. A single upstream update can introduce a new gadget without anyone on the security team noticing.
  • Public, low-effort exploitation tooling. PHPGGC and similar tools mean that once a reachable unserialize() sink is found, generating a working exploit chain for a common framework or library is often a matter of minutes, not days.
  • Detection blind spots. Static analyzers that only look for the literal string unserialize( in first-party code miss sinks introduced through wrapper functions, session serialization handlers configured via php.ini (unserialize_callback_func, custom session save handlers), and phar deserialization triggered by filesystem functions like file_exists() or fopen() on attacker-influenced paths.

Why Reachability Is the Deciding Factor

The single biggest predictor of whether a given PHP Object Injection finding matters is not "does this dependency contain a gadget class" — it's "can attacker-controlled data actually reach an unserialize() call, and is a gadget class with a dangerous magic method loadable in that same request's class graph." Two applications can depend on the exact same vulnerable version of a logging or templating library; in one, the gadget is completely unreachable because the app never deserializes user input, and in the other, it's a critical pre-auth RCE. Treating both findings identically — which is what most SCA tools that only match against known-CVE package versions still do — either buries security teams in noise or, worse, gives them false confidence that an absence of a CVE match means an absence of risk.

How Safeguard Helps

Safeguard is built around exactly this reachability problem. Rather than flagging every application that happens to depend on a package with a historical PHP Object Injection advisory, Safeguard's reachability analysis traces whether attacker-controlled input can actually flow to an unserialize() sink and whether a gadget class with an exploitable magic method is loadable in that code path — separating theoretical exposure from exploitable risk. Griffin, Safeguard's AI-driven analysis engine, reviews the surrounding code context to assess exploitability the way a human security engineer would, cutting through the false-positive volume that plagues traditional pattern-matching scanners. Safeguard also generates and ingests SBOMs to give teams continuous, accurate visibility into which transitive dependencies carry known gadget-chain classes across their entire PHP application portfolio, not just the packages they directly require. And where a fix is available, Safeguard can open an auto-fix pull request that replaces unsafe deserialization calls or upgrades the affected dependency, shrinking the time between "reachable PHP Object Injection confirmed" and "patched in production" from weeks to hours.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.