# CVE-2026-32621 — Structure Diagrams ## Attack Flow Overview ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ CVE-2026-32621 Attack Flow │ │ │ │ Attacker Apollo Gateway Subgraph(s) │ │ ──────── ────────────── ────────── │ │ │ │ 1. Send GraphQL ──────────────► │ │ query with 2. Forward query ──────► │ │ __proto__ alias to subgraph(s) │ │ │ │ 3. Subgraph returns ◄─── │ │ JSON with __proto__ │ │ as own property │ │ │ │ 4. Gateway calls ◄──────────────────── │ │ deepMerge() to │ │ merge responses │ │ │ │ 5. deepMerge iterates │ │ Object.keys(source) │ │ → finds "__proto__" │ │ │ │ 6. target["__proto__"] │ │ → resolves to │ │ Object.prototype │ │ │ │ 7. deepMerge writes to ◄──────── │ │ Object.prototype │ │ → POLLUTED! │ │ │ │ 8. All subsequent ◄──────────── │ │ requests inherit │ │ polluted properties │ │ (isAdmin, polluted, etc.) │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## deepMerge Vulnerability Mechanism ``` deepMerge(target, source) ───────────────────────── Source Object (from JSON.parse) Target Object (accumulated data) ┌──────────────────────┐ ┌──────────────────────┐ │ { │ │ { │ │ data: { │ │ data: { │ │ products: [...], │ │ products: [...], │ │ __proto__: { ◄───┤ OWN PROPERTY │ (no __proto__) │ │ polluted: true, │ from │ } │ │ isAdmin: true │ JSON.parse │ } │ │ } │ └──────────────────────┘ │ } │ │ │ } │ │ └──────────────────────┘ │ │ │ ▼ ▼ Object.keys(source) for each key in source: → ["data"] key = "data" target["data"] exists? → YES, recurse: deepMerge(target.data, source.data) Inside deepMerge(target.data, source.data): ┌─────────────────────────────────────────────────────────────────┐ │ Object.keys(source.data) = ["products", "__proto__"] │ │ │ │ key = "products" → normal merge (no issue) │ │ │ │ key = "__proto__": │ │ target.data["__proto__"] │ │ ┌──────────────────────────────────────────┐ │ │ │ JavaScript prototype chain lookup: │ │ │ │ │ │ │ │ target.data (plain object) │ │ │ │ └── __proto__ → Object.prototype │ │ │ │ │ │ │ │ So target.data["__proto__"] │ │ │ │ = Object.prototype │ │ │ └──────────────────────────────────────────┘ │ │ │ │ source.data["__proto__"] = { polluted: true, isAdmin: true } │ │ isObject? → YES │ │ │ │ → deepMerge(Object.prototype, { polluted: true, isAdmin: true })│ │ │ │ Inside deepMerge(Object.prototype, source): │ │ key = "polluted" → Object.prototype.polluted = true ◄── POLLUTED!│ │ key = "isAdmin" → Object.prototype.isAdmin = true ◄── POLLUTED!│ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## The Fix: defineOwn() ``` PATCHED deepMerge with defineOwn(): ──────────────────────────────────── for (const key of Object.keys(source)) { defineOwn(target, key); ◄── THE FIX ... } defineOwn(target, "__proto__"): ┌──────────────────────────────────────────────────────┐ │ │ │ Check: hasOwn(target, "__proto__")? → NO │ │ Check: "__proto__" in target? → YES (via prototype) │ │ │ │ → Object.defineProperty(target, "__proto__", { │ │ configurable: true, │ │ enumerable: true, │ │ value: undefined, ◄── Shadows prototype! │ │ writable: true, │ │ }) │ │ │ │ Now target["__proto__"] = undefined (own property) │ │ NOT Object.prototype │ │ │ │ → deepMerge(undefined, source["__proto__"]) │ │ → source is not null/undefined check fails │ │ → target["__proto__"] = source["__proto__"] │ │ → Writes to own property, NOT prototype │ │ │ └──────────────────────────────────────────────────────┘ ``` --- ## Component Architecture ``` ┌─────────────────────────────────────────────────────────────────────┐ │ PoC Exploit Architecture │ │ │ │ ┌─────────────┐ ┌───────────────┐ ┌──────────────────────┐ │ │ │ exploit.js │ │ setup_vulnerable│ │ test_exploit.js │ │ │ │ │ │ .js │ │ │ │ │ │ • 5 attack │ │ • HTTP server │ │ • 15 unit tests │ │ │ │ vectors │ │ • Vulnerable │ │ • Vulnerable vs │ │ │ │ • Local demo │──►│ deepMerge │ │ patched comparison │ │ │ │ • Remote │ │ • Subgraph │ │ • Pollution verify │ │ │ │ exploit │ │ simulation │ │ • Fix verification │ │ │ └─────────────┘ └───────────────┘ └──────────────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ e2e_test.js │ │ │ │ │ │ │ │ 1. Start gateway subprocess │ │ │ │ 2. Send exploit payloads via HTTP │ │ │ │ 3. Verify pollution in gateway response │ │ │ │ 4. Verify cross-request persistence │ │ │ │ 5. Run unit tests in subprocess │ │ │ │ 6. Report pass/fail summary │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Attack Vector Comparison ``` ┌────────────────┬──────────────┬───────────────┬──────────────────────┐ │ Vector │ Source │ Requires │ Pollution │ │ │ │ Subgraph │ Persistence │ ├────────────────┼──────────────┼───────────────┼──────────────────────┤ │ Field Alias │ Client-side │ No (any) │ Process-wide │ │ (__proto__) │ │ │ │ ├────────────────┼──────────────┼───────────────┼──────────────────────┤ │ Variable Names │ Client-side │ No (any) │ Process-wide │ │ (__proto__) │ │ │ │ ├────────────────┼──────────────┼───────────────┼──────────────────────┤ │ Nested Alias │ Client-side │ No (any) │ Process-wide │ │ (constructor. │ │ │ │ │ prototype) │ │ │ │ ├────────────────┼──────────────┼───────────────┼──────────────────────┤ │ Subgraph │ Subgraph-side│ Yes (control │ Process-wide │ │ Response │ │ subgraph) │ │ ├────────────────┼──────────────┼───────────────┼──────────────────────┤ │ Direct │ Local │ N/A │ Process-wide │ │ deepMerge │ │ │ │ └────────────────┴──────────────┴───────────────┴──────────────────────┘ ``` --- ## Mermaid Diagram ```mermaid flowchart TD A[Attacker] -->|GraphQL query with __proto__ alias| B[Apollo Gateway] B -->|Forward query| C[Subgraph] C -->|JSON response with __proto__ as own property| B B --> D[deepMerge - merge subgraph responses] D --> E{Object.keys source} E -->|__proto__ key found| F[target.__proto__] F -->|Prototype chain lookup| G[Object.prototype] D -->|Recursion: deepMerge Object.prototype, source| H[Object.prototype.polluted = true] D -->|Recursion: deepMerge Object.prototype, source| I[Object.prototype.isAdmin = true] H --> J[ALL objects in process inherit polluted properties] I --> J J --> K[Privilege Escalation] J --> L[Denial of Service] J --> M[Data Integrity Violation] style G fill:#ff4444,color:#fff style H fill:#ff4444,color:#fff style I fill:#ff4444,color:#fff style J fill:#ff6600,color:#fff ``` --- ## Network Traffic Flow ``` Client Gateway Subgraph │ │ │ │ POST /graphql │ │ │ { "query": │ │ │ "query { │ │ │ __proto__: │ │ │ products { id } │ │ │ }" │ │ │ } │ │ │ ──────────────────────► │ │ │ │ │ │ │ POST /graphql │ │ │ { "query": │ │ │ "{ products { id } }" │ │ │ } │ │ │ ─────────────────────────► │ │ │ │ │ │ 200 OK │ │ │ { "data": { │ │ │ "products": [...] │ │ │ } } │ │ │ ◄───────────────────────── │ │ │ │ │ │ deepMerge( │ │ │ target, │ │ │ subgraphResponse) │ │ │ → Object.prototype │ │ │ .polluted = true │ │ │ ⚠️ POLLUTED! │ │ │ │ │ 200 OK │ │ │ { "data": {...}, │ │ │ "extensions": { │ │ │ "polluted": true │ │ │ } } │ │ │ ◄────────────────────── │ │ │ │ │ │ │ ┌──────────────────────┐ │ │ │ │ Object.prototype now │ │ │ │ │ has: │ │ │ │ │ .polluted = true │ │ │ │ │ .isAdmin = true │ │ │ │ │ │ │ │ │ │ Affects ALL future │ │ │ │ │ requests to gateway │ │ │ │ └──────────────────────┘ │ │ │ │ ```