jay-tank / unserializeslip
Static analyzer that flags PHP Object Injection: a call to the bare global unserialize(...) on a value traceable (directly, or via a same-function local-variable assignment) to $_GET/$_POST/$_REQUEST/$_COOKIE/$_SERVER['HTTP_*']/Laravel request()->input()/$request->get()/$request->cookie(), with no [
Requires
- php: >=8.1
- nikic/php-parser: ^5.8
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-24 11:19:29 UTC
README
Static analyzer that flags PHP's version of the same deserialization-RCE
bug class this portfolio already covers for Java (unsafe YAML
Constructor) and .NET (Newtonsoft TypeNameHandling): a call to the
bare global unserialize(...) on a value traceable -- directly, or via
a same-function local-variable assignment -- to request/user-controlled
input ($_GET, $_POST, $_REQUEST, $_COOKIE, $_SERVER['HTTP_*'],
Laravel's request()->input(...)/$request->get(...)/
$request->cookie(...)), with no ['allowed_classes' => false] (or an
explicit class allowlist) passed as the second argument.
This is PHP Object Injection: unserialize() on attacker-controlled
data lets the attacker construct an instance of any class already
loaded (or autoloadable) in the application, with attacker-chosen
property values. If any such class has a "magic method" (__wakeup(),
__destruct(), __toString(), ...) with a dangerous side effect, the
attacker can trigger it purely by crafting a malicious serialized
string -- often with no other vulnerability needed. PHP's own
unserialize() manual page explicitly recommends allowed_classes as
the primary defense, and recommends json_decode() instead whenever
the data doesn't actually need to carry PHP object state. See
DETAILS.md for the full mechanism, the detection
heuristic, and honest limitations.
Quick start
composer require jay-tank/unserializeslip --dev vendor/bin/unserializeslip check app
$ vendor/bin/unserializeslip check demo/Bad.php demo/Bad.php:14: USS001 unserialize(...) is called on a value traceable to user/request-controlled input without ['allowed_classes' => false] (or an explicit class allowlist) as the second argument -- this is PHP Object Injection: an attacker can construct an instance of ANY class already loaded by the application with attacker-chosen property values, and if any such class has a magic method (__wakeup(), __destruct(), __toString(), ...) with a dangerous side effect, a crafted serialized string alone can trigger it. Pass ['allowed_classes' => false] (or an explicit array of allowed class names) as the second argument, or use json_decode() instead if the data does not need to carry PHP object state. unserializeslip: 1 finding(s).
$ vendor/bin/unserializeslip check demo/Good.php unserializeslip: 0 finding(s).
See docs/USAGE.md for the CLI, library API, and full rule reference.
The only correct patterns
// Bad -- any class already loaded can be instantiated with attacker- // chosen properties, and a dangerous magic method can fire on it. $state = unserialize($_POST['session_blob']); // Bad -- 'allowed_classes' => true explicitly allows every class, // which defeats the entire protection. $state = unserialize($_POST['session_blob'], ['allowed_classes' => true]); // Good -- no class can ever be instantiated; scalars/arrays still decode. $state = unserialize($_POST['session_blob'], ['allowed_classes' => false]); // Good -- only the named, deliberately reviewed classes can be instantiated. $state = unserialize($_POST['session_blob'], ['allowed_classes' => ['App\Dto\ImportRecord']]); // Best -- if the data doesn't need to carry PHP object state at all, // PHP's own manual recommends this instead of unserialize() entirely. $state = json_decode($_POST['session_blob'], true);
Prior art, honestly
Checked (2026-10) before building this. Real, overlapping prior art exists, but each has a real gap this package fills:
- Exakat's
Security/UnserializeSecondArgrule is genuine, Critical- severity prior art for the core idea ("unserialize()calls missing the second argument are dangerous") -- but it's shipped only in Exakat's Enterprise Edition/Cloud product (not the free CLI), and from its own documentation it flags anyunserialize()call missing the argument, with no taint/data-flow requirement -- it doesn't distinguish a call on genuinely request-controlled input from one on the application's own internal cache file. - Psalm's
TaintedUnserializeis real, closer prior art -- it uses Psalm's full taint-analysis engine to trace untrusted input all the way to anunserialize()sink. But it only runs under Psalm's taint-analysis mode (--taint-analysis, not Psalm's default static analysis), requires Psalm's broader type/taint infrastructure to be set up across the whole codebase, and (per Psalm's own docs) still flags tainted input reachingunserialize()regardless ofallowed_classesbeing set at all -- it does not specifically verify whetherallowed_classesis present,false, an allowlist, ortruethe wayunserializeslipdoes. - Datadog's (commercial) static analysis product has a
php-security/avoid-unserializerule with the same core idea and a compliant example matchingallowed_classes-- real prior art, but a paid, hosted product rather than a standalone, zero-config Composer CLI. - PHPStan's default rule set has nothing here -- no built-in rule
about
unserialize()orallowed_classeswas found.
What this means honestly: the core idea isn't new -- Exakat, Psalm,
and Datadog all have real, overlapping coverage. unserializeslip's
specific niche is combining (1) a standalone, zero-config, free Composer
CLI, (2) same-function taint tracing from the concrete request-input
sources used in this portfolio's other packages (not full project-wide
taint analysis), and (3) an exact, narrow check of allowed_classes's
value (false/allowlist = safe, missing/true/non-literal = unsafe)
rather than only its presence. Anyone already running Exakat Enterprise,
Psalm with taint-analysis enabled, or Datadog's static analysis should
treat this as a narrow, complementary check, not a replacement.
What it is not
Not a secrets scanner, not a full data-flow/taint analysis engine (like Psalm's taint mode), and not semantic/type resolution. It's a syntax-only AST pass with same-function-only variable tracing -- see DETAILS.md before wiring this into a blocking CI gate.
License
MIT (c) 2026 Jay Tank