Introduction
A critical prototype pollution vulnerability existed in a backend-for-frontend (BFF) proxy edge function that handles requests to a QR code generation endpoint. The flaw was in how the endpoint parsed incoming JSON request bodies: while it performed a basic type check to ensure the body was an object, it did not filter or validate the keys themselves. This meant an attacker could send a JSON payload with keys like __proto__, constructor, or prototype, which JavaScript treats as special handles to the Object prototype. By exploiting this, an attacker could inject arbitrary properties into the global Object prototype, affecting every object created or used by the application for the duration of that request or longer.
This is particularly dangerous in serverless and edge-computing contexts, where function instances may be reused across multiple requests, allowing a single malicious request to potentially poison an instance serving subsequent users.
Affected Versions
| Affected | Not applicable (first-party code) |
| Fixed in | Not applicable (first-party code)—fix applied in PR |
| Ecosystem | Node.js / Netlify Edge Functions |
| CVE / GHSA | Not assigned |
| CWE | CWE-915 (Improperly Controlled Modification of Dynamically-Managed Code Resources) |
The Vulnerability Explained
The vulnerable code was a request handler for a QR code generation endpoint that accepted JSON bodies directly from clients. Here is the problematic logic:
qrBody = await request.json();
if (qrBody === null || typeof qrBody !== 'object') {
throw new Error('body must be a JSON object');
}
The handler checked that qrBody was not null and that its type was 'object', but it performed no validation on the keys inside that object. In JavaScript, certain keys are reserved as prototypes or prototype chain accessors:
__proto__is the dunder-proto property, a direct reference to an object's[[Prototype]]constructoraccesses the constructor function, which itself has aprototypepropertyprototypedirectly references the prototype object
If an attacker sent a request like:
POST /bff/qrcode-generate
Content-Type: application/json
{
"__proto__": {
"isAdmin": true,
"canDelete": true
}
}
The parsed qrBody object would contain these keys. If the downstream code then used this object in any object-oriented operation—such as iterating its keys, assigning it to another object, or using it as a template—the JavaScript runtime would apply the prototype chain modifications. Every subsequent object created in that execution context would inherit the poisoned properties:
const someUser = { name: "alice" };
console.log(someUser.isAdmin); // true — inherited from polluted prototype!
In a QR code generation context, this could be leveraged to:
- Escalate an unprivileged request to admin if role checks use prototype-inherited properties
- Bypass permission checks if canDelete or similar flags are checked against all objects
- Inject malicious functions into the prototype, causing code execution when inherited methods are called
The Fix
The fix adds two critical safeguards:
if (qrBody === null || typeof qrBody !== 'object' || Array.isArray(qrBody)) {
throw new Error('body must be a JSON object');
}
if (['__proto__', 'constructor', 'prototype'].some(key => Object.prototype.hasOwnProperty.call(qrBody, key))) {
throw new Error('body contains forbidden keys');
}
Change 1: Reject arrays.
The first addition checks Array.isArray(qrBody) to ensure the body is a plain object, not an array. While arrays are technically objects in JavaScript, they should not be accepted as request bodies for a QR code endpoint and could be used in alternative prototype pollution attacks.
Change 2: Whitelist-validate keys.
The second addition uses Object.prototype.hasOwnProperty.call(qrBody, key) to check if the parsed body directly owns any of the three forbidden keys. This is critical:
- It checks the object's own properties, not inherited ones, using
hasOwnProperty - It uses the
call()method to ensure we check against the true Object.prototype, not a polluted one - It rejects the entire request if any of these keys are present, failing safely
This approach is whitelist-based: rather than allowing anything and trying to sanitize it, we explicitly define what we will not accept. If a request contains __proto__, it is rejected outright at the entry point, before any downstream code can be affected.
Key Takeaways
-
Prototype pollution is a parser-level threat in JavaScript. Unlike SQL injection or command injection, it doesn't require a downstream sink—the mere presence of a forbidden key in a parsed object can cause harm if that object is ever used in a context where prototype chain semantics apply.
-
Type checks alone are insufficient. Validating that
typeof obj === 'object'does not tell you whether the object's keys are safe. You must validate the keys themselves. -
Use
hasOwnPropertywith.call()for safety. CallinghasOwnPropertydirectly on an untrusted object (e.g.,qrBody.hasOwnProperty('__proto__')) could be subverted if the object itself overrideshasOwnProperty. UsingObject.prototype.hasOwnProperty.call(qrBody, key)ensures you check against the true base Object prototype. -
Reject at the entry point. Catching prototype pollution at the request parsing stage prevents the corrupted object from ever propagating to downstream code, where the damage could be harder to detect or undo.
-
Edge functions and serverless environments are high-risk contexts. If function instances are reused across requests, a prototype pollution attack in one request can poison the environment for subsequent requests from other users.
How Orbis AppSec Detected This
Source: The request.json() method parsing untrusted client-supplied JSON into the qrBody variable in the QR code generation handler.
Sink: Any subsequent use of qrBody in object operations, property assignments, or property checks where prototype chain lookup would occur.
Missing control: No validation of qrBody's keys. The code performed a type check (typeof qrBody === 'object') but did not filter out prototype-manipulation keys before allowing the object to be used.
CWE: CWE-915 — Improperly Controlled Modification of Dynamically-Managed Code Resources.
Fix: Added a post-parse validation check that explicitly rejects request bodies containing the keys __proto__, constructor, or prototype, rejecting the entire request with a 400 error if any are found.
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 a deceptively dangerous vulnerability in JavaScript because it operates at the language semantics level—no special sinks or dangerous functions are required, just the presence of malicious keys in a parsed object. The QR code endpoint's failure to validate input object keys created an entry point for attackers to poison the Object prototype, potentially compromising the integrity of every object in the application.
The fix is surgical and effective: reject requests containing forbidden keys at the parser boundary. This is a reminder that in JavaScript, input validation must consider not just types and values, but the structure and semantics of objects themselves. When parsing untrusted JSON, especially in serverless or edge contexts where function state may persist, always ask: What keys should this object contain, and what keys should it never contain?