🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

fix: prevent open redirect in proxy authentication flow - #26647

Merged
sreya merged 9 commits into
mainfrom
subdomain-redirect
Jun 25, 2026
Merged

fix: prevent open redirect in proxy authentication flow#26647
sreya merged 9 commits into
mainfrom
subdomain-redirect

Conversation

@sreya

@sreya sreya commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

After consuming a smuggled subdomain API-key, handleAPIKeySmuggling issued a 303 redirect back to the request path to strip the query param. The target was built from the raw, already-decoded r.URL.Path, which can be manipulated by a malicious user.

The redirect target is now built through a url.URL whose Path collapses any leading / and \ run to a single /. url.URL.String() escapes the path, so control characters, backslashes, ?, and # are percent-encoded and can no longer introduce an off-origin host, query, or fragment. The redirect stays same-origin for every input while legitimate paths and unrelated query params are preserved

sreya added 2 commits June 23, 2026 23:30
… open redirect

Adds TestHandleSubdomain_APIKeySmuggling_NoOpenRedirect, which mints a valid
smuggled API key and drives HandleSubdomain with attacker-controlled paths that
survive to r.URL.Path: //evil.com/..., /\evil.com/..., and a leading tab
/<tab>/evil.com/.... After the key is consumed, HandleSubdomain issues a 303
redirect built from the uncleaned path, so the Location header resolves to an
off-origin host once a browser normalizes tabs and backslashes.

The leading-tab case is a real bypass over HTTP/1: http.Redirect emits the raw
tab verbatim (url.Parse rejects the control byte and skips cleaning) and the
header writer does not sanitize tabs.

TestHandleSubdomain_APIKeySmuggling_PreservesPath locks in that legitimate
paths still round-trip and that unrelated query params survive while the
smuggled key is stripped. The security test fails on the current code.

Refs ANT-2026-22456.
… path

handleAPIKeySmuggling consumed the smuggled subdomain API-key query param, set
the app session cookie, then issued a 303 redirect back to the same request.
The target was built from the uncleaned, attacker-controlled, already-decoded
r.URL.Path.

http.Redirect parses the target with url.Parse, which treats a leading "//" as
a scheme-relative URL with a host, and emits the target verbatim when url.Parse
rejects it (e.g. a decoded control byte). Browsers additionally strip tab and
newline characters and normalize a leading "/\" before resolving. So
//evil.com, /\evil.com, and /<tab>/evil.com all turned a trusted app subdomain
into an open redirect (the slash-collapsing middleware writes to chi's
RoutePath, not r.URL.Path, so the raw path survived).

Collapse any leading run of "/" and "\" into a single "/", then build the
redirect through url.URL so the path is escaped: control bytes, backslashes,
"?" and "#" become percent-encoded and can no longer introduce an authority,
query, or fragment. The redirect now stays same-origin for every input.

Fixes ANT-2026-22456.
@sreya

sreya commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-24 22:03 UTC by @sreya
Spend: $82.79 / $100.00

Review history
  • R1 (2026-06-24): 18 reviewers, 2 Nit, 2 P3, COMMENT. Review
  • R2 (2026-06-24), 2 Nit, 2 P3, COMMENT. Review
  • R3 (2026-06-24): 18 reviewers, 4 Nit, 2 P3, COMMENT. Review
  • R4 (2026-06-25): 10 reviewers, 4 Nit, 2 P3, APPROVE. Review

deep-review v0.9.0 | Round 4 | 51591e3..550616c

Last posted: Round 4, 6 findings (2 P3, 4 Nit), APPROVE. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (1ba1fe4) proxy_test.go:192 Missing \n/\r test vectors in NoOpenRedirect table R1 Bisky Yes
CRF-2 P3 Author fixed (1ba1fe4) proxy.go:269 PR title does not name the vulnerability class R1 Leorio Yes
CRF-3 Nit Author contested; panel closed R3 (8/8 accept) proxy_test.go:1 Commit subjects exceed 72-character limit R1 Leorio Yes
CRF-4 Nit Author fixed (1ba1fe4) proxy_test.go:98 Test helper duplicates server construction from existing test R1 Robin Yes
CRF-5 P2→Note Dropped by orchestrator (security traceability for internal audit tracking; test name and description explain the vulnerability independently) proxy_test.go:181 External audit ID ANT-2026-22456 in test comment R1 Gon No
CRF-6 P2→Note Dropped by orchestrator (Leorio praised same comments as good security documentation; over-documentation in security context is preferable to under-documentation) proxy.go:269, proxy_test.go:95,111,149,179,194,213,219,234,241 Comment bloat pattern (10 instances of comments restating code) R1 Gon No
CRF-7 Nit Author fixed (550616c) proxy_test.go:101 Stale cross-reference Test_originLocalPath should be Test_originLocalURL R3 Netero Yes
CRF-8 Nit Author fixed (550616c) proxy_internal_test.go:34 Test loops use require without t.Run per entry R3 Bisky Yes

Contested and acknowledged

CRF-3 (Nit, proxy_test.go:1) - Commit subjects exceed 72-character limit

  • Finding: Individual commit subjects (74, 83, 75 chars) exceed the 72-character project convention.
  • Author defense: "The commits are being squashed so the messages do not matter and the title of the PR will be used." The PR title is 55 characters, within the limit. Individual commit subjects are discarded on squash merge.
  • Panel closure (R3, 8/8): All eight evaluating reviewers accepted the defense. Squash merge discards individual commit subjects; the PR title (55 chars) becomes the final commit message and is within the 72-char convention.

Round log

Round 1

Panel (18 reviewers). 0 P0, 0 P1, 2 P3, 2 Nit, 1 Note. 10 Gon P2 comment findings dropped (Leorio praised same comments). Reviewed against 51591e3..43f62ba.

Round 2

BLOCKED. CRF-3 (Nit, commit subject length) silent. CRF-1, CRF-2, CRF-4 addressed. No review.

Round 3

PROCEED. CRF-3 contested, panel closed (8/8 accept). CRF-1, CRF-2, CRF-4 verified fixed. Netero: 1 Nit (CRF-7). Panel: 1 new Nit (CRF-8). Reviewed against 51591e3..1ba1fe4.

Round 4

PROCEED. CRF-7, CRF-8 addressed. All findings resolved. Panel (10 reviewers): 0 new findings. Reviewed against 51591e3..550616c.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

A deep review of ANT-2026-22456 found the same open-redirect class in two more
redirects in proxyWorkspaceApp that build the Location from the raw,
attacker-controlled r.URL.Path: the trailing-slash redirect (r.URL.Path+"/")
and the default-query redirect (r.URL.String()). These are reachable for coderd
path apps because singleSlashMW cleans chi's RoutePath while leaving r.URL.Path
raw, so a request like //@user/ws/apps/app can match the route yet redirect to a
scheme-relative //@user/... Location (off-origin once a browser resolves it).

Extract the same-origin normalization into originLocalPath and route all three
redirects (including the API-key smuggling one) through it, building each
Location via url.URL so control characters are escaped. Add an internal unit
test covering the helper; the existing RedirectsWithSlash/RedirectsWithQuery
integration tests confirm legitimate redirects are unchanged.

Refs ANT-2026-22456.

@coder-agents-review coder-agents-review 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.

Clean, well-scoped security fix. The two-layer defense (TrimLeft for slash/backslash collapsing + url.URL.String() for control-character encoding) is the right approach, and both layers are independently necessary: TrimLeft alone misses the tab-stripping bypass, url.URL alone passes "//evil.com" verbatim. 18 reviewers found no correctness or security issues.

Severity count: 2 P3, 2 Nit.

"I tried to build a case against this change and couldn't. The problem is correctly understood, the solution is proportional, and the fix is at the right level." (Pariston)


coderd/workspaceapps/proxy_test.go:1

Nit [CRF-3] Both commit subjects exceed the 72-character limit (fix: 74, test: 83). Shorter alternatives:

  • fix(coderd/workspaceapps): prevent open redirect via API-key smuggling (71)
  • test(coderd/workspaceapps): regression test for API-key open redirect (70)

(Leorio)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/workspaceapps/proxy_test.go Outdated
Comment thread coderd/workspaceapps/proxy.go
Comment thread coderd/workspaceapps/proxy_test.go Outdated
Match the Go convention of naming an unexported-function test
Test_<func> rather than Test<Func>.

Refs ANT-2026-22456.
@sreya sreya changed the title fix: only proxy redirect on existing origin fix: prevent open redirect in proxy authentication flow Jun 24, 2026
sreya added 4 commits June 24, 2026 19:17
Address review feedback on the API-key smuggling redirect tests:

- Collapse newSmugglingTestServer/doSmugglingRequest and the two table-driven
  HandleSubdomain tests into a single focused wiring test constructed inline,
  matching the sibling TestHandleSubdomain_IgnoresUntrustedForwardedHost. There
  is no lighter shared fixture, and the full app test harness cannot model an
  attacker path that bypasses the slash-cleaning middleware, so the inline
  setup is the minimal option.
- Move the exhaustive path-sanitization matrix to Test_originLocalPath (no
  server needed) and add an EscapesControlCharacters case asserting tab, newline,
  and CR are percent-encoded (%09/%0A/%0D), so a regression in any single
  control-character class is caught.

Refs ANT-2026-22456.
Fold the API-key smuggling redirect test into a single TestHandleSubdomain
parent alongside the forwarded-host test, trim the verbose comments, and drop
the internal tracking reference.
Have the helper return the sanitized *url.URL directly instead of a path
string, since every caller wrapped it in a url.URL anyway. Rename it to
originLocalURL and assert on its String() output in the unit test.
…rowser

The origin-local URL test reproduced browser URL normalization (stripping
tab/newline/CR and flipping backslash to slash) to check the redirect target.
That made test-only logic look like it belonged in production. Assert the
invariant directly instead: the escaped Location parses as a relative,
same-origin reference and contains no raw backslash or control byte that a
browser could normalize back into an authority. url.URL.String() guarantees
this by percent-encoding, so production needs no ad-hoc stripping.
@sreya

sreya commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review 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.

CRF-1, CRF-2, and CRF-4 are addressed. The test restructuring, originLocalURL extraction, and additional redirect-site hardening look solid at a glance. Full panel review is blocked on one silent finding:

CRF-3 (Nit): Commit subjects exceed the 72-character limit. The original commits (74, 83 chars) remain, and the new commit also exceeds it (75 chars: test(coderd/workspaceapps): assert escaping instead of simulating a browser). This was folded into the R1 review body (outside diff hunks), so it may not have been visible as a distinct action item.

To unblock: acknowledge, contest, or fix CRF-3. A one-line reply is sufficient.

🤖 This review was automatically generated with Coder Agents.

@sreya

sreya commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

The commits are being squashed so the messages do not matter and the title of the PR will be used

@sreya

sreya commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review 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.

R1 findings CRF-1, CRF-2, and CRF-4 verified fixed. CRF-3 (Nit, commit subject length) contested by author and closed by panel (8/8 accept: squash merge discards individual subjects; PR title at 55 chars is within the convention).

The originLocalURL extraction, additional redirect-site hardening, and test restructuring are all clean. 18 reviewers found no correctness or security issues with the updated code. All three redirect sites in proxy.go that derive their target from r.URL.Path now go through originLocalURL; the remaining redirect uses server-controlled s.AccessURL.String().

Severity count: 2 Nit (1 stale reference, 1 test style).

"I tried to construct a scenario where this fix fails to prevent an open redirect and could not. The differential diagnosis produced four framings; the author chose the right one." (Pariston)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/workspaceapps/proxy_test.go Outdated
Comment thread coderd/workspaceapps/proxy_internal_test.go Outdated
Wrap each table entry in t.Run so every malicious and legitimate path is an
independent subtest, matching the sibling tests in this package. A failing
vector no longer hides the rest, which matters for a security table. Also fix
a stale Test_originLocalPath reference left by the rename to Test_originLocalURL.
@sreya

sreya commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@sreya
sreya requested a review from Emyrk June 24, 2026 22:50

@coder-agents-review coder-agents-review 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.

All findings resolved across 4 rounds. No new issues found by Netero or the 10-reviewer panel.

originLocalURL is a clean, minimal sanitizer applied at all three r.URL.Path-derived redirect sites. The two-layer defense (TrimLeft for slash/backslash collapsing + url.URL.String() for control-character encoding) eliminates the class of open redirect by construction. Test coverage is thorough: 18 attack vectors, 3 encoding assertions, 5 legitimate-path preservation cases at the unit level, plus an end-to-end integration test through the full handler with a real encrypted API key.

"I tried to build a case against this change and could not. The problem is correctly understood, the solution is proportional, and the fix is at the right causal level." (Pariston, R4)

🤖 This review was automatically generated with Coder Agents.

@sreya
sreya merged commit 7e7a6b4 into main Jun 25, 2026
29 of 30 checks passed
@sreya
sreya deleted the subdomain-redirect branch June 25, 2026 16:17
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants