How Prototype Pollution Happens in Node.js protobufjs and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | Prototype Pollution |
| CVE | CVE-2023-36665 |
| CWE | CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes |
| Severity | Critical |
| Affected Package | protobufjs 6.11.3 (and < 7.2.5 in the v7 branch) |
| Fixed In | protobufjs 6.11.4 / 7.2.5 |
| Language | JavaScript / Node.js |
Introduction
The package-lock.json in this project pinned protobufjs to version 6.11.3 — a version that contains a critical prototype pollution vulnerability. Any code path that deserializes a user-supplied protobuf message using this library becomes an entry point for an attacker to silently overwrite properties on Object.prototype, the root of JavaScript's object inheritance chain. Because prototype properties are inherited by every object in a Node.js process, a single malicious message can alter the behavior of unrelated parts of the application without any obvious error being thrown.
The vulnerability was assigned CVE-2023-36665 and rated Critical. It was detected by Trivy scanning the project's package-lock.json dependency tree.
The Vulnerability Explained
What Is Prototype Pollution?
In JavaScript, every object inherits properties from Object.prototype. If an attacker can cause your code to execute something equivalent to:
obj["__proto__"]["isAdmin"] = true;
…then every object in the running process suddenly has isAdmin === true, because they all share the same prototype. This is prototype pollution.
How protobufjs 6.11.3 Was Vulnerable
Protocol Buffers (protobuf) is a binary serialization format developed by Google. The protobufjs library parses .proto schema definitions and decodes binary or JSON-encoded protobuf messages into JavaScript objects. During decoding, the library maps protobuf field names to JavaScript object keys.
In protobufjs versions before 6.11.4, the deserialization logic did not adequately guard against field names that are special in JavaScript's prototype chain — specifically keys like:
__proto__constructorprototype
If an attacker crafts a protobuf message (or a JSON-encoded protobuf payload) containing one of these reserved names as a field, the library would process it and assign the attacker-controlled value directly onto the target object using bracket notation — effectively walking up the prototype chain rather than setting an own property.
A simplified illustration of the dangerous pattern inside the library's object-building logic (before the fix):
// Vulnerable pattern — no key sanitization
function setProperty(obj, key, value) {
obj[key] = value; // If key is "__proto__", this pollutes Object.prototype
}
When key is "__proto__" and value is { "isAdmin": true }, the assignment obj["__proto__"] = { "isAdmin": true } modifies the prototype of obj, not a property named __proto__. In V8 (Node.js's JavaScript engine), this propagates to Object.prototype itself.
Concrete Attack Scenario
Consider an application that:
1. Accepts binary or JSON protobuf messages from users over an HTTP API.
2. Uses protobufjs 6.11.3 to decode those messages.
3. Later checks if (request.user.isAdmin) to gate privileged operations.
An attacker sends a crafted protobuf message with a field named __proto__ containing { "isAdmin": true }. After protobufjs decodes this message, Object.prototype.isAdmin is now true for the entire process lifetime. Every subsequent request.user.isAdmin check — even for unauthenticated users — returns true, granting full administrative access.
Beyond privilege escalation, prototype pollution can be chained with other vulnerabilities (e.g., property injection into child_process.spawn option objects) to achieve Remote Code Execution.
What Was Pinned in This Project
The vulnerable version was declared in two places in package-lock.json and package.json:
// package.json — before fix
"protobufjs": "^6.11.2"
// package-lock.json — node_modules/protobufjs — before fix
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
"integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg=="
Because the semver range was ^6.11.2 (which resolves to >=6.11.2 <7.0.0), the lock file had resolved to 6.11.3 — the vulnerable release. The range would allow 6.11.4, but the lock file pinned the older version until explicitly updated.
The Fix
The fix upgrades protobufjs from 6.11.3 to 6.11.4 in both package.json and package-lock.json. The 6.11.4 release adds sanitization of user-controlled field names during protobuf message deserialization, rejecting or neutralizing keys that would traverse the prototype chain.
Before and After: package.json
- "protobufjs": "^6.11.2",
+ "protobufjs": "^6.11.4",
Bumping the lower bound of the range to ^6.11.4 ensures that npm install will never again resolve to a vulnerable patch version within the 6.x series.
Before and After: package-lock.json (node_modules/protobufjs)
- "version": "6.11.3",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
- "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
+ "version": "6.11.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
+ "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
"hasInstallScript": true,
+ "license": "BSD-3-Clause",
The integrity hash change is critical — it confirms that npm will now verify the downloaded tarball against the hash of the patched release, not the vulnerable one. Notably, 6.11.4 also formally declares its BSD-3-Clause license in the lock file metadata, a minor housekeeping improvement included in that release.
The same version bump appears in the flattened dependencies section of package-lock.json (the legacy npm v2 format section), ensuring consistency across both lock file representations:
"protobufjs": {
- "version": "6.11.3",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
- "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
+ "version": "6.11.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
+ "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
Why Both Files Must Be Updated
Many developers assume that changing package.json alone is sufficient. It is not. The package-lock.json is the authoritative source of truth for npm ci (used in CI/CD pipelines). If only package.json is updated, a npm ci run in a deployment pipeline will still install 6.11.3 because it reads the exact version from the lock file. Updating both files is mandatory to guarantee the fix takes effect in all environments.
Prevention & Best Practices
1. Treat Deserialized Data as Untrusted Input
Any data arriving from outside your process boundary — HTTP bodies, WebSocket frames, message queue payloads — must be treated as potentially adversarial. This is especially true for rich deserialization formats like Protocol Buffers, which can encode complex nested structures.
2. Sanitize Keys Before Dynamic Property Assignment
When you must assign user-controlled keys to objects, guard against prototype-polluting names:
const DISALLOWED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
function safeSet(obj, key, value) {
if (DISALLOWED_KEYS.has(key)) {
throw new Error(`Rejected potentially dangerous key: ${key}`);
}
obj[key] = value;
}
Or use Object.create(null) to create property bags with no prototype:
const safeMap = Object.create(null);
safeMap[userKey] = userValue; // No prototype to pollute
3. Lock and Audit Your Dependency Tree Regularly
The vulnerability in this project existed because the lock file pinned 6.11.3 while a patched 6.11.4 was available. Integrate dependency scanning into your CI pipeline:
# Audit with npm
npm audit
# Scan with Trivy (as used in this project)
trivy fs --scanners vuln package-lock.json
4. Use Object.freeze(Object.prototype) in Security-Sensitive Contexts
For applications where prototype pollution would be catastrophic, you can freeze the prototype at startup:
// Add to your application entry point
Object.freeze(Object.prototype);
Object.freeze(Object);
This causes prototype pollution attempts to fail silently (or throw in strict mode) rather than succeeding. Note that some third-party libraries may break if they legitimately extend Object.prototype.
5. Keep Semver Lower Bounds Up to Date
The original range "^6.11.2" allowed vulnerable versions. After a security fix, update the lower bound of your range to exclude all vulnerable versions:
// Before: allows 6.11.2, 6.11.3 (vulnerable)
"protobufjs": "^6.11.2"
// After: minimum is the patched version
"protobufjs": "^6.11.4"
Security Standards Reference
- OWASP A08:2021 — Software and Data Integrity Failures covers dependency vulnerabilities like this one.
- CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') is the authoritative classification.
- OWASP Dependency-Check and npm audit are the recommended first-line tools for Node.js projects.
Key Takeaways
protobufjs6.11.3 treats user-controlled field names as trusted object keys — any field named__proto__in a parsed message can overwriteObject.prototypefor the entire process.- Both
package.jsonandpackage-lock.jsonmust be updated — updating onlypackage.jsonleavesnpm cideployments still installing the vulnerable6.11.3. - The integrity hash change from
sha512-xL96...tosha512-5kQW...is the cryptographic proof that the patched tarball is now being used, not the vulnerable one. - Prototype pollution via deserialization is silent — it does not throw errors, making it especially dangerous in production systems where observability depends on normal exception handling.
- Semver ranges like
^6.11.2do not automatically protect you — the lock file freezes the resolved version until you explicitly regenerate it with a patched release.
How Orbis AppSec Detected This
- Source: User-controlled binary or JSON protobuf message payload supplied to
protobufjsdeserialization logic in any code path importing theprotobufjspackage declared inpackage-lock.json. - Sink: The
protobufjsinternal object-building routine that assigns decoded field names as JavaScript object keys without sanitizing prototype-polluting names (__proto__,constructor,prototype), present innode_modules/protobufjsversion6.11.3. - Missing control: No validation or rejection of field names that correspond to JavaScript prototype chain properties before dynamic property assignment inside the deserialization library.
- CWE: CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').
- Fix: Both
package.jsonandpackage-lock.jsonwere updated to resolveprotobufjsat version6.11.4, which includes internal key sanitization that blocks prototype-polluting field names during message decoding.
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
CVE-2023-36665 is a sharp reminder that deserialization libraries are a high-value attack surface. protobufjs is trusted to faithfully translate binary protobuf data into JavaScript objects — but in version 6.11.3, that trust extended too far, allowing attacker-controlled field names to silently corrupt the JavaScript runtime's prototype chain. The fix is surgical: upgrade to 6.11.4, update both manifest files, and verify the new integrity hash is in place.
More broadly, every library that maps external data to object keys deserves scrutiny. Prototype pollution is notoriously difficult to detect at runtime because it produces no immediate error — its effects manifest elsewhere in the application, often in security checks or configuration reads that suddenly return attacker-controlled values. Automated scanning of your dependency tree, as demonstrated here with Trivy, is the most reliable way to catch these issues before they reach production.
References
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes
- OWASP A08:2021 – Software and Data Integrity Failures
- OWASP Prototype Pollution Prevention Cheat Sheet
- protobufjs npm package — official changelog
- Semgrep rules for prototype pollution
- fix: upgrade protobufjs to 7.2.5, 6.11.4 (CVE-2023-36665)