You ask an AI assistant for a quick PHP snippet, it needs to store some session data, or cache an object, or pass a configuration array between requests, the model returns code that works, and the code looks clean, and there is a line in it that reads unserialize($data).
Nothing about that line looks alarming unless you know what it does, and that is the whole problem, because AI assistants have been trained on years of PHP code that uses unserialize() casually, and they reproduce that casualness faithfully, even when the input is user controlled.
This is the unsafe pattern, and it is worth understanding because it is one of the most reliably exploitable issues in PHP applications, and AI tools are making it appear in new codebases at a steady rate.
Important Disclaimer
This article is intended for educational and defensive purposes only, and the techniques described here are shared to help developers and security professionals recognize and fix unsafe patterns in code they own.
Do not use these techniques against systems you do not own or do not have explicit written permission to test, because unauthorized testing is illegal in most jurisdictions.
The author assumes no liability for any damages, legal consequences, or other outcomes resulting from the use or misuse of this information, so always obtain proper authorization before conducting any testing, and stay legal, stay ethical, stay responsible.
Why unserialize Is Dangerous in One Paragraph
PHP's serialize() turns an object into a string, and unserialize() turns that string back into an object, and during that reconstruction PHP will call magic methods such as __wakeup() and __destruct() on the object it is rebuilding.
If an attacker can control the string that reaches unserialize(), they can choose which class gets instantiated and what properties it holds, and if any class in the codebase has a magic method that does something interesting with those properties, the attacker can chain them together into a payload that runs code, writes files, deletes data, or reaches the network.
The attacker does not need a vulnerability in unserialize() itself, they need a gadget chain, which is a sequence of existing classes whose magic methods interact in a useful way, and large applications almost always contain at least one.
Why AI Assistants Reproduce the Pattern
There is no conspiracy here, and there is no bug in the model, there is a training data problem, and it is worth understanding because it explains the behavior you will see.
The Pattern Is Everywhere in Training Data
For two decades, PHP tutorials, forum answers, and open source projects have used unserialize() to handle cookies, sessions, caches, queues, and form state, and those examples were written when the risk was less well understood, and many of them are still online.
The model learned from that corpus, and what it learned is that unserialize() is a normal tool for turning a string back into data, which is a reasonable thing to learn from the data it was given.
The Pattern Appears Without Context
A model does not know whether the string came from a trusted cache or from a cookie the user controls, and it has no way to know, because the prompt rarely includes that context.
If you ask for a snippet that parses a cookie, the model will produce unserialize() on the cookie value, and it will be doing exactly what you asked, and the result will be a vulnerability.
Documentation Frames It Neutrally
The official documentation describes what the function does and lists the magic methods it invokes, and it also warns against untrusted input, and the model may have learned the description more thoroughly than the warning.
The warning is easy to miss when the surrounding text is technical, and models learn the technical parts very well.
Completion Rewards the Familiar
When the model is completing code, it produces what statistically follows from the surrounding context, and in a great deal of PHP code, a string that came from base64_decode() is followed by unserialize(), so the completion follows that pattern even when it is unsafe.
What the Generated Code Usually Looks Like
The pattern appears in a small number of recognizable shapes, and once you know them, you can scan for them quickly.
The Cookie Parser
$data = base64_decode($_COOKIE['prefs']);
$prefs = unserialize($data);The user controls the cookie, so the user controls the serialized string, and the application reconstructs whatever object the user chose.
The Cache Loader
$cached = unserialize(file_get_contents($cacheFile));If an attacker can write to the cache directory, or if the cache key is derived from user input, this is exploitable, and the code does not check any of that.
The API Response Handler
$response = unserialize($payload);If the payload comes from a third party service, and if the service is compromised, or if the transport is not authenticated, the application trusts whatever object the response contains.
The Form State Field
$state = unserialize(base64_decode($_POST['state']));Hidden fields are user controlled, and users can edit them, and the code above treats them as trusted.
How the Exploitation Works
The exploitation path is worth understanding because it explains why the fix is not simply "validate the input."
Step One: Find a Gadget
A gadget is a class with a magic method that does something useful, and the classic example is a class whose __destruct() writes a file or whose __wakeup() makes a network request.
Large codebases and their dependencies almost always contain several, and they do not need to be malicious, they just need to be present and to do something with the properties the attacker can set.
Step Two: Build the Payload
The attacker constructs an object of that class with properties chosen to trigger the useful behavior, and serializes it, and the resulting string is the payload.
Step Three: Chain If Needed
If a single gadget is not enough, the attacker chains several, where one class's __destruct() calls a method on a property that is another attacker controlled object, and that object's method does something else, and the chain continues until the goal is reached.
Step Four: Deliver It
The payload is base64 encoded, placed in the cookie, the field, or the API parameter, and sent to the application, and unserialize() reconstructs the object and invokes the magic methods, and the code runs.
The reason this works is that the attacker is not breaking into the application, they are using the application's own classes exactly as the application uses them, and every step is legitimate PHP behavior.
Two Real Scenarios
Scenario 1: The Preference Cookie
The Setup
A PHP application stores user display preferences in a base64 encoded cookie, and an AI assistant generated the loader in response to a prompt about persisting settings across requests.
The Attack
A researcher examines the application and identifies a class in the codebase whose __destruct() deletes a file at a path taken from a property, and they construct a serialized object of that class with the property set to a sensitive file.
The Result
The cookie is delivered, unserialize() reconstructs the object, the object is destroyed at the end of the request, and the file is deleted.
The Lesson
The generated code was correct for the task, and the vulnerability came from the combination of untrusted input and a gadget that already existed.
Scenario 2: The Job Queue
The Setup
A background worker pulls serialized job payloads from a queue, and the queue is shared with other services, and the worker deserializes whatever arrives.
The Attack
A compromised service, or a service with weaker controls, publishes a crafted serialized object to the queue, and the worker deserializes it and invokes the magic methods.
The Result
Code runs in the worker's context, which often has elevated database and filesystem access.
The Lesson
Trust boundaries matter, and a queue that accepts messages from multiple sources is a boundary that the code does not enforce.
The Fixes That Actually Work
The fix is not to avoid serialize() and unserialize() entirely, it is to stop using them on data you do not fully control, and there are better tools for almost every use case.
1. Use JSON for Data Interchange
For preferences, form state, API responses, and cache values that are simple data, use json_encode() and json_decode() with $associative = true, because JSON cannot instantiate objects and therefore cannot invoke magic methods.
This is the single most effective change, and it applies to the majority of cases where the model generated unserialize().
2. Use allowed_classes When You Cannot Avoid It
PHP 7 and later support an allowed_classes option that restricts which classes can be instantiated, and passing false disables object instantiation entirely.
$data = unserialize($input, ['allowed_classes' => false]);If the payload only needs arrays and scalar values, this option makes the function safe even on user input, and it is worth adding whenever you cannot switch to JSON immediately.
3. Sign Your Payloads
If you must accept serialized data from a user, sign it, and verify the signature before deserializing, using an HMAC with a key the user does not have.
This is the same approach frameworks use for session cookies, and it works, as long as the key stays secret and the verification happens before unserialize() runs.
4. Keep the Input Trusted
If the serialized data comes from your own cache, make sure the cache cannot be written by an attacker, and make sure the cache key cannot be influenced by user input.
Trust boundaries are the real fix, and unserialize() is safe when everything that reaches it is genuinely trusted.
5. Patch the Gadgets
If your codebase contains classes whose magic methods write files, make network requests, or execute commands based on properties, consider whether those behaviors need to be reachable through deserialization, and restrict them where possible.
This does not replace the other fixes, and it reduces the value of any remaining object injection primitive.
6. Add Static Analysis
PHPStan, Psalm, and Semgrep all support rules that flag unserialize() on tainted input, and running them in CI catches the pattern before it ships.
This is how you catch the model's output, because the model will keep generating it.
7. Review AI-Generated Code Specifically for This
When you accept a suggestion that handles cookies, sessions, caches, or API responses, look for unserialize(), and if it appears, ask why, and if the answer is "because it is simpler," switch to JSON.
The review is a two second habit that closes the gap the model cannot see.
8. Disable Unnecessary Deserialization Endpoints
If a feature only needs to accept scalar values, make the endpoint reject anything that looks like a serialized object, and log the attempt, because that pattern in a request body is rarely legitimate.
Quick Reference: unserialize Risk Checklist
|
Context |
Risk |
Fix |
|
Cookie or form field |
High |
Use JSON, or allowed_classes => false |
|
Cache file |
High |
Ensure the cache is not writable by users |
|
API response |
Moderate |
Verify transport and trust, prefer JSON |
|
Job queue |
High |
Authenticate producers, prefer JSON |
|
Session store |
High |
Use the framework handler, restrict writes |
|
Migration scripts |
Moderate |
Review as production code |
The Bottom Line
AI assistants generate unserialize() because the training data is full of it, and they generate it in contexts where it is unsafe because they cannot see the data flow that makes it unsafe.
The pattern is not a model failure, it is a data flow failure, and the fix is the same fix that has always applied, which is to keep untrusted input away from the function, to prefer JSON, to use allowed_classes where you cannot, and to sign what you must accept.
Review the generated code for this pattern specifically, because it is small, it is common, and it is one of the few lines in a PHP file that can turn a harmless feature into remote code execution.
The model will keep suggesting it, and the review is what makes the suggestion safe.
FAQ
Why does AI keep generating unserialize()?
Because PHP code on the internet has used it for decades, and the model learned that pattern from its training data without the context that makes it dangerous.
Is unserialize() always vulnerable?
No, it is safe when the input is genuinely trusted, and it becomes dangerous when an attacker can control the string that reaches it.
What is a POP chain?
It is a sequence of classes whose magic methods interact, allowing an attacker who controls a serialized object to trigger useful behavior through legitimate application code.
Can I just validate the input?
Validation helps, and it is not sufficient on its own, because the serialized format is complex and the safest fix is to avoid the function on untrusted data.
What is allowed_classes?
It is a PHP option that limits the creation of certain classes upon deserialization. When this directive is set to false, no objects will be instantiated.
Why not use JSON instead?
Yes, almost always when you have simple data, because JSON does not instantiate objects and therefore does not invoke magic methods.