# Prototype pollution via recursive merge

Untrusted keys reaching recursive merge, deep-set, or bracket writes without blocking __proto__ and constructor

_Area: Web injection and code execution. Seen in 4 disclosed reports; latest 2026-10-22. Updated 2026-10-27._

## How it works
A merge or deep-set helper walks attacker-supplied keys and writes each one onto a target object. The assumption that breaks is that keys are just data. Keys like `__proto__`, `constructor`, `prototype`, `class`, or `superclass` are not ordinary names: assigning them reaches the object's prototype chain or class-level state instead of adding a field. Once that state is poisoned, every later lookup in the process can see the attacker's value. The same root cause appears in Ruby, where recursive merges call `instance_variable_set`/`attr_accessor` on attacker keys and recurse through `respond_to?`/`public_send`, so keys like `class` or `superclass` escape the object into class state [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md).

## Where it shows up
- Recursive merge, `deep_merge`, deep-set, or `copyObject`-style helpers fed JSON or parsed config from users.
- Ruby merge helpers passing keys to `instance_variable_set`/`attr_accessor` and recursing via `respond_to?`/`public_send` [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md).
- Parsers that serialize key paths to strings and compare them, where quoting or escaping is incomplete [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-h895-cgqg-jf52.md).
- Unguarded `for...in` over untrusted keys used for bracket reads and writes on a plain `{}` [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md).
- Template engines that resolve property paths from templates, on both the write and read side [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md).

## How to review
Find every place an untrusted key becomes a property name. The invariant: no key from untrusted input may name `__proto__`, `constructor`, `prototype`, or (in Ruby) `class`/`superclass`, and no write may land on a shared prototype or class object. Check that the guard covers the terminal container, not only intermediate hops [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-h895-cgqg-jf52.md), and that it applies to reads as well as writes [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md). Where a sibling helper already blocks these keys, confirm the same guard exists in every copy of the pattern [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md).

## Variants seen
- Ruby class pollution through recursive merge reaching instance, parent-class, and unrelated-class state [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md).
- Key-path desynchronization: unescaped quotes make distinct paths compare equal, clearing an ownership guard [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-h895-cgqg-jf52.md).
- Read-path bypass: guard added only to assignment target, property reads still traverse `constructor.constructor` [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md).
- Unguarded `for...in` plus bracket write, with a guard present in a sibling function but not propagated [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md).

## Why fixes fail
E2 shows an ownership check that covered intermediate hops but not the terminal container, leaving the same desync reachable via a different delimiter. E3 shows a key blocklist applied only to the write path while the read path stayed open. E4 shows a guard in one helper never copied to its sibling.

## Impact and severity
Outcomes in evidence: authentication bypass and remote code execution in Ruby [E1](https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md), `Object.prototype` pollution from a short TOML document [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-h895-cgqg-jf52.md), arbitrary command execution via template property reads [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md), and denial of service of every library call [E4](https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md). Severity is high where polluted state reaches code execution or auth checks; worse when the poisoned object is process-wide and long-lived.

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2024-10-02-class-pollution-ruby-html.md
- E2: https://bugbountylibrary.com/r/ghsa%3AGHSA-h895-cgqg-jf52.md
- E3: https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3.md
- E4: https://bugbountylibrary.com/r/ghsa%3AGHSA-73wf-gq98-2v4g.md
