Skip to content

fix(json-schema): prevent prototype injection when coercing objects#1726

Merged
dinwwwh merged 1 commit into
1.xfrom
claude/object-prototype-injection-coercer-4f0819
Jul 25, 2026
Merged

fix(json-schema): prevent prototype injection when coercing objects#1726
dinwwwh merged 1 commit into
1.xfrom
claude/object-prototype-injection-coercer-4f0819

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Jul 25, 2026

Copy link
Copy Markdown
Member

Fixes two prototype chain bugs in the object branch of JsonSchemaCoercer#coerce. The coercer runs on untrusted input before validation (SmartCoercionPlugin), so the keys below are attacker controlled.

1. Coerced properties were collected into a plain {}

coercedItems['__proto__'] = value triggers the inherited __proto__ setter instead of creating an own property:

schema: { type: 'object', properties: { a: { type: 'number' } } }
input:  {"a":"1","__proto__":{"isAdmin":true}}

out = { a: 1 }        // __proto__ entry dropped from the output
out.isAdmin === true  // attacker object became the prototype

Object.prototype is not touched, but the returned value inherits attacker controlled properties, and anything reading them through the prototype chain sees them.

Fix: collect into NullProtoObj, the helper already used for untrusted keys in bracket-notation.ts and rpc-matcher.ts.

2. schema.properties?.[key] returned inherited members

For keys like constructor, toString and __proto__ the lookup resolves to Object.prototype members, which were then used as sub-schemas. They coerce nothing and report satisfied: true, so those keys skipped the unknown property path and selected the wrong union branch:

anyOf: [{ type: 'object', properties: { a: { type: 'number' } } },
        { type: 'object', additionalProperties: { type: 'boolean' } }]

{ z: 'true' }           => { z: true }              // correct branch
{ constructor: 'true' } => { constructor: 'true' }  // before: wrong branch, not coerced

Fix: guard the lookup with Object.hasOwn.

Verification

prevents prototype injection when coercing objects covers both cases and fails without the fix. The json-schema, openapi and openapi-client suites pass (412 tests). tsc -b and eslint are clean.

The object branch of `JsonSchemaCoercer` accumulated coerced properties
into a plain `{}`, so an input key named `__proto__` hit the
`Object.prototype` setter instead of creating an own property: the key
was dropped from the output and the attacker-supplied object became the
prototype of the coerced value.

It also looked up sub-schemas with `schema.properties?.[key]`, which
resolves `constructor`, `toString` and `__proto__` through
`Object.prototype`. Those inherited values were then used as schemas,
which coerce nothing and report `satisfied: true`, letting such keys
bypass the unknown-property path (and, in a union, select the wrong
branch).

Accumulate into `NullProtoObj` and guard the lookup with
`Object.hasOwn`.
@pullfrog

pullfrog Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor
  • Read the full diff and PR summary to understand changes
  • Triage: understand the codebase context (coercer, NullProtoObj, schema handling)
  • Identify load-bearing hypotheses and dispatch specialists if needed
  • Aggregate findings, draft review, submit via pullfrog_create_pull_request_review

Pullfrog  | View workflow run | via Pullfrog | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview, Comment Jul 25, 2026 2:50pm

@pkg-pr-new

pkg-pr-new Bot commented Jul 25, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1726

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1726

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1726

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1726

@orpc/experimental-durable-iterator

npm i https://pkg.pr.new/@orpc/experimental-durable-iterator@1726

@orpc/hey-api

npm i https://pkg.pr.new/@orpc/hey-api@1726

@orpc/interop

npm i https://pkg.pr.new/@orpc/interop@1726

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1726

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1726

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1726

@orpc/openapi-client

npm i https://pkg.pr.new/@orpc/openapi-client@1726

@orpc/otel

npm i https://pkg.pr.new/@orpc/otel@1726

@orpc/experimental-pino

npm i https://pkg.pr.new/@orpc/experimental-pino@1726

@orpc/experimental-publisher

npm i https://pkg.pr.new/@orpc/experimental-publisher@1726

@orpc/experimental-publisher-durable-object

npm i https://pkg.pr.new/@orpc/experimental-publisher-durable-object@1726

@orpc/experimental-ratelimit

npm i https://pkg.pr.new/@orpc/experimental-ratelimit@1726

@orpc/react

npm i https://pkg.pr.new/@orpc/react@1726

@orpc/react-query

npm i https://pkg.pr.new/@orpc/react-query@1726

@orpc/experimental-react-swr

npm i https://pkg.pr.new/@orpc/experimental-react-swr@1726

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1726

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1726

@orpc/solid-query

npm i https://pkg.pr.new/@orpc/solid-query@1726

@orpc/standard-server

npm i https://pkg.pr.new/@orpc/standard-server@1726

@orpc/standard-server-aws-lambda

npm i https://pkg.pr.new/@orpc/standard-server-aws-lambda@1726

@orpc/standard-server-fastify

npm i https://pkg.pr.new/@orpc/standard-server-fastify@1726

@orpc/standard-server-fetch

npm i https://pkg.pr.new/@orpc/standard-server-fetch@1726

@orpc/standard-server-node

npm i https://pkg.pr.new/@orpc/standard-server-node@1726

@orpc/standard-server-peer

npm i https://pkg.pr.new/@orpc/standard-server-peer@1726

@orpc/svelte-query

npm i https://pkg.pr.new/@orpc/svelte-query@1726

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1726

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1726

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1726

@orpc/vue-colada

npm i https://pkg.pr.new/@orpc/vue-colada@1726

@orpc/vue-query

npm i https://pkg.pr.new/@orpc/vue-query@1726

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1726

commit: 26ef5f5

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dinwwwh
dinwwwh merged commit 8a35262 into 1.x Jul 25, 2026
6 checks passed
dinwwwh added a commit that referenced this pull request Jul 25, 2026
Same two prototype chain bugs as #1726, in `ZodSmartCoercionPlugin` (zod
3) and `experimental_ZodSmartCoercionPlugin` (zod 4). Both plugins
coerce untrusted input before validation, so the keys below are attacker
controlled.

### 1. Coerced properties were collected into a plain `{}`

`newObj['__proto__'] = value` triggers the inherited `__proto__` setter
instead of creating an own property. This affects the `object` and
`record` branches in both plugins.

Fix: collect into `NullProtoObj`, the helper already used for untrusted
keys in `bracket-notation.ts` and `rpc-matcher.ts`.

### 2. `shape[key]` returned inherited members

For keys like `constructor`, `toString` and `__proto__` the lookup
resolves to `Object.prototype` members, which were then dereferenced as
Zod schemas:

```
schema: z.object({ a: z.number() })
input:  { a: '123', constructor: '456' }

zod 3 => TypeError: Cannot read properties of undefined (reading 'Symbol(ORPC_CUSTOM_ZOD_DEF)')
zod 4 => TypeError: Cannot read properties of undefined (reading 'def')
```

The throw happens before the assignment, so in practice these keys made
coercion fail rather than injecting a prototype. Any client could break
a request this way.

Fix: guard the lookup with `Object.hasOwn`, so unknown keys fall through
to `catchall` as they already do for every other name.

### Verification

`zodSmartCoercionPlugin prevents prototype injection` added to both test
files, covering `object`, `record` and `catchall`. Both fail without the
fix. The zod coercer suites pass (583 tests). `tsc -b` and eslint are
clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant