# `@orpc/client` has Prototype Pollution via `StandardRPCJsonSerializer` Deserialization

Prototype pollution in @orpc/client's StandardRPCJsonSerializer deserializer lets unauthenticated attackers set Object.prototype properties via __proto__ paths in meta/maps arrays, enabling auth bypass and potential RCE.

- Payout: Research
- Severity: critical
- Program: @orpc/client
- Found by: mnixry
- Disclosed: 2026-03-02
- CVE: CVE-2026-28794
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-m272-9rp6-32mc
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-m272-9rp6-32mc
- Tags: area:oss domain:oss impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:auth-bypass vuln:prototype-pollution

**Target:** RPC JSON deserializer (StandardRPCJsonSerializer) in Node.js server adapters
**Root cause:** deserialize() follows attacker-controlled path segments from meta and maps arrays without filtering __proto__/constructor keys, writing into Object.prototype. The maps vector works because getBlob(i) returns FormData text fields cast `as Blob`, a compile-time-only cast.
**Steps:**
1. Set up playgrounds/astro and run `pnpm dev`
2. Send `curl -X POST http://localhost:4321/rpc/planet/create -F 'data={"json":{},"meta":[],"maps":[["__proto__","role"]]}' -F '0=admin'`
3. Deserializer follows __proto__ path and maps index 0 to string "admin", setting Object.prototype.role = "admin" process-wide
**How found:** Code review of deserialize() path handling in rpc-json-serializer.ts, noting missing __proto__/constructor filtering and that getBlob's `as Blob` cast lets text FormData fields return arbitrary strings.
**Exploitation:** Unauthenticated POST to any RPC endpoint with a multipart body whose `data` JSON contains maps/meta paths traversing __proto__; pollution occurs before Zod validation, so it persists even if the request is rejected. Global Object.prototype.role="admin" then satisfies defaulted role checks for all users.
**Escalation:** Chain with prototype pollution gadgets in app or dependencies for RCE; overwrite built-ins like toString for DoS.
**Insight:** In multipart/FormData-based deserializers, TypeScript `as` casts on getBlob() are not runtime-enforced — text fields can supply arbitrary strings for blob slots. Also check whether deserialization runs before schema validation, since pollution then survives rejected requests.

