# Hunting for Prototype Pollution gadgets in jQuery (intigriti 0124 challenge)

New jQuery prototype pollution gadgets via attr({}) and on()/submit() let polluted selector arrays reach jQuery() sink, yielding XSS from a polluted form.

- Payout: Research
- Severity: medium
- Program: jQuery
- Write-up by: Johan Carlsson
- Disclosed: 2024-01-26
- Write-up quality: 8/10
- Original: https://joaxcar.com/blog/2024/01/26/hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge/
- Library page: https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-01-26-hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge
- Tags: area:web domain:web impact:integrity kind:research platform:research sev:medium technique:chained technique:source-review vuln:prototype-pollution vuln:xss

**Target:** jQuery library (attr, on/submit event handling)
**Root cause:** jQuery's for...in over attr objects and event handler objects picks up inherited enumerable properties from Object.prototype; the handlers() sink concatenates handleObj.selector with a string, coercing a polluted array to HTML that jQuery() parses via innerHTML.
**Steps:**
1. Pollute via Axios form-to-JSON: form inputs named `__proto__.selector[]`, `__proto__.handler[]`, `__proto__.delegateType`, `__proto__.needsContext`, `__proto__.owner`.
2. Set `selector` to an array `["<img/src/onerror=alert(1)>"]` so matchesSelector's `typeof selector !== "string"` early-return skips CSS validation.
3. Set `handler` to an array so `handler.handler` is falsy and handleObjIn path is skipped, keeping polluted selector.
4. Set `delegateType` to `focusout` to control trigger event; set `needsContext` to any truthy value to hit `jQuery(sel, this)` branch.
5. Trigger via `$("#search").submit(fn)` or `.on("submit", fn)`; handlers() builds `sel = handleObj.selector + " "` coercing array to HTML string, jQuery() parses it into DOM and fires onerror.
6. 0-click: frame the page, append `#y` to iframe src to focus input, then shift parent focus to trigger focusout.
**How found:** After top-down source-to-sink stepping stalled, switched to bottom-up: identified jQuery()/innerHTML in buildFragment as a sink, grepped internal jQuery() calls, found handlers() using handleObj.selector, traced callers dispatch()->add()->on()/submit(). Blocked by matchesSelector CSS validation until realizing non-string selectors early-return without error, then exploited array-to-string coercion at `sel = handleObj.selector + " "`.
**Exploitation:** Attacker crafts a malicious link with a form whose input names pollute Object.prototype via the Axios form-to-JSON bug; when the victim loads the page, jQuery's submit/on handler registration picks up the polluted selector array, delegateType and needsContext, and the handlers() sink coerces the array to an HTML string parsed by jQuery() into an <img onerror> that executes. Framing plus fragment-selector focus shifting achieves 0-click XSS.
**Escalation:** Chained Axios prototype pollution (form-to-JSON) with jQuery gadget; used iframe framing and fragment-based focus shifting to convert click-required focusout trigger into 0-click XSS.
**Insight:** When a validation function like matchesSelector only errors on strings, pass an array/object to bypass it; then look for later string concatenation that coerces the array back to your payload. Also, for...in over user-supplied objects in libraries enumerates inherited Object.prototype keys, making any such loop a pollution sink.
