feat(server): add PrototypePollutionProtectionHandlerPlugin - #1961
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
orpc | 1379f9e | Commit Preview URL Branch Preview URL |
Aug 27 2026, 07:46 AM |
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
ℹ️ Minor suggestions only — the plugin is correct, well-tested, and consistent with existing plugin conventions. One robustness edge worth a look.
Reviewed changes
PrototypePollutionProtectionHandlerPlugin— newStandardHandlerPluginthat prepends a client interceptor rejecting any matched request whose decoded input carries an own__proto__key or aconstructor-holding-prototypepair with a400 BAD_REQUEST, mirroring secure-json-parse.- Detection walk — recurses plain/null-prototype objects, arrays, and
Map/Setkeys and values with a cycle guard, leaving non-containers (File,Date) untouched; loneconstructor/prototypekeys stay allowed. - 23 tests — interceptor-level block/allow/cycle cases plus
RPCHandlerintegration against raw JSON bodies; strict assertions, verified to pass. - Docs page —
/docs/plugins/prototype-pollution-protectioncovering setup and the event-iterator / all-or-nothing / defense-in-depth limitations; naming matches sibling plugin docs. - Exports — one line added to
plugins/index.ts.
The design is sound and I verified the walking logic independently: isPlainObject does include null-prototype objects (so the __proto__-on-null-proto block test is meaningful), Object.hasOwn-based checks correctly tolerate lone constructor/prototype keys, and the prepend ordering matches SmartCoercionHandlerPlugin. One inline robustness suggestion below.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ The new streaming-input guard is correct, well-tested, and consistent with the shared
override/wrapAsyncIteratorpattern. One previously-noted robustness edge remains open (see the existing thread on the recursion walk).
Reviewed changes
- Streaming-input guard — the interceptor now detects an
AsyncIteratorObjectinput and wraps it withoverride(input, wrapAsyncIterator(input, { mapResult })), so each yielded value and the return value are run throughcontainsPollutingKeyas they arrive; a polluting value fails thenext()promise with the same400 BAD_REQUEST. - Three new non-vacuous tests — guarded-iterator identity change plus benign
for awaitpass-through; mid-stream rejection (benign yield resolves, polluting yield rejects with the exactORPCError); and a return-value check via generatorreturn. - Docs updated — the walk description now states async-iterator inputs are checked value by value, and the "event iterator payloads pass uninspected" limitation was removed.
The implementation mirrors the established output-side pattern in packages/shared/src/interceptor.ts (same override + wrapAsyncIterator, same @warning comment), and isAsyncIteratorObject is the canonical detection used across the framework. The guard covers both next()-driven and for await consumption, which the tests exercise. No new issues in this delta.
The prior review's inline thread on the unbounded recursion in containsPollutingKey (deep parse-acceptable nesting → RangeError/500 instead of 400) is unaffected by this commit and remains open.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ Docs-only change reviewed — one informational note on the removed "Limitations" section.
Reviewed changes
- Removed the "Limitations" section from the plugin docs page — dropping the "defense in depth, not a substitute for safe code" bullet and the "rejection is all-or-nothing with a generic
BAD_REQUEST" caveat. No source, test, or export changes in this delta.
ℹ️ The all-or-nothing caveat was worth keeping
The removed "Limitations" section carried two genuinely useful, non-obvious points that are not covered elsewhere on the page: the defense-in-depth framing (plug vulnerabilities from merging/cloning, prefer Object.create(null)/Map) and the operational all-or-nothing behavior — a procedure that legitimately accepts a __proto__/constructor key as data would be silently blocked with a generic 400 and no doc pointer telling the author to drop the plugin for that handler. If the trimming was intentional, that's fine; I'd just want to confirm the all-or-nothing caveat specifically isn't a loss users will trip on.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found. The prior recursion-thread concern is resolved; the iterative walk is correct and well-tested.
Reviewed changes
- Iterative prototype-pollution walk — replaced the recursive
containsPollutingKeywith an explicit-stackwhileloop. Semantically equivalent (sameWeakSetcycle guard, array/Map/Set/plain-object handling, and__proto__/constructor.prototypechecks), but bounded heap usage instead of the call stack, so arbitrarily deep input no longer throws aRangeError. - Deep-nesting tests — two new cases: a 100k-level benign object is allowed without overflowing the stack, and the same nesting with a polluting
__proto__at the bottom is blocked with the expected400 BAD_REQUEST.
This directly addresses the prior inline thread (iterative scan was the suggested fix). I re-derived the walk's semantics against the old recursion — behavior is preserved, including the visited cycle guard and short-circuit on first polluting hit — and the full 28-test suite passes. No new issues in this delta.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Adds
PrototypePollutionProtectionHandlerPluginto@orpc/server/plugins. It rejects any request whose decoded input contains an own__proto__key, or aconstructorkey holding aprototypekey, with a400 BAD_REQUESTbefore the procedure runs — the same rule secure-json-parse enforces for Fastify. oRPC's own decoding is already pollution-safe; the plugin stops these keys from reaching application code that merges, clones, or path-sets input with a vulnerable utility.Behavior
RPCHandler, form data and bracket notation inOpenAPIHandler.Map/Setkeys and values at any depth, iteratively with a cycle guard, so nesting deeper than the call stack still gets a400instead of aRangeError; non-container objects such asFileandDatepass untouched.AsyncIteratorObjectinput is checked value by value as it arrives; a polluting value fails that iteration with the same error.constructororprototypekeys stay allowed; neither alone can pollute.Testing
AsyncIteratorObjectinputs, andRPCHandlerintegration with raw JSON bodies; the full server suite (532 tests) passes.pnpm type:check, eslint, and the docs JSDoc backlink checker all pass.Docs
/docs/plugins/prototype-pollution-protectiondocuments how the check works and setup.