CVE-2026-33993: Locutus Locutus Has Prototype Pollution Vulnerability
MODERATECVE-2026-33993 is a MODERATE locutus has prototype pollution vulnerability (CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')) affecting Locutus libraries in the npm ecosystem. The issue involves locutus has prototype pollution via __proto__ key injection in unserialize(). Remediation is available by upgrading affected packages to version 3.0.25.
CVE-2026-33993 at a Glance
How to Fix CVE-2026-33993
To remediate this vulnerability, upgrade all affected Locutus dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install locutus@3.0.25
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls locutus
How to Check if You Are Affected
To determine whether your Node.js application is affected by CVE-2026-33993, inspect your project dependencies using npm:
npm ls locutus
Analyze the terminal output to locate any version of Locutus in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.0.25), your application is affected.
Note: The npm ls locutus command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution'), where the product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype. In Locutus, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-33993 Affected Locutus Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
locutus
|
npm |
< 3.0.25
|
3.0.25
|
References
Frequently Asked Questions
What is CVE-2026-33993?
Which Locutus versions are affected?
What version fixes CVE-2026-33993?
Is locutus affected?
What CWE is associated with CVE-2026-33993?
How can I check npm dependencies for CVE-2026-33993?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-4mph-v827-f877",
"modified" : "2026-03-30T20:14:41Z",
"published" : "2026-03-27T17:57:39Z",
"aliases" : [ "CVE-2026-33993" ],
"summary" : "Locutus has Prototype Pollution via __proto__ Key Injection in unserialize()",
"details" : "## Summary\n\nThe `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.\n\nThis 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.\n\n## Details\n\nThe vulnerable code is in two functions within `src/php/var/unserialize.ts`:\n\n**`expectArrayItems()` at line 358:**\n```typescript\n// src/php/var/unserialize.ts:329-366\nfunction expectArrayItems(\n str: string,\n expectedItems = 0,\n cache: CacheFn,\n): [UnserializedObject | UnserializedValue[], number] {\n // ...\n const items: UnserializedObject = {}\n // ...\n for (let i = 0; i < expectedItems; i++) {\n key = expectKeyOrIndex(str)\n // ...\n item = expectType(str, cache)\n // ...\n items[String(key[0])] = item[0] // line 358 — no __proto__ filtering\n }\n // ...\n}\n```\n\n**`expectObject()` at line 278:**\n```typescript\n// src/php/var/unserialize.ts:246-287\nfunction expectObject(str: string, cache: CacheFn): ParsedResult {\n // ...\n const obj: UnserializedObject = {}\n // ...\n for (let i = 0; i < propCount; i++) {\n // ...\n obj[String(prop[0])] = value[0] // line 278 — no __proto__ filtering\n }\n // ...\n}\n```\n\nBoth 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:\n\n1. Properties in the attacker-supplied prototype become accessible via dot notation and the `in` operator\n2. These properties are invisible to `Object.keys()`, `JSON.stringify()`, and `hasOwnProperty()`\n3. They propagate to copies made via `for...in` loops, becoming real own properties\n4. The attacker can override `hasOwnProperty`, `toString`, `valueOf` with non-function values\n\nNotably, `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`.\n\nThis is **not** global `Object.prototype` pollution — only the deserialized object's prototype is replaced. Other objects in the application are not affected.\n\n## PoC\n\n**Setup:**\n```bash\nnpm install locutus@3.0.24\n```\n\n**Step 1 — Property injection via array deserialization:**\n```js\nimport { unserialize } from 'locutus/php/var/unserialize';\n\nconst payload = 'a:2:{s:9:\"__proto__\";a:1:{s:7:\"isAdmin\";b:1;}s:4:\"name\";s:3:\"bob\";}';\nconst config = unserialize(payload);\n\nconsole.log(config.isAdmin); // true (injected via prototype)\nconsole.log(Object.keys(config)); // ['name'] — isAdmin is hidden\nconsole.log('isAdmin' in config); // true — bypasses 'in' checks\nconsole.log(config.hasOwnProperty('isAdmin')); // false — invisible to hasOwnProperty\n```\n\n**Verified output:**\n```\ntrue\n[ 'name' ]\ntrue\nfalse\n```\n\n**Step 2 — for...in propagation makes injected properties real:**\n```js\nconst copy = {};\nfor (const k in config) copy[k] = config[k];\nconsole.log(copy.isAdmin); // true (now an own property)\nconsole.log(copy.hasOwnProperty('isAdmin')); // true\n```\n\n**Verified output:**\n```\ntrue\ntrue\n```\n\n**Step 3 — Method override denial of service:**\n```js\nconst payload2 = 'a:1:{s:9:\"__proto__\";a:1:{s:14:\"hasOwnProperty\";b:1;}}';\nconst obj = unserialize(payload2);\nobj.hasOwnProperty('x'); // TypeError: obj.hasOwnProperty is not a function\n```\n\n**Verified output:**\n```\nTypeError: obj.hasOwnProperty is not a function\n```\n\n**Step 4 — Object type (stdClass) is also vulnerable:**\n```js\nconst payload3 = 'O:8:\"stdClass\":2:{s:9:\"__proto__\";a:1:{s:7:\"isAdmin\";b:1;}s:4:\"name\";s:3:\"bob\";}';\nconst obj2 = unserialize(payload3);\nconsole.log(obj2.isAdmin); // true\nconsole.log('isAdmin' in obj2); // true\n```\n\n**Step 5 — Confirm NOT global pollution:**\n```js\nconsole.log(({}).isAdmin); // undefined — global Object.prototype is clean\n```\n\n## Impact\n\n- **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.\n- **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.\n- **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.\n\nThe 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.\n\n## Recommended Fix\n\nFilter dangerous keys before assignment in both `expectArrayItems` and `expectObject`. Use `Object.defineProperty` to create a data property without triggering the `__proto__` setter:\n\n```typescript\nconst DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\n// In expectArrayItems (line 358) and expectObject (line 278):\nconst keyStr = String(key[0]); // or String(prop[0]) in expectObject\nif (DANGEROUS_KEYS.has(keyStr)) {\n Object.defineProperty(items, keyStr, {\n value: item[0],\n writable: true,\n enumerable: true,\n configurable: true,\n });\n} else {\n items[keyStr] = item[0];\n}\n```\n\nAlternatively, create objects with a null prototype to prevent `__proto__` setter invocation entirely:\n\n```typescript\n// Replace: const items: UnserializedObject = {}\n// With:\nconst items = Object.create(null) as UnserializedObject;\n```\n\nThe `Object.create(null)` approach is more robust as it prevents the `__proto__` setter from ever being triggered, regardless of key value.\n\n\n## Maintainer Reponse\n\nThank 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.\n\nThis is now fixed on `main` and released in `locutus@3.0.25`.\n\n## Fix Shipped In\n\n- **PR:** [#597](https://github.com/locutusjs/locutus/pull/597)\n- **Merge commit on `main`:** `345a6211e1e6f939f96a7090bfeff642c9fcf9e4`\n- **Release:** [v3.0.25](https://github.com/locutusjs/locutus/releases/tag/v3.0.25)\n\n## What the Fix Does\n\nThe 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.\n\n## Tested Repro Before the Fix\n\n- Attacker-controlled serialized `__proto__` key produced inherited properties on the returned object\n- `Object.keys()` hid the injected key while `'key' in obj` stayed true\n- Built-in methods like `hasOwnProperty` could be disrupted\n\n## Tested State After the Fix in `3.0.25`\n\n- Dangerous keys are kept as own enumerable properties\n- The returned object's prototype is not replaced\n- The regression is covered by `test/custom/unserialize-prototype-pollution.vitest.ts`\n\n---\n\nThe locutus team is treating this as a real package vulnerability with patched version `3.0.25`.",
"severity" : [ {
"type" : "CVSS_V4",
"score" : "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"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "locutus"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "3.0.25"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/locutusjs/locutus/security/advisories/GHSA-4mph-v827-f877"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-33993"
}, {
"type" : "WEB",
"url" : "https://github.com/locutusjs/locutus/pull/597"
}, {
"type" : "WEB",
"url" : "https://github.com/locutusjs/locutus/commit/345a6211e1e6f939f96a7090bfeff642c9fcf9e4"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/locutusjs/locutus"
}, {
"type" : "WEB",
"url" : "https://github.com/locutusjs/locutus/releases/tag/v3.0.25"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-1321" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-03-27T17:57:39Z",
"nvd_published_at" : "2026-03-27T23:17:14Z"
}
}