GHSA-4mph-v827-f877: Locutus has Prototype Pollution via __proto__ Key Injection in unserialize()
SummaryThe `unserialize()` function in `locutus/php/var/unserialize` assigns deserialized keys to plain objects via bracket notation without filtering the `__proto__` key. When a PHP serialized payload contains `__proto__` as an array or object key, JavaScript's `__proto__` setter is invoked, replacing the deserialized object's prototype with attacker-controlled content. This enables property injection, for...in propagation of injected properties, and denial of service via built-in method override.This is distinct from the previously reported prototype pollution in `parse_str` (GHSA-f98m-q3hr-p5wq, GHSA-rxrv-835q-v5mh) — `unserialize` is a different function with no mitigation applied.DetailsThe vulnerable code is in two functions within `src/php/var/unserialize.ts`:**`expectArrayItems()` at line 358:****`expectObject()` at line 278:**Both functions create a plain object (`{}`) and assign user-controlled keys via bracket notation. When the key is `__proto__`, JavaScript's `__proto__` setter replaces the object's prototype rather than creating a regular property. This means:Properties in the attacker-supplied prototype become accessible via dot notation and the `in` operatorThese properties are invisible to `Object.keys()`, `JSON.stringify()`, and `hasOwnProperty()`They propagate to copies made via `for...in` loops, becoming real own propertiesThe attacker can override `hasOwnProperty`, `toString`, `valueOf` with non-function valuesNotably, `parse_str` in the same package has a regex guard against `__proto__` (line 74 of `src/php/strings/parse_str.ts`), but no equivalent protection was applied to `unserialize`.This is **not** global `Object.prototype` pollution — only the deserialized object's prototype is replaced. Other objects in the application are not affected.PoC**Setup:****Step 1 — Property injection via array deserialization:****Verified output:****Step 2 — for...in propagation makes injected properties real:****Verified output:****Step 3 — Method override denial of service:****Verified output:****Step 4 — Object type (stdClass) is also vulnerable:****Step 5 — Confirm NOT global pollution:**Impact**Property injection**: Attacker-controlled properties become accessible on the deserialized object via dot notation and the `in` operator while being invisible to `Object.keys()` and `hasOwnProperty()`. Applications that use `if (config.isAdmin)` or `if ('role' in config)` patterns on deserialized data are vulnerable to authorization bypass.**Property propagation**: When consuming code copies the object using `for...in` (a common JavaScript pattern for object spreading or cloning), injected prototype properties materialize as real own properties, surviving all subsequent `hasOwnProperty` checks.**Denial of service**: The injected prototype can override `hasOwnProperty`, `toString`, `valueOf`, and other `Object.prototype` methods with non-function values, causing `TypeError` when these methods are called on the deserialized object.The primary use case for locutus `unserialize` is deserializing PHP-serialized data in JavaScript applications, often from external or untrusted sources. This makes the attack surface realistic.Recommended FixFilter dangerous keys before assignment in both `expectArrayItems` and `expectObject`. Use `Object.defineProperty` to create a data property without triggering the `__proto__` setter:Alternatively, create objects with a null prototype to prevent `__proto__` setter invocation entirely:The `Object.create(null)` approach is more robust as it prevents the `__proto__` setter from ever being triggered, regardless of key value.Maintainer ReponseThank you for the report. This issue was reproduced locally against `locutus@3.0.24`, confirming that `unserialize()` was vulnerable to `__proto__`-driven prototype injection on the returned object.This is now fixed on `main` and released in `locutus@3.0.25`.Fix Shipped In**PR:** [#597](https://github.com/locutusjs/locutus/pull/597)**Merge commit on `main`:** `345a6211e1e6f939f96a7090bfeff642c9fcf9e4`**Release:** [v3.0.25](https://github.com/locutusjs/locutus/releases/tag/v3.0.25)What the Fix DoesThe fix hardens `src/php/var/unserialize.ts` by treating `__proto__`, `constructor`, and `prototype` as dangerous keys and defining them as plain own properties instead of assigning through normal bracket notation. This preserves the key in the returned value without invoking JavaScript's prototype setter semantics.Tested Repro Before the FixAttacker-controlled serialized `__proto__` key produced inherited properties on the returned object`Object.keys()` hid the injected key while `'key' in obj` stayed trueBuilt-in methods like `hasOwnProperty` could be disruptedTested State After the Fix in `3.0.25`Dangerous keys are kept as own enumerable propertiesThe returned object's prototype is not replacedThe regression is covered by `test/custom/unserialize-prototype-pollution.vitest.ts`The locutus team is treating this as a real package vulnerability with patched version `3.0.25`.
CVSS v4.0: 6.9