# Prototype pollution via unguarded key merge

Untrusted keys reaching merge, for-in copy, or bracket read that walk to prototype or Function

_Area: Web injection and code execution. Seen in 4 disclosed reports; latest 2026-09-01. Updated 2026-11-01._

## How it works
Code treats an attacker-supplied key as an inert data name, but the language resolves it through the object model. A recursive merge, a `for...in` copy, or a bracket read like `base[key]` follows keys such as `__proto__`, `constructor`, or `prototype` into shared state: `Object.prototype`, a parent class, or the `Function` constructor. In Ruby, merge helpers that call `instance_variable_set`/`attr_accessor` on attacker keys and recurse via `respond_to?`/`public_send` can be walked through keys like `class` and `superclass` to escape the object into class-level state [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md). The broken assumption is that keys name data; some keys name the machinery holding the data, so writing or reading them escapes the intended container.

## Where it shows up
- Recursive merge helpers passing attacker keys to `instance_variable_set`/`attr_accessor`, recursing via `respond_to?`/`public_send` [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md)
- `for...in` over untrusted JSON/stats objects, then bracket read or assignment on a plain `{}` [E2](https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-01-26-hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge.md)[E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md)
- Property-read expression evaluators indexing `base[property]` with no key filter [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md)
- Guards (`isBlockedPathKey`, `__proto__` checks) present in one function but not its siblings or its read path [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md)[E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md)

## How to review
Find every place an untrusted key is used for lookup or assignment. The invariant: a key from input must never resolve to a prototype, constructor, class, or function reference, and must never be written outside the intended container. Check that:
- blocked-key filtering covers both write and read paths, and every sibling helper doing the same pattern [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md)[E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md)
- non-string values (arrays, objects) cannot slip past validation that only rejects strings, then be coerced later at a string sink [E2](https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-01-26-hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge.md)

## How finders spotted it
- Reasoned from JS/Python prototype pollution that Ruby's merge primitives could be walked via `class`/`superclass`, then tested real libraries [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md)
- After top-down tracing stalled, worked bottom-up from a dangerous sink, grepped internal calls, and traced callers back to the entry point [E2](https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-01-26-hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge.md)
- Noticed a prior advisory's guard lived only in the assignment path, then traced value evaluation to an unfiltered read [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md)
- Saw one helper already guarded `__proto__`/`constructor`/`prototype`, then grepped for siblings repeating the pattern unguarded [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md)

## Variants seen
- Object-level attribute pollution escalating to class-variable pollution across unrelated classes [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md)
- Inherited enumerable properties picked up by `for...in`, then coerced to HTML at a string-concatenation sink [E2](https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-01-26-hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge.md)
- Property read reaching `constructor.constructor` to obtain `Function` [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md)
- `__proto__` assignment plus inherited-name crashes in a stats normalizer [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md)

## Why fixes fail
Guards are applied narrowly: only to the assignment target, not the value expression [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md); only to one helper, not its siblings [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md).

## Impact and severity
Ranges from denial of service on any library call [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md) to auth bypass, signing-key forgery and SSRF [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md), and arbitrary command execution via `Function` [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md). Impact rises when pollution persists across requests [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md) or chains with a second gadget (Axios pollution plus a jQuery sink) to reach 0-click XSS [E2](https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-01-26-hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge.md).

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md
- E2: https://bugbountylibrary.com/r/blog%3Ajoaxcar-com-blog-2024-01-26-hunting-for-prototype-pollution-gadgets-in-jquery-intigriti-0124-challenge.md
- E3: https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md
- E4: https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md
