Repository navigation
Conversation
… decoded again
API Gateway HTTP APIs deliver the request path url-decoded (payload format
1.0 `path` and 2.0 `rawPath`), but `toStandardUrl` left `%` and `\` as-is
for every source. A decoded `%2e%2e` was then decoded again by `URL` into
a dot segment, and a decoded `\` (sent as `%5C`) was treated as `/`, so
the app resolved a path API Gateway never routed on: `/public/x%5C..%5C..%5Csecret`
matches `/public/{proxy+}` yet the app serves `/secret`, skipping a
route-level authorizer on `GET /secret`.
## Fixes
- HTTP API paths now also escape `%` and `\`, so they decode back to
exactly what API Gateway delivered and keep their segment structure.
- REST APIs, ALB and Lambda Function URLs deliver encoded paths and keep
passing through unchanged.
## Notes
- Sources are told apart without guessing from the path: only HTTP APIs
send a top-level `version` on payload format 1.0 (REST and ALB omit it),
and Function URLs always carry a
`requestContext.domainName` of `<url-id>.lambda-url.<region>.on.aws`.
A 2.0 event without a `domainName` (never seen in real events) is
treated as an HTTP API, so hand-built test events with an encoded
`rawPath` now need a Function URL `domainName` to pass through.
- `version` and `requestContext.domainName` are added as optional fields,
`@types/aws-lambda` events still satisfy both types.
- Live captures (hotsock/voker#8) show HTTP APIs decoding the path twice
(`%2525` arrives as `%`), so `%252e%252e` may still arrive as a literal
`..`, which no URL can carry unresolved. The README documents this and
advises authorizing in the app rather than relying on route-level
authorizers alone.
## Testing
- `url.test.ts` covers HTTP API v1/v2 with `%2e%2e`, `%25`, `%2525` and
`\`, a decode round-trip, Function URL and REST passthrough, and the
`domainName` detection. The new HTTP API cases fail on the previous
implementation.
- `pnpm run check` and `pnpm test` are green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbRvaNSKxHggDmMVk5VhHM
…url tests - Escape everything but RFC 3986 path characters in decoded HTTP API paths instead of the WHATWG set plus `%` and `\`. Complete by construction and no longer duplicates the encoded-path regex; it additionally escapes `[`, `]` and `|`, which decode to themselves. - Share one payload 2.0 test builder, drop the unused `version: '2.0'`, merge the per-source blocks with `describe.each`, and remove tests already covered by the case tables and the round-trip check. - Tighten the README bullet on path re-escaping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbRvaNSKxHggDmMVk5VhHM
@standard-server/aws-lambda
@standard-server/core
@standard-server/fastify
@standard-server/fetch
@standard-server/node
@standard-server/peer
@standard-server/shared
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
ℹ️ No critical issues — the security fix is correct for every source I could verify. One open question inline.
Reviewed changes
- Path source detection (
packages/aws-lambda/src/url.ts) —toStandardUrlnow classifies each event as delivering a decoded or still-encoded path (HTTP API vs REST/ALB/Function URL) and passes anisDecodedflag intotoPathname, which escapes decoded paths with an RFC 3986 allow-list (%,\,[,],|, ... included). This closes the decoded-%2e%2e/\route-confusion gap while leaving encoded paths byte-identical. - Event types (
packages/aws-lambda/src/types.ts) — adds optionalversion(v1) andrequestContext.domainName(v2) and documents the decode contract per source. - Tests (
packages/aws-lambda/src/url.test.ts) — 68 cases; decoded paths round-trip throughnew URL(...).pathname+safeDecodeURIComponentback to the delivered path exactly once, encoded paths stay identical, and a decoded?/#cannot leak into the query/fragment. - README — documents the re-escaping, the recognition rules, and the double-decode caveat.
I verified the load-bearing AWS contracts against primary sources: HTTP API v2 rawPath is decoded and rawQueryString encoded (hotsock/voker#8), REST and ALB path are encoded (aws-sam-cli#771), and Function URL domainName is <url-id>.lambda-url.<region>.on.aws even behind CloudFront. All of those classifications match the code. pnpm exec vitest run packages/aws-lambda/src/url.test.ts passes 68/68.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Live captures only cover payload format 2.0. Keep escaping the 1.0 `path` as decoded, and note why: wrongly escaping costs a double-encoding, wrongly not escaping reopens the traversal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbRvaNSKxHggDmMVk5VhHM

API Gateway HTTP APIs deliver the request path url-decoded (payload format 1.0
pathand 2.0rawPath), buttoStandardUrlescaped it like an encoded path, leaving%and\as-is. The app then decoded the path a second time: a decoded%2e%2ebecame a dot segment and a decoded\a/, resolving to a path API Gateway never routed. For exampleGET /public/x%5C..%5C..%5Csecretmatches an unauthenticated/public/{proxy+}route and arrives as/public/x\..\..\secret, whichnew URL()resolves to/secret, skipping the route-level authorizer onGET /secret.Fixes
%,\,[,]and|included), so they decode back to exactly what API Gateway delivered and keep the segments it routed on.version: '1.0'on payload format 1.0 (REST and ALB omit it), and Function URL events carry arequestContext.domainNameof<url-id>.lambda-url.<region>.on.aws. Both fields are added to the event types as optional, so@types/aws-lambdaevents still satisfy them.Notes
domainName(never the case in real events) is treated as an HTTP API, so hand-built events with an encodedrawPathnow need a Function URLdomainNameto pass through unchanged.pathis not in those captures and follows #93.%2525arrives as%), so%252e%252emay still arrive as a literal.., which no url can carry without resolving it. The README documents this and advises authorizing in the app, not only with route-level authorizers.Testing
url.test.tscovers HTTP API v1 and v2 with%2e%2e,%25,%2525,\and[]|, a check that the url decodes back to the delivered path exactly once, Function URL and REST passthrough (%2525and%2e%2eincluded), and thedomainNamedetection. 16 of them fail againstmain.pnpm run checkandpnpm testpass (1348 vitest tests, plus the Bun and Deno suites).🤖 Generated with Claude Code
https://claude.ai/code/session_01NbRvaNSKxHggDmMVk5VhHM