Phar deserialization is the PHP bug that fires with no unserialize() call anywhere in sight. When people study phar deserialization, the surprise is always the same: object injection happens because a file function touched a path, not because the code asked to rebuild an object. The phar:// stream wrapper does the rebuilding quietly, using metadata baked into a PHP archive the attacker planted earlier.
This is a sibling of plain php object injection, and it rides the same gadget machinery. If the mechanics of rebuilt objects and magic methods are new to you, start with the insecure deserialization primer and the deserialization gadget chain hub, then come back.
What phar deserialization is and why it hides
A Phar is a PHP archive, PHP’s version of a self contained package. Every Phar carries a metadata field, and that field is stored in serialized PHP form. Here is the catch. When any filesystem function is given a path that begins with the phar:// wrapper, PHP opens the archive and calls unserialize() on that metadata to read it. The call is inside the engine, so it never appears in the application source.
That means a line as ordinary as this can be the sink:
if (file_exists($_GET['path'])) {
// ...
}
If an attacker sets path to phar://uploads/avatar.jpg/x, and they earlier uploaded a crafted Phar disguised as an image, then file_exists() triggers the metadata unserialize. Whatever objects the metadata describes get built, and their magic methods run.
The file functions that reach the stream wrapper
A long list of functions accept stream wrapped paths, and most developers never think of them as dangerous. Any of these can start the process when handed a phar:// path:
file_exists(),is_file(),is_dir(), and the rest of the stat family.file_get_contents(),fopen(),copy(), andunlink().getimagesize(), which image upload code calls all the time.- Even
includeandrequirewhen the path is built from user input.
The attacker needs two things: a place to drop a file whose bytes form a valid Phar, and a file function that will later read a path they influence. Image uploads make both easy, because a Phar can be prefixed with image bytes and still parse, so it passes a naive content check and sits on disk as avatar.jpg.
How phar deserialization turns into code execution
Once the metadata is unserialized, the attack is identical to ordinary object injection. The metadata describes an object of a class that is already loaded, its magic method fires, and that method kicks off a chain that you never wrote to accept outside input. The finding of those chains is its own craft, covered in finding deserialization gadget chains.
Here is a minimal view of building the malicious archive, for defenders who want to understand the shape of the payload:
$p = new Phar('evil.phar');
$p->startBuffering();
$p->addFromString('x', 'stub');
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$p->setMetadata(new Logger()); // an object with a dangerous __destruct
$p->stopBuffering();
The GIF89a prefix makes the file look like an image to a quick check. The real work is in setMetadata(), which stores the serialized gadget object. When a victim app later does file_exists('phar://.../evil.phar/x'), that object is rebuilt and its __destruct() runs.
The dangerous call is not in your code. It is in a file function you trusted, the instant it was handed a path that started with phar.
Spotting it in a codebase
Grep will not save you here, because there is no unserialize() to find. The real question is a data flow one: can user input reach the path argument of a file function? Look for:
- Paths built from request data:
$_GET,$_POST, route parameters, or headers passed into file calls. - Upload handlers that keep the original file on disk and then stat or read it by name.
- Image libraries fed a filename that came from the client.
Any of these lets an attacker swap in a phar:// path and point it at a file they control.
How to fix phar deserialization
The fixes work at two levels: stop the wrapper, and stop attacker files from ever being reachable.
- Validate the scheme of user paths. Reject any path that contains
phar://, or better, allowlist the exact schemes you expect and refuse the rest. A simplestripos($path, 'phar://') !== falsecheck, applied before the file call, blocks the common case. - Do not feed user input into file functions. Resolve uploads by a server generated id, never by a client supplied name or path.
- Store uploads outside the web root and rename them. Strip the extension, give the file a random name, and serve it through a handler that sets the content type. An archive that cannot be addressed by a predictable path is hard to target.
- Keep gadget classes out of reach. The same defenses that harden plain object injection apply, since the payload is still a serialized object that needs a usable class to build a chain. Prefer a data only format for anything you control.
For more bugs where trusted input handling turns against the app, see the injection and input category.
Phar deserialization is hard to catch by pattern because the sink is invisible and the source is a file path that looks harmless. UnboundCompute reasons about whether attacker input can actually reach a file function with a phar:// path and drive an object chain, rather than scanning for a literal call that is not there. Read how that assumption testing works on our about page.
Frequently asked questions
How does phar deserialization work without a call to unserialize?
Every PHP archive stores a metadata field in serialized form. When a filesystem function is handed a path that begins with the phar:// stream wrapper, the PHP engine opens the archive and unserializes that metadata on its own. The call lives inside the engine, so the application source shows only a normal file function.
Which PHP functions can trigger a phar deserialization?
Many file functions accept stream wrapped paths, including file_exists, is_file, file_get_contents, fopen, copy, unlink, and getimagesize, plus include and require when the path comes from user input. Any of them can start the metadata unserialize if an attacker supplies a phar:// path.
How do attackers plant the malicious Phar?
A Phar can be prefixed with image bytes such as a GIF header and still parse as a valid archive, so it passes a naive upload check and lands on disk as something like avatar.jpg. Later, a file function that reads a user supplied path can be pointed at it with a phar:// prefix to trigger the payload.
What is the fix for phar deserialization?
Reject user paths that contain phar://, or allowlist only the schemes you expect. Do not build file paths from client input, resolve uploads by a server generated id instead, and store uploads outside the web root with random names. These steps keep attacker files unreachable and keep the wrapper from firing.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
