Manual Testing for JavaScript Prototype Pollution
Prototype pollution is a serious security vulnerability in JavaScript applications, where an attacker is able to inject or modify properties on the Object prototype. This can lead to unexpected behavior, denial of service, or even remote code execution depending on how the polluted properties are used. In this deep-dive post, we’ll explore the mechanics of JavaScript prototype pollution, demonstrate manual testing techniques during web-application penetration tests, and provide code examples to help you both understand and detect these vulnerabilities.
Understanding Prototype Pollution
JavaScript uses prototypes to allow objects to inherit properties from one another. The Object.prototype
is the base prototype that nearly all objects inherit from. When this prototype is polluted—i.e., when an attacker can modify it by injecting new properties—the changes propagate to all objects. This can be exploited to:
- Alter application logic by overriding expected values.
- Bypass security controls that rely on default property values.
- Cause unexpected behavior or system crashes.
The Mechanics
In many cases, web applications accept JSON input and merge it with existing objects without proper validation. For instance, using functions like Object.assign()
, _.merge()
from lodash, or similar object merging techniques can lead to pollution if they inadvertently allow control over the prototype chain.
Common Vulnerable Patterns
Unrestricted JSON Merging:
Applications that blindly merge user-supplied JSON data with internal objects can be exploited. For example:const payload = JSON.parse(userInput); const config = Object.assign({}, defaultConfig, payload);
If
userInput
contains a key like"__proto__"
, it might allow changes to the prototype of the resulting object.Improper Sanitization of User Input:
Any operation that does not sanitize object keys can be a potential vector. Libraries that perform deep merges without filtering special keys are common culprits.Dynamic Property Access:
Code that dynamically accesses properties based on user-supplied keys may inadvertently expose prototype properties to manipulation.
Manual Testing Methodology
When performing manual penetration tests, follow these steps to determine if an application is vulnerable to prototype pollution:
1. Identify Potential Injection Points
- JSON Endpoints: Look for endpoints that accept JSON data, especially where the data is merged with other objects.
- Parameter Pollution: Check if URL parameters, form data, or headers are used to build objects dynamically.
- Configuration Objects: Identify functions that use object merging for configuration purposes.
2. Craft a Prototype Pollution Payload
A typical payload for testing prototype pollution might try to add a new property to the Object.prototype
. For example:
{
"__proto__": {
"polluted": "Yes, polluted!"
}
}
3. Inject and Observe
Using Interception Tools:
Tools like Burp Suite can help intercept and modify requests. Inject the payload into JSON bodies or parameters that seem to be merged with internal objects.Testing in a Controlled Environment:
If you have a local copy of the target code or a safe test environment, simulate the merge operation:let targetObject = {}; let payload = JSON.parse('{"__proto__": {"polluted": "Yes, polluted!"}}'); Object.assign(targetObject, payload); console.log({}.polluted); // Expected output: "Yes, polluted!"
If the output confirms that the
polluted
property exists on a new object literal ({}.polluted
), it indicates that the prototype was successfully polluted.
4. Validate the Impact
Functional Testing:
After injecting the payload, explore the application’s functionality. Look for areas where the polluted property might alter control flow, authentication, or other sensitive operations.Code Review:
Examine the source code (if available) for use of object merging functions. Pay attention to any use of libraries like lodash, where functions like_.merge
have historically been vulnerable.
Code Examples and Demonstrations
Example 1: Basic Prototype Pollution Using Object.assign()
Below is a simple demonstration showing how the pollution occurs:
// Step 1: Create an empty object.
let victim = {};
// Step 2: Define a payload with a __proto__ key.
let payload = JSON.parse('{"__proto__": {"polluted": "Yes, polluted!"}}');
// Step 3: Merge payload into the victim object.
Object.assign(victim, payload);
// Step 4: Check if the pollution succeeded.
if ({}.polluted) {
console.log("Prototype polluted:", {}.polluted); // Expected output: "Yes, polluted!"
} else {
console.log("Prototype not polluted.");
}
Explanation:
- The payload injects a new property
polluted
into__proto__
. - When merging with
Object.assign
, the prototype of all objects is affected. - Testing with a fresh object literal confirms the pollution.
Example 2: Exploiting Deep Merge Vulnerabilities
Many libraries perform deep merges. Consider a function that recursively merges objects:
function deepMerge(target, source) {
for (let key in source) {
if (source[key] && typeof source[key] === 'object') {
if (!target[key]) {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
let victim = {};
let payload = JSON.parse('{"__proto__": {"polluted": "Deep polluted!"}}');
deepMerge(victim, payload);
if ({}.polluted) {
console.log("Prototype polluted via deep merge:", {}.polluted); // Expected output: "Deep polluted!"
} else {
console.log("Prototype not polluted.");
}
Explanation:
- A custom recursive merge function is used.
- The payload injects a property into
__proto__
deep within the object hierarchy. - Testing confirms the pollution in a deep merge scenario.
Example 3: Testing via HTTP Request Interception
When testing against a live application endpoint, you might use an interception proxy like Burp Suite to modify JSON payloads. For example, if an endpoint at /api/update-config
accepts JSON, you might send:
{
"setting": "value",
"__proto__": {
"admin": true
}
}
After the request is processed, verify in the application if any new behavior (such as elevated privileges or configuration changes) is observed that could be attributed to the prototype pollution.
Steps to Validate:
- Intercept a legitimate JSON request to
/api/update-config
. - Modify the JSON body with the payload above.
- Forward the request and observe the server response.
- Use any available functionality or additional testing endpoints to check if the
admin
flag or any other polluted property influences application behavior.
Mitigation and Remediation Considerations
While our focus is on manual testing, it’s crucial to understand mitigation strategies:
Input Sanitization:
Filter out keys such as__proto__
,constructor
, andprototype
before merging user-supplied data.Use Safe Merging Libraries:
Libraries likemerge-options
or patched versions oflodash
offer safer merge functions.Immutable Data Structures:
Where possible, avoid in-place mutations of objects that can affect the global prototype chain.Code Audits:
Regularly review code for any dangerous patterns, especially when accepting and merging JSON input.
Conclusion
Prototype pollution remains a potent vulnerability in modern JavaScript applications. By understanding its mechanics and knowing how to manually test for it, penetration testers can effectively identify and report these issues before they are exploited. This article has provided a comprehensive guide with code examples and practical testing methodologies suitable for a highly technical audience.
Happy pentesting!