Back to Blog
critical SEVERITY9 min read

How Prototype Pollution happens in Node.js and how to fix it

A critical prototype pollution vulnerability was discovered in `worker/import-core.js`, where `request.json()` parsed untrusted HTTP request bodies without filtering dangerous keys like `__proto__` and `constructor`. An attacker could send a crafted JSON payload to corrupt the global `Object` prototype, potentially affecting every object in the application runtime. The fix replaces the unsafe parse with a JSON reviver function that strips these dangerous keys before any object is constructed.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is a Prototype Pollution vulnerability (CWE-1321) in a Node.js Cloudflare Worker endpoint (`worker/import-core.js`). The `handleImport` function called `request.json()` directly on untrusted HTTP input, allowing attackers to inject `__proto__` or `constructor` keys and mutate the global Object prototype. The fix replaces `request.json()` with `JSON.parse(text, reviverFn)` using a reviver that returns `undefined` for those dangerous keys, preventing prototype mutation before any application logic runs.

Vulnerability at a Glance

cweCWE-1321
fixReplaced `request.json()` with `JSON.parse(rawText, reviverFn)` that strips dangerous keys via a reviver function
riskAttacker can corrupt the global Object prototype, affecting all objects in the runtime
languageJavaScript (Node.js)
root cause`request.json()` parsed untrusted HTTP bodies without filtering `__proto__` or `constructor` keys
vulnerabilityPrototype Pollution via unsafe JSON parsing

How Prototype Pollution happens in Node.js and how to fix it

The Vulnerability at a Glance

Field Detail
Vulnerability Prototype Pollution via unsafe JSON parsing
CWE CWE-1321
Language JavaScript (Node.js)
Risk Attacker corrupts global Object prototype, affecting all runtime objects
Root Cause request.json() parsed untrusted HTTP bodies without filtering __proto__ or constructor keys
Fix JSON.parse(rawText, reviverFn) with a reviver that strips dangerous keys

Introduction

The worker/import-core.js file handles inbound import requests — it reads a JSON specification from an HTTP request body and builds an internal map from it. On the surface, this looks like straightforward request handling. But a single line in the handleImport function created a critical security hole: the request body was parsed with request.json() and handed directly to application logic without any sanitization.

// Line 25 — the vulnerable pattern
let spec; try { spec = await request.json(); } catch (e) { return J(400, { error: 'body must be valid JSON' }); }

The problem is not that JSON is being parsed — it's how it's being parsed. request.json() in the Cloudflare Workers runtime (and in most JS environments) is a thin wrapper around JSON.parse. It does not filter or reject keys like __proto__ or constructor. An attacker who can reach this endpoint can send a payload that mutates Object.prototype itself, corrupting the behavior of every plain object created after that point in the same runtime isolate.


The Vulnerability Explained

What is Prototype Pollution?

Every JavaScript object inherits properties from its prototype chain. Object.prototype sits at the top of that chain, meaning any property added to it becomes visible on every plain object in the runtime. Prototype pollution is the act of exploiting a JSON parser (or a deep merge function) to write attacker-controlled values onto that shared prototype.

The attack vector is deceptively simple. Consider this JSON payload:

{
  "__proto__": {
    "isAdmin": true
  }
}

When JSON.parse processes this naively, it creates an object with a __proto__ key. In older Node.js versions and some environments, assigning to __proto__ during object construction directly mutates Object.prototype. Even in environments where direct mutation is blocked at assignment time, deeply nested payloads using constructor.prototype can achieve the same result:

{
  "constructor": {
    "prototype": {
      "isAdmin": true
    }
  }
}

The Specific Vulnerable Code

In worker/import-core.js, the handleImport function consumed the entire request body through request.json():

// BEFORE — vulnerable
let spec; try { spec = await request.json(); } catch (e) { return J(400, { error: 'body must be valid JSON' }); }
let map;  try { map = buildMapFromSpec(spec); } catch (e) { return J(400, { error: String(e && e.message || e) }); }

The parsed spec object is then passed directly into buildMapFromSpec(spec). If spec contains a __proto__ or constructor key, those keys may be processed by buildMapFromSpec or any downstream function that iterates object properties — spreading prototype pollution throughout the runtime.

Real-World Attack Scenario

An attacker sends a POST request to the import endpoint with a crafted body:

POST /import HTTP/1.1
Authorization: Bearer <valid-token>
Content-Type: application/json

{
  "__proto__": { "polluted": true },
  "nodes": [...]
}

Because the Authorization header check passes (the attacker has a valid token, or the token is not configured), the request reaches request.json(). The resulting spec object carries the __proto__ key. Depending on the JavaScript engine version and how buildMapFromSpec processes the spec, this can:

  1. Inject properties into all subsequent plain objects — any {} literal created later in the same isolate may unexpectedly have polluted: true.
  2. Bypass authorization checks — if any downstream code does if (obj.isAdmin) without an Object.hasOwn guard, a polluted prototype can make that check pass for every object.
  3. Cause denial of service — overwriting Object.prototype.toString or Object.prototype.hasOwnProperty breaks fundamental operations across the entire runtime.
  4. Enable remote code execution — in server-side template engines or eval-adjacent code paths, prototype pollution can escalate to RCE.

This is particularly dangerous because worker/import-core.js is described as a Node.js library — meaning downstream consumers who import this package inherit the vulnerability.


The Fix

What Changed

The fix replaces the one-liner request.json() call with a two-step process: first read the raw text with request.text(), then parse it using JSON.parse with a reviver function that explicitly returns undefined for any key named __proto__ or constructor.

// AFTER — safe
let spec; try { const _t = await request.text(); spec = JSON.parse(_t, (k, v) => (k === '__proto__' || k === 'constructor') ? undefined : v); } catch (e) { return J(400, { error: 'body must be valid JSON' }); }

Before vs. After

Before (vulnerable):

spec = await request.json();

After (safe):

const _t = await request.text();
spec = JSON.parse(_t, (k, v) => (k === '__proto__' || k === 'constructor') ? undefined : v);

Why This Fix Works

The JSON.parse reviver function is called for every key-value pair before the object is assembled. When the reviver returns undefined for a key, that key is completely omitted from the resulting object — it is never assigned, never iterated, and never reaches buildMapFromSpec. The dangerous keys are neutralized at the earliest possible point in the data pipeline.

This is the correct approach because:

  • It operates at parse time, not after the object is constructed. Filtering keys after JSON.parse is too late if the runtime has already processed __proto__ during object construction.
  • It is zero-dependency. No additional library is needed; the reviver is a native capability of JSON.parse defined in the ECMAScript specification.
  • It is composable. The reviver can be extended to block additional dangerous keys (e.g., "__defineGetter__", "__defineSetter__") without changing the surrounding logic.
  • It preserves all legitimate data. The reviver only strips the two specifically dangerous keys; all other keys pass through unchanged, so buildMapFromSpec receives a clean, structurally equivalent object.

Prevention & Best Practices

1. Always Use a Reviver When Parsing Untrusted JSON

Never call JSON.parse on untrusted input without a reviver or a schema validator. A minimal safe reviver looks like this:

const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function safeReviver(key, value) {
  if (DANGEROUS_KEYS.has(key)) return undefined;
  return value;
}

const parsed = JSON.parse(untrustedString, safeReviver);

2. Consider secure-json-parse or destr for Shared Libraries

For libraries consumed by many downstream projects, consider using a dedicated safe-parse library:

  • secure-json-parse — drops dangerous keys and optionally throws
  • destr — a fast, safe alternative to JSON.parse

3. Validate Against a Schema After Parsing

Even with a safe reviver, validate the parsed object against a known schema using a library like zod, ajv, or typebox. Schema validation catches unexpected shapes that could still trigger logic bugs downstream:

import { z } from 'zod';

const SpecSchema = z.object({
  nodes: z.array(z.object({ id: z.string() })),
  edges: z.array(z.object({ source: z.string(), target: z.string() })),
});

const spec = SpecSchema.parse(JSON.parse(_t, safeReviver));

4. Freeze Object.prototype in Critical Environments

In environments where you control the entire runtime, you can add a defense-in-depth measure:

Object.freeze(Object.prototype);

This prevents any code from adding properties to Object.prototype at runtime. Note that this may break some third-party libraries that rely on prototype augmentation.

5. Use Object.create(null) for Data Bags

When creating objects that are used purely as data containers (maps, dictionaries), use Object.create(null) instead of {}. These objects have no prototype and are therefore immune to prototype pollution:

const dataMap = Object.create(null);
dataMap['key'] = 'value'; // safe — no prototype chain

6. Lint for Unsafe JSON Parsing

Add a Semgrep rule to your CI pipeline to flag request.json() or bare JSON.parse calls on request bodies:

rules:
  - id: unsafe-json-parse
    pattern: JSON.parse($INPUT)
    message: "Use JSON.parse with a reviver to prevent prototype pollution"
    severity: WARNING
    languages: [javascript, typescript]

OWASP & CWE References

  • CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
  • OWASP A03:2021 — Injection (prototype pollution is a form of injection into the object model)
  • OWASP Input Validation Cheat Sheet — always validate and sanitize parsed data structures

Key Takeaways

  • request.json() is not safe for untrusted input — in worker/import-core.js, it silently passed __proto__ and constructor keys straight into application logic without any filtering.
  • The reviver function in JSON.parse is the correct fix — it strips dangerous keys before the object is constructed in memory, not after.
  • Prototype pollution in a shared library is a supply-chain risk — because import-core.js is consumed by downstream projects, a single exploitable endpoint could pollute the prototype in every consumer's runtime.
  • Authorization checks are not a sufficient defense — the vulnerability exists even for authenticated users; any valid token holder (or a misconfigured instance with no token) could trigger it.
  • buildMapFromSpec was one function call away from processing poisoned input — the lack of a sanitization boundary between the HTTP layer and application logic is the root architectural mistake.

How Orbis AppSec Detected This

  • Source: The HTTP request body received by handleImport in worker/import-core.js, parsed via request.json() at line 25.
  • Sink: The parsed spec object passed directly into buildMapFromSpec(spec) at line 26, where attacker-controlled keys including __proto__ and constructor could influence object construction.
  • Missing control: No reviver function, no schema validation, and no key filtering between the HTTP layer and application logic. The raw output of request.json() was used without any sanitization boundary.
  • CWE: CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
  • Fix: Replaced request.json() with request.text() followed by JSON.parse(rawText, (k, v) => (k === '__proto__' || k === 'constructor') ? undefined : v), stripping dangerous keys at parse time.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

Prototype pollution is one of those vulnerabilities that looks harmless in isolation — after all, it's "just" a JSON parsing call — but its blast radius is enormous. By mutating Object.prototype, an attacker can corrupt the behavior of every plain object in the runtime, bypass authorization logic, cause denial of service, and in some environments escalate to remote code execution. In worker/import-core.js, the handleImport function was one crafted HTTP request away from exposing the entire application runtime to this risk.

The fix is elegant in its simplicity: a five-word reviver function (k, v) => dangerous ? undefined : v placed at the exact point where untrusted data enters the system. This is the right pattern — sanitize at the source, before the data touches any application logic.

If you're writing Node.js services that parse JSON from HTTP requests, audit every request.json() and bare JSON.parse() call in your codebase. Ask whether the parsed object could contain __proto__ or constructor keys, and whether your downstream code is protected against prototype mutation. In most cases, it isn't — and the fix is exactly as simple as the one demonstrated here.


References

Frequently Asked Questions

What is prototype pollution?

Prototype pollution is an attack where an attacker injects or modifies properties on JavaScript's `Object.prototype`, causing those properties to appear on every object in the runtime, potentially enabling privilege escalation, denial of service, or remote code execution.

How do you prevent prototype pollution in Node.js?

Use a JSON reviver function with `JSON.parse` that returns `undefined` for keys like `__proto__` and `constructor`, or use a library like `destr` or `secure-json-parse`. You can also use `Object.create(null)` for data objects and freeze the prototype with `Object.freeze(Object.prototype)`.

What CWE is prototype pollution?

Prototype pollution is classified as CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').

Is input validation alone enough to prevent prototype pollution?

Not always. String-level validation may miss nested `__proto__` keys in deeply structured JSON. The safest approach is to filter dangerous keys at the `JSON.parse` level using a reviver function, which intercepts keys before any object is created in memory.

Can static analysis detect prototype pollution?

Yes. Tools like Semgrep, CodeQL, and Snyk can detect patterns where `JSON.parse` or equivalent calls process untrusted input without a reviver or schema validation. Orbis AppSec's multi-agent AI scanner flagged this exact pattern in `worker/import-core.js`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #6

Related Articles

critical

How Server-Side Template Injection happens in Node.js EJS and how to fix it

CVE-2022-29078 is a critical server-side template injection (SSTI) vulnerability in EJS versions prior to 3.1.7, where the `outputFunctionName` option is passed directly into generated code without sanitization, allowing attackers to execute arbitrary JavaScript on the server. The fix upgrades the EJS dependency from 2.7.4 to 3.1.7+ (resolved here as 6.0.1), eliminating the unsafe code generation path. Any Node.js application rendering EJS templates with user-influenced options is at risk of ful

high

How Prototype Pollution happens in JavaScript via defu and how to fix it

CVE-2026-35209 is a high-severity prototype pollution vulnerability in the `defu` JavaScript library (versions prior to 6.1.5), where a crafted `__proto__` key in the defaults argument can corrupt the global Object prototype. The fix upgrades `defu` from 6.1.4 to 6.1.5 in `pnpm-lock.yaml` and enforces the version via a workspace override, closing the attack surface in production code that depends on `defu` for deep object merging.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript

high

How Unsafe eval() in JavaScript Happens in React Components and How to Fix It

A high-severity code injection vulnerability was discovered in `TurnPlanner.tsx`, where the `parseInputExpr` function used JavaScript's `Function` constructor — effectively `eval()` — to evaluate user-provided mathematical expressions. The regex guard in place only checked for the presence of arithmetic operators, not whether the input was safe to execute, leaving the door open for arbitrary JavaScript injection. A targeted whitelist fix was applied to reject any input containing characters outs

high

How Prototype Pollution happens in Node.js and how to fix it

A high-severity prototype pollution vulnerability (CVE-2020-8203) was identified in the lodash library via the `zipObjectDeep` function, present as a transitive dependency through postcss in the project's `yarn.lock`. The fix upgrades postcss from 8.5.8 to 8.5.12 using a Yarn resolution override, eliminating the vulnerable lodash code path and reducing the attack surface against crafted CSS input. This change protects the application from object prototype manipulation that could lead to informat

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr