Skip to content

fix(aws-lambda): re-encode decoded HTTP API paths so they are not decoded twice - #148

Closed
dinwwwh wants to merge 3 commits into
mainfrom
claude/vibrant-galileo-xw0jf1
Closed

dinwwwh wants to merge 3 commits into
mainfrom
claude/vibrant-galileo-xw0jf1

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

API Gateway HTTP APIs deliver the request path url-decoded (payload format 1.0 path and 2.0 rawPath), but toStandardUrl escaped it like an encoded path, leaving % and \ as-is. The app then decoded the path a second time: a decoded %2e%2e became a dot segment and a decoded \ a /, resolving to a path API Gateway never routed. For example GET /public/x%5C..%5C..%5Csecret matches an unauthenticated /public/{proxy+} route and arrives as /public/x\..\..\secret, which new URL() resolves to /secret, skipping the route-level authorizer on GET /secret.

Fixes

  • Decoded HTTP API paths now escape everything but RFC 3986 path characters (%, \, [, ] and | included), so they decode back to exactly what API Gateway delivered and keep the segments it routed on.
  • REST API, ALB and Lambda Function URL paths arrive still encoded and keep passing through unchanged.
  • Sources are told apart from the event, not guessed from the path: only HTTP APIs send a top-level version: '1.0' on payload format 1.0 (REST and ALB omit it), and Function URL events carry a requestContext.domainName of <url-id>.lambda-url.<region>.on.aws. Both fields are added to the event types as optional, so @types/aws-lambda events still satisfy them.

Notes

  • A payload format 2.0 event without a domainName (never the case in real events) is treated as an HTTP API, so hand-built events with an encoded rawPath now need a Function URL domainName to pass through unchanged.
  • Which sources decode is based on the live captures in hotsock/voker#8 and AWS's sample events; it was not re-tested against a live API. HTTP API payload format 1.0 path is not in those captures and follows #93.
  • The captures also show HTTP APIs decoding the path twice (%2525 arrives as %), so %252e%252e may 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.ts covers 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 (%2525 and %2e%2e included), and the domainName detection. 16 of them fail against main.
  • pnpm run check and pnpm test pass (1348 vitest tests, plus the Bun and Deno suites).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NbRvaNSKxHggDmMVk5VhHM

claude added 2 commits October 7, 2026 03:09
… 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
@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
@standard-server/aws-lambda

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

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@148

@standard-server/fastify

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

@standard-server/fetch

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

@standard-server/node

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

@standard-server/peer

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

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@148

commit: 8e6e23a

@dinwwwh dinwwwh changed the title Fix path encoding for Lambda Function URLs and HTTP APIs fix(aws-lambda): re-encode decoded HTTP API paths so they are not decoded twice Oct 8, 2026
@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/vibrant-galileo-xw0jf1 (8e6e23a) with main (668cc70)

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@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 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) — toStandardUrl now classifies each event as delivering a decoded or still-encoded path (HTTP API vs REST/ALB/Function URL) and passes an isDecoded flag into toPathname, 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 optional version (v1) and requestContext.domainName (v2) and documents the decode contract per source.
  • Tests (packages/aws-lambda/src/url.test.ts) — 68 cases; decoded paths round-trip through new URL(...).pathname + safeDecodeURIComponent back 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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread packages/aws-lambda/src/url.ts
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
@dinwwwh dinwwwh closed this Oct 8, 2026
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.

2 participants