Skip to content

refactor(shared, json-schema): reuse getOwn for own-property lookups - #2025

Merged
dinwwwh merged 2 commits into
middleapi:mainfrom
dinwwwh:claude/confident-napier-799bf3
Sep 15, 2026
Merged

dinwwwh merged 2 commits into
middleapi:mainfrom
dinwwwh:claude/confident-napier-799bf3

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 15, 2026

Copy link
Copy Markdown
Member

get() in @orpc/shared and the property lookup in JsonSchemaCoercer were hand-rolled copies of getOwn; they now call it. The coercer's properties !== undefined ternary is gone in favor of the same ?? {} default its patternProperties lookup already uses.

Testing

  • New RPC and OpenAPI serializer tests pin that a blob or special value stored under an own __proto__ key, or behind an own constructorprototype chain, is restored in place with the global Object.prototype untouched and the container never re-parented.
  • pnpm vitest run across client, openapi, json-schema and shared, pnpm lint, and pnpm type:check pass.

Not changed

  • The RPC and OpenAPI deserializer walks and getRouter / getRouterContract keep their existing reads and writes. The deserializers already verify every segment with Object.hasOwn before reading it, and router paths come from the router definition rather than the request, so switching them to getOwn / setOwn would only add a check per segment.

Converge every remaining path walk over untrusted segments onto the
shared getOwn/setOwn helpers, matching BracketNotationSerializer
(middleapi#2024): the RPC and OpenAPI JSON deserializers, getRouter,
getRouterContract, JsonSchemaCoercer, and get() in @orpc/shared.

getRouter and getRouterContract previously resolved segments like
__proto__ and constructor through the prototype chain; they now return
undefined. The other sites are idiom convergence: they were own-only
already, but only via the previous iteration's Object.hasOwn guard.
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pkg-pr-new

pkg-pr-new Bot commented Sep 15, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

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

@orpc/arktype

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

@orpc/bun

npm i https://pkg.pr.new/middleapi/orpc/@orpc/bun@2025

@orpc/client

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

@orpc/cloudflare

npm i https://pkg.pr.new/middleapi/orpc/@orpc/cloudflare@2025

@orpc/contract

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

@orpc/experimental-effect

npm i https://pkg.pr.new/middleapi/orpc/@orpc/experimental-effect@2025

@orpc/evlog

npm i https://pkg.pr.new/middleapi/orpc/@orpc/evlog@2025

@orpc/hibernation

npm i https://pkg.pr.new/middleapi/orpc/@orpc/hibernation@2025

@orpc/json-schema

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

@orpc/experimental-lock

npm i https://pkg.pr.new/middleapi/orpc/@orpc/experimental-lock@2025

@orpc/experimental-msw

npm i https://pkg.pr.new/middleapi/orpc/@orpc/experimental-msw@2025

@orpc/nest

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

@orpc/next

npm i https://pkg.pr.new/middleapi/orpc/@orpc/next@2025

@orpc/node

npm i https://pkg.pr.new/middleapi/orpc/@orpc/node@2025

@orpc/openapi

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

@orpc/opentelemetry

npm i https://pkg.pr.new/middleapi/orpc/@orpc/opentelemetry@2025

@orpc/pinia-colada

npm i https://pkg.pr.new/middleapi/orpc/@orpc/pinia-colada@2025

@orpc/pino

npm i https://pkg.pr.new/middleapi/orpc/@orpc/pino@2025

@orpc/publisher

npm i https://pkg.pr.new/middleapi/orpc/@orpc/publisher@2025

@orpc/ratelimit

npm i https://pkg.pr.new/middleapi/orpc/@orpc/ratelimit@2025

@orpc/server

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

@orpc/shared

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

@orpc/swr

npm i https://pkg.pr.new/middleapi/orpc/@orpc/swr@2025

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/zod

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

commit: 5dabce2

@codspeed

codspeed Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing dinwwwh:claude/confident-napier-799bf3 (5dabce2) with main (b0b52fa)

Open in CodSpeed

@dinwwwh dinwwwh changed the title fix: stop path walks resolving through the prototype chain fix(rpc, openapi): stop path walks resolving through the prototype chain Sep 15, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • getRouter and getRouterContract stop walking the prototype chaincurrent[segment] becomes getOwn(current, segment), so __proto__, constructor, toString, etc. return undefined instead of Object.prototype / Object. This is the PR's real behavioral change; the new assertions in packages/server/src/router-utils.test.ts:341-347 and packages/contract/src/router-utils.test.ts:199-205 fail against the pre-PR code.
  • RPC and OpenAPI JSON deserializers switch to getOwn/setOwn — behavior-identical: each read key was already validated as an own property by the previous loop iteration's Object.hasOwn guard, so getOwn and a raw index agree, and setOwn is defensive hardening. The new __proto__/constructor tests pass against pre-PR code too, which the PR body acknowledges.
  • JsonSchemaCoercer property lookup and writeschema.properties ?? {} plus getOwn and setOwn, preserving the __proto__-as-own-property semantics already relied on by coercer.test.ts.
  • get()/set() in @orpc/sharedgetOwn replaces the inline Object.hasOwn guard; semantically identical, just the unified pattern.

I independently traced the deserializer read/write ordering and agree with the "behavior-identical" claim, and a reference pass over the request pipeline (rpc-matcher, openapi-matcher, bracket-notation, the prototype-pollution plugin) found no remaining unguarded prototype-traversing read or non-setOwn write reachable from attacker-controlled segments.

ℹ️ Resolver traversal is now stricter than the for...in indexers

walkProcedureContractsSync / walkProcedureContractsAsync / augmentImplementedRouter iterate with for...in, which enumerates inherited enumerable keys, while getRouter / getRouterContract now reject anything not an own property. The only shape affected is Object.create(baseRouter)-style inheritance, which is neither type-supported nor documented as a router shape and has no test, doc, or first-party builder in the repo — so this is intended hardening, not a regression. Worth being aware of if that inheritance style is ever meant to be supported.

Technical details
# Traversal semantics asymmetry

## Affected sites
- `packages/server/src/router-utils.ts:268,303,340``for (const key in router)` indexes inherited enumerable entries
- `packages/server/src/router-utils.ts:204``getOwn` rejects inherited entries at resolve time
- `packages/contract/src/router-utils.ts:75` — same for contracts

## Why it is not a finding
- Routers/contracts are documented and typed as plain own-property records
  (`apps/content/docs/router.mdx`, `packages/server/src/router.ts`), and every
  first-party builder (`augmentRouter`, `augmentImplementedRouter`,
  `augmentContractRouter`, `createRouterImplementerInternal`) materializes entries
  as own keys, so indexing and resolving agree for every supported shape.
- `withHiddenRouterContract` uses a Proxy with only a `get` trap, so
  `Object.hasOwn` forwards to the target and own-key resolution is unchanged.
- `getRouter` is fed `entry.path` derived from the developer router, not the
  request pathname, so the hardening is defense-in-depth for the matchers; it is
  directly reachable via `createRouterClient` property access and the dynamic-client
  link codec.

## Open question for the human
- Is `Object.create(base)` router inheritance intended to be supported? If not,
  no action; if yes, the `for...in` walkers and the `getOwn` resolvers need to be
  reconciled.

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

Revert getOwn/setOwn in the RPC and OpenAPI deserializers, getRouter,
getRouterContract and the coercer write. The deserializer loops verify
each segment with Object.hasOwn before reading it, so the extra check
only costs, and router paths come from the router definition, not the
request. Keep getOwn in get() and the coercer property lookup, which
were hand-rolled copies of it.
@dinwwwh dinwwwh changed the title fix(rpc, openapi): stop path walks resolving through the prototype chain refactor(shared, json-schema): reuse getOwn for own-property lookups Sep 15, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run re-reviewed the delta since the prior pullfrog review (987ad554): commit 5dabce22 reverts the prototype-chain hardening that commit introduced, leaving a behavior-preserving refactor.

  • Reverted the router and deserializer hardeninggetRouter, getRouterContract, the RPC/OpenAPI deserializer walks and the JsonSchemaCoercer write are back to their pre-PR reads/writes; the removed getOwn/setOwn calls and their router tests are gone.
  • Kept the getOwn reuse as a pure refactorget()/set() in packages/shared/src/object.ts and the propertySchemas lookup in packages/json-schema/src/coercer.ts delegate to getOwn; traced against the code they replace, observable behavior is identical for __proto__, constructor, and symbol keys.
  • Kept the deserializer regression tests — the new RPC/OpenAPI __proto__ and own constructorprototype cases still pass on the reverted sources, so they pin existing behavior rather than the reverted change.

pnpm vitest run for the four touched packages (286 tests), pnpm type:check, and pnpm lint all pass.

ℹ️ The branch no longer carries the prototype-chain hardening

Reverting returns getRouter / getRouterContract to raw current[segment], so __proto__ / constructor segments resolve through the prototype chain again. This matches main, and every caller feeds a router- or developer-derived path (rpc-matcher.ts:224, openapi-matcher.ts:180, router-client.ts:66, openapi-link-codec.ts:422), so it is not a request-reachable regression — but if that hardening is still wanted, it now needs a separate change.

Technical details
# Removed router hardening

## Affected sites
- packages/server/src/router-utils.ts:204 — `current[segment]` resolves inherited members
- packages/contract/src/router-utils.ts:75 — same for contracts
- packages/client/src/rpc-json-serializer.ts:386,406 — raw reads, guarded by `Object.hasOwn`
- packages/openapi/src/openapi-json-serializer.ts:322 — same
- packages/json-schema/src/coercer.ts:232 — raw write into `{ ...coerced }`

## Why it is not a bug
- Behavior matches `main`, so this is a scope reduction, not a regression.
- The deserializer loops validate each segment with `Object.hasOwn` before
  reading it, and only write to keys already proven own, so a `__proto__`
  segment cannot re-parent the container.
- `coercedItems` is spread from an `isPlainObject`-checked source, so an own
  `__proto__` stays an own data property and the raw write updates it.

## Open question for the human
- Is the `getRouter` / `getRouterContract` prototype-chain rejection still
  desired? If so it needs a follow-up PR.

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

@dinwwwh
dinwwwh merged commit 31bc665 into middleapi:main Sep 15, 2026
11 checks passed
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