Repository navigation
Conversation
API Gateway HTTP APIs deliver `rawPath` (payload 2.0) and `path` (payload 1.0) already url-decoded, but `toStandardLazyRequest` treats every path as encoded and never escapes `%`, so a literal `%XX` was decoded a second time by the matcher. A request could then route to a different procedure than the one API Gateway routed on, getting past path-scoped AWS controls such as route authorizers or WAF rules. `AwsLambdaHandler` now re-escapes decoded paths before matching, so they are decoded only once. Payload 1.0 events are decoded when they carry `version: '1.0'`, payload 2.0 events unless `requestContext.domainName` is a Lambda Function URL host. REST API, ALB and Function URL events are unchanged. A new `pathEncoding` option overrides the detection. `@orpc/openapi`'s AWS Lambda handler is fixed by the same change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xa1ajNQxtP8VeB8FnC1LEw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xa1ajNQxtP8VeB8FnC1LEw
Merging this PR will not alter performance
|
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-lock
@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: |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Decoded-path re-escaping (
packages/server/src/adapters/aws-lambda/handler.ts) —AwsLambdaHandler.handlenow handstoStandardLazyRequesta shallow copy of the event whose path (rawPathfor 2.0,pathfor 1.0) is re-escaped with an RFC 3986 allow-list, so the matcher's single decode yields exactly what API Gateway delivered and a literal%XXis no longer decoded twice. AddspathEncoding?: 'auto' | 'encoded' | 'decoded'(default'auto'). - Source detection — payload 1.0 counts as decoded only with top-level
version: '1.0'; payload 2.0 counts as decoded unlessrequestContext.domainNamematches the anchored Function URL host pattern. - Tests — new
handler.test.tsasserts/rpc/%61dmin/deleteUserdoes not reachadmin.deleteUseracross HTTP API 2.0, a custom domain, a domain posing as a Function URL, a missingdomainName, and HTTP API 1.0, while it still matches plain/encoded REST/Function URL paths, plus one case perpathEncodingvalue;openapi-handler.test.tsadds path-param round-trips. - Docs — new "Path Encoding" section in
aws-lambda.mdxcovering the behavior and thepathEncodingoverride.
I traced the escape → toStandardUrl/toPathname → both matchers (normalizeHttpPath and normalizeRou3Path/decodeParams) and confirmed the escaping is exactly inverted by one decode, with % always escaped, so neither matcher can decode a delivered %XX again. pnpm vitest run packages/server/src/adapters packages/openapi/src/adapters passes (326 tests), and the upstream premise (HTTP API decodes rawPath, Function URL/REST/ALB keep it encoded) matches the live captures the fix cites.
ℹ️ The domainName heuristic is the one place escaping is skipped
isDecodedPathEvent skips re-escaping entirely when a 2.0 event's requestContext.domainName ends in .lambda-url.<region>.on.aws, so a decoded-path event that presents such a host would be treated as encoded and re-open the double decode. This is not attacker-controllable in practice — the lambda-url.<region>.on.aws zone is AWS-owned, so a custom domain cannot live under it — but it is the single trust point in the fix, and the suffix regex is the only signal used. Worth keeping in mind if the detection is ever revisited.
Technical details
# `domainName` heuristic is the sole gate on escaping
## Affected sites
- `packages/server/src/adapters/aws-lambda/handler.ts:80-90` — `escapeDecodedPath` returns the event untouched when `pathEncoding === 'encoded'` or (`'auto'` and `!isDecodedPathEvent(event)`).
- `packages/server/src/adapters/aws-lambda/handler.ts:96-105` — `isDecodedPathEvent`; for 2.0 it trusts `requestContext.domainName` against `FUNCTION_URL_DOMAIN_REGEX` (`:33`).
## Why it is not exploitable as written
- API Gateway populates `requestContext.domainName` from the host used to invoke the API; a host matching the regex requires a custom domain inside the AWS-owned `lambda-url.<region>.on.aws` zone, which a tenant cannot create.
- The suffix is anchored (`$`), so `...on.aws.example.com` is correctly classified as decoded (covered by the test at `handler.test.ts:58`).
## Optional hardening (not required)
- Lowercase `domainName` before the test, since DNS host names are case-insensitive and the regex is not.
- Guard a missing `requestContext` before reading `domainName` (`handler.ts:102`); AWS events always include it, so this is only defensiveness.
## Open question for the human
- Is there a more robust signal than the domain suffix for distinguishing a Function URL from an HTTP API? If not, the current heuristic is the best available and no change is needed.deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
orpc | 5ea093d | Commit Preview URL Branch Preview URL |
Oct 15 2026, 06:59 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |

Security hardening fix. API Gateway HTTP APIs deliver the request path already URL-decoded (
rawPathin payload 2.0,pathin payload 1.0).@standard-server/aws-lambda'stoStandardLazyRequesttreats every path as encoded and never escapes%, so the oRPC matcher decoded a literal%XXa second time (CWE-174). A request could then reach a different procedure than the one API Gateway routed on, which can get it past path-scoped AWS controls in front of a single oRPC Lambda: an HTTP API route authorizer on/rpc/admin/{proxy+}next to an open/rpc/{proxy+}, or WAF / resource-policy path rules. oRPC's own middleware and auth were never bypassed.@orpc/openapi's AWS LambdaOpenAPIHandlerextendsAwsLambdaHandler, so the same change fixes it.Fix
AwsLambdaHandler.handlenow passestoStandardLazyRequesta shallow copy of the event with the decoded path re-escaped. The escape uses an RFC 3986 allow-list (/[^\w\-.~!$&'()*+,;=:@/]/guwithsafeEncodeURIComponent), so%gets escaped, along with\,[,],|and non-ASCII. The matcher's single decode then yields exactly the path API Gateway delivered.'auto'detection:versionis'1.0'(HTTP API). REST API and ALB events leaveversionout and are unchanged.requestContext.domainNamematches the anchored Function URL host pattern/\.lambda-url\.[a-z0-9-]+\.on\.aws$/. Every other 2.0 event counts as decoded, including custom domains and a missingdomainName.pathEncoding?: 'auto' | 'encoded' | 'decoded'(default'auto') to override the detection, with JSDoc.RPCMatcher,normalizeHttpPathand the OpenAPI matcher are unchanged. Their single decode is correct for every other adapter.Compatibility
%.%in an HTTP API path or OpenAPI path param now resolves to what API Gateway delivered (for example/users/%41now givesid: '%41', where it used to give'A'). This also fixes correctness.%2F→/) cannot be recovered, and this PR doesn't try to.Upstream note
The long-term fix belongs in middleapi/standard-server
packages/aws-lambda/src/url.ts, which is what the closed, unmerged middleapi/standard-server#148 did. If@standard-server/aws-lambdais later bumped to a version that escapes decoded paths itself, remove the escaping here in the same change, or paths will be escaped twice. The new/users/100%test catches that, because it would yield100%25.Tests
packages/server/src/adapters/aws-lambda/handler.test.ts:/rpc/%61dmin/deleteUsermust not reachadmin.deleteUserfor: HTTP API 2.0, a custom domain, a custom domain posing as a Function URL, a missingdomainName, and HTTP API 1.0.pathEncodingvalue.openapi-handler.test.ts: path param casesa%2Fb,%41,100%, a space,é\from an HTTP API, anda%2Fbfrom a Function URL (decoded once).a%2Fb/%41came back asa/b/A). They pass now, and so do the existing Lambda and matcher tests.pnpm type:check,pnpm lintandpnpm docs:validatepass.Docs
docs/adapters/aws-lambda.mdxgets a new "Path Encoding" section. It covers HTTP API vs. REST API / Function URL behavior and thepathEncodingoption, and recommends enforcing authorization in oRPC middleware instead of relying only on API Gateway route authorizers or WAF path rules.The branch has a stray
pnpm-lock.yamlrewrite from a local install, reverted in its second commit, so the net diff touches only the four files above. Please squash merge.🤖 Generated with Claude Code
https://claude.ai/code/session_01Xa1ajNQxtP8VeB8FnC1LEw
Generated by Claude Code