fix(proxy): forward original encoded request-path instead of decoded …#76
Open
Victor-De-Decker wants to merge 4 commits into
Open
fix(proxy): forward original encoded request-path instead of decoded …#76Victor-De-Decker wants to merge 4 commits into
Victor-De-Decker wants to merge 4 commits into
Conversation
…guard Deduplicates the identical try/catch around both http(s).request call sites, as flagged by the aikido-pr-checks review.
insuT0ver
added a commit
that referenced
this pull request
Jul 21, 2026
Addresses review feedback on #76: 1. Revert the accidental import reformatting in proxy.ts (brace-spacing + reordering) back to main's style — keeps the diff to the security-relevant hunks and matches the file's own `{ ... }` convention. 2. Add proxy-forward-path.test.ts: a unit-level regression that drives createProxyServer() against a real local upstream (no Docker, no live container) and asserts the upstream receives the *encoded* bytes (`/foo/a%20b`, `/foo/%E2%9C%93`, query preserved) while traversal (`/foo/..%2f..%2fadmin`) is still fail-closed (403, never forwarded). Guards the fix under `npm test`, not only the E2E_ENABLED-gated e2e. Binding-gated like the other SQLite suites (skips where better-sqlite3 isn't built; runs in CI/image). 3. Tighten the e2e assertions: instead of `not.toBe('403') / not.toBe('000')` (passes on almost any status), require a well-formed upstream status that is neither '000' (crash / refused CONNECT), '403' (Huddle block), nor '400' (the bad_request forward-guard — exactly what firing would mean the decoded raw-space form was forwarded again). Non-brittle: no dependency on example.org's exact status. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on #76: 1. Revert the accidental import reformatting in proxy.ts (brace-spacing + reordering) back to main's style — keeps the diff to the security-relevant hunks and matches the file's own `{ ... }` convention. 2. Add proxy-forward-path.test.ts: a unit-level regression that drives createProxyServer() against a real local upstream (no Docker, no live container) and asserts the upstream receives the *encoded* bytes (`/foo/a%20b`, `/foo/%E2%9C%93`, query preserved) while traversal (`/foo/..%2f..%2fadmin`) is still fail-closed (403, never forwarded). Guards the fix under `npm test`, not only the E2E_ENABLED-gated e2e. Binding-gated like the other SQLite suites (skips where better-sqlite3 isn't built; runs in CI/image). To make this testable, createProxyServer() takes an optional `port` (default = PROXY_PORT, so index.ts is unchanged); the test binds port 0 (ephemeral) instead of the privileged :80. 3. Tighten the e2e assertions: instead of `not.toBe('403') / not.toBe('000')` (passes on almost any status), require a well-formed upstream status that is neither '000' (crash / refused CONNECT), '403' (Huddle block), nor '400' (the bad_request forward-guard — exactly what firing would mean the decoded raw-space form was forwarded again). Non-brittle: no dependency on example.org's exact status. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
insuT0ver
force-pushed
the
fix/proxy-forward-encoded-path
branch
from
July 21, 2026 15:00
7655176 to
e9be8ed
Compare
Contributor
|
huddle init --experiment 76 Can you try to run it with this. if everything succeeds and you checked my last commit. I will approve and merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One ordinary request through the proxy could crash the entire gateway — and with it every running huddle.
Since the finding #7 hardening (#67), the proxy normalizes request paths with
decodeURIComponent()for the firewall rule check, but then forwarded that decoded path to the upstream. Any percent-encoded character that decodes to something illegal in an HTTP request-target (%20, any non-ASCII UTF-8) makes Node'shttp(s).request()throwERR_UNESCAPED_CHARACTERSsynchronously; the throw reached theuncaughtExceptionhandler inindex.ts, which callsprocess.exit(1). In practice this was triggered by legitimate traffic: an Azure DevOps project name containing a space (/org/My%20Project/_git/...) killed the gateway on everygit fetch, minutes after each huddle started.This PR:
normalizePathname()'s documentation inrules.tsalready describes. It also strengthens finding Mcp policies token hiding #7: forwarding the decoded form let double-encoded%252e%252ereach the upstream half-decoded (whose own decode turns it into.., reopening the traversal differential), and silently corrupted legitimate%2F/%20in forwarded paths.http(s).request()construction in try/catch in both handlers, so any future synchronous validation throw fails that single request with a 400 instead of exiting the process.boundary.e2e.ts) covering both proxy paths.Verified end-to-end against a live container: the previously crashing URL now returns the upstream's normal response (302 from dev.azure.com) on both paths, traversal (
/foo/..%2f..%2fadmin) is still blocked with 403, and the gateway stays up.Related issue
Fixes #
Type of change
fix)feat)docs)refactor/chore)Related issue
Checklist
mainand focused on a single change.npm --prefix gateway test) and I added/updated tests where relevant.Notes for reviewers
path:changes (gateway/src/proxy.ts). The rule check still receives the decoded form — only the forwarded bytes changed. The existing finding Mcp policies token hiding #7 e2e test (traversal → 403) passes unchanged.socket-proxy.test.tsfailures are pre-existing onmain(Unix-socketEACCES); the skip fix (de51151) lives onexperiment/58-wslc-supportand hasn't landed onmain. Linux CI is unaffected.