/** * Vulnerable Apollo Gateway Setup for CVE-2026-32621 Testing * * This script sets up a minimal Apollo Federation gateway using * vulnerable versions of @apollo/gateway and @apollo/query-planner. * * Requirements: * npm install @apollo/gateway@2.13.1 @apollo/server @apollo/subgraph * npm install graphql express cors * * Or use the included vulnerable deepMerge directly (no install needed). */ 'use strict'; const http = require('http'); /** * Minimal vulnerable gateway simulator. * * Instead of requiring the full Apollo Gateway stack (heavy deps), * this simulates the exact vulnerable code path: deepMerge being called * with subgraph response data that contains __proto__ keys. * * This is functionally identical to what happens in the real gateway * when processing federated query plans. */ class VulnerableGatewaySimulator { constructor(port = 4000) { this.port = port; this.server = null; this.requestCount = 0; } isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } // VULNERABLE deepMerge - no defineOwn call deepMerge(target, source) { if (source === undefined || source === null) return target; for (const key of Object.keys(source)) { if (source[key] === undefined) continue; if (target[key] && this.isObject(source[key])) { this.deepMerge(target[key], source[key]); } else if ( Array.isArray(source[key]) && Array.isArray(target[key]) && source[key].length === target[key].length ) { for (let i = 0; i < source[key].length; i++) { if (this.isObject(target[key][i]) && this.isObject(source[key][i])) { this.deepMerge(target[key][i], source[key][i]); } else { target[key][i] = source[key][i]; } } } else { target[key] = source[key]; } } return target; } // Simulate subgraph fetch response fetchFromSubgraph(query) { // Normal subgraph response const normalResponse = { data: { products: [ { id: '1', name: 'Widget', price: 9.99 }, { id: '2', name: 'Gadget', price: 19.99 }, ], }, }; // If query contains __proto__ alias, the subgraph would return // data with that alias as a key. In a real attack, the subgraph // is either compromised or the alias is crafted by the client. if (query.includes('__proto__')) { // The subgraph processes the alias and returns data under that key // JSON.parse is used to deserialize the subgraph response // This creates __proto__ as an OWN property return JSON.parse( '{"data":{"products":[{"id":"1","name":"Widget","price":9.99}],"__proto__":{"polluted":true,"isAdmin":true}}}' ); } if (query.includes('constructor')) { return JSON.parse( '{"data":{"products":[{"id":"1","name":"Widget"}],"constructor":{"prototype":{"polluted_via_constructor":true}}}}' ); } return normalResponse; } // Simulate query plan execution executeQueryPlan(query) { // Step 1: Start with existing data from prior fetches // In a real gateway, multiple subgraph fetches are merged together. // The target must already have the 'data' key as an object so // deepMerge recurses into it (instead of just overwriting). const accumulatedData = { data: { products: [{ id: '0', name: 'Existing' }], }, }; // Step 2: Fetch from subgraph const subgraphResponse = this.fetchFromSubgraph(query); // Step 3: Merge response into accumulated data (VULNERABLE) // deepMerge will recurse into data, finding __proto__ as a key // target.data.__proto__ -> Object.prototype -> POLLUTED this.deepMerge(accumulatedData, subgraphResponse); // Step 4: Return result (Object.prototype may now be polluted) return accumulatedData; } handleRequest(req, res) { if (req.method === 'POST' && req.url === '/graphql') { let body = ''; req.on('data', (chunk) => (body += chunk)); req.on('end', () => { this.requestCount++; try { const { query, variables } = JSON.parse(body); console.log(`[Gateway] Request #${this.requestCount}: ${query.substring(0, 80)}...`); // Execute query plan (uses vulnerable deepMerge) const result = this.executeQueryPlan(query); // Check if prototype was polluted const polluted = ({}).polluted === true || ({}).isAdmin === true || ({}).polluted_via_constructor === true; if (polluted) { console.log(`[Gateway] \x1b[31mWARNING: Object.prototype has been polluted!\x1b[0m`); console.log(`[Gateway] polluted = ${({}).polluted}`); console.log(`[Gateway] isAdmin = ${({}).isAdmin}`); console.log(`[Gateway] polluted_via_constructor = ${({}).polluted_via_constructor}`); } // Build response with only own properties (avoid prototype chain issues) const responseData = result.data || {}; const safeData = {}; for (const key of Object.keys(responseData)) { safeData[key] = responseData[key]; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ data: safeData, extensions: { polluted, requestCount: this.requestCount, }, })); } catch (e) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ errors: [{ message: e.message }] })); } }); } else if (req.url === '/') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(`

CVE-2026-32621 Vulnerable Gateway

Send GraphQL queries to /graphql

Try: {"query":"{ __proto__: products { id } }"}

`); } else { res.writeHead(404); res.end('Not found'); } } start() { this.server = http.createServer((req, res) => this.handleRequest(req, res)); this.server.listen(this.port, () => { console.log(`\n\x1b[31m[CVE-2026-32621]\x1b[0m Vulnerable Gateway running at http://localhost:${this.port}`); console.log(` GraphQL endpoint: http://localhost:${this.port}/graphql`); console.log(` Vulnerability: Prototype pollution via deepMerge`); console.log(` Patched versions: 2.9.6, 2.10.5, 2.11.6, 2.12.3, 2.13.2`); console.log(`\n Send exploit with:`); console.log(` node exploit.js -u http://localhost:${this.port}/graphql\n`); }); } stop() { if (this.server) { this.server.close(); console.log('[Gateway] Server stopped'); } } } // Run if called directly if (require.main === module) { const port = parseInt(process.argv[2]) || 4000; const gateway = new VulnerableGatewaySimulator(port); gateway.start(); process.on('SIGINT', () => { gateway.stop(); process.exit(0); }); } module.exports = VulnerableGatewaySimulator;