fix(auth): accept any port on loopback redirect URIs (RFC 8252 §7.3) #6
No reviewers
Labels
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/carddav-mcp!6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "oauth-loopback-port"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
is_allowed_redirect_uricompared the requested redirect URI to the allowlist by exact string equality, so the allowlistedhttp://localhost:8787/callbackcould never match a native client that binds a random ephemeral port. Claude Code CLI draws a free port per session, and DCR answeredunregistered redirect_uri.RFC 8252 §7.3 requires the authorization server to allow any port for a loopback redirect URI.
matches_loopback_entryimplements exactly that half of the rule: candidate and allowlist entry must both be cleartexthttpon the same loopback host, with identical path and query. The port is the only component omitted.httpsand private-use entries keep byte-for-byte matching, port included, sohttps://claude.ai:8443/api/mcp/auth_callbackstill fails againsthttps://claude.ai/api/mcp/auth_callback.Three new tests: port-agnostic loopback matching, host/path/query still exact, and non-loopback entries still exact on port.
Each test was checked against a deliberately broken matcher before being trusted. Four mutations, each turning red on the test that covers it:
loopback_entry_matches_any_port,loopback_port_relaxation_does_not_relax_host_or_pathloopback_port_relaxation_does_not_relax_host_or_pathloopback_port_relaxation_does_not_relax_host_or_pathnon_loopback_entries_keep_exact_port_matchingGates, all green locally on rustc 1.96.0:
cargo fmt --all --check,cargo clippy --all-targets --all-features --locked -- -D warnings,cargo test --all-features --locked(90 passed),cargo audit,cargo deny check bans licenses sources.Cross-engine review by Codex (read-only), asked whether the relaxation admits any non-loopback URI or any loopback URI with a different host, path or query: no. Its one nuance is that
url::Urlnormalizes dot segments before the path comparison, so/callback/../callbackcompares equal to/callback— the same effective path, not a different one.No deployment change: the shipped allowlist already carries
http://localhost:8787/callback, which starts matching once the rule is right.Closes #5
Two guards were not falsifiable by the first round of tests, and neither turns out to be redundant. Both hardenings arrived from the parallel
m365-mcpwork; the code already had them, the tests did not.https://localhost:3118/callbackmatches anhttp://localhost:8787/callbackentry.httpcandidate borrows the host and path of a private-use entry such ascursor://localhost/callback, because the port is the only thing being ignored.Full guard-removal table, each guard removed alone and restored:
loopback_entry_matches_any_port,loopback_port_relaxation_does_not_relax_host_or_pathhttploopback_relaxation_checks_the_requested_schemehttploopback_relaxation_checks_the_entry_schemeloopback_port_relaxation_does_not_relax_host_or_pathloopback_port_relaxation_does_not_relax_host_or_pathloopback_port_relaxation_does_not_relax_host_or_pathThe last row is honest rather than fixed. That check duplicates
validate_redirect_uri, whichis_allowed_redirect_urialready ran, so no input can reachmatches_loopback_entrywith a non-loopbackhttphost. It stays as a deliberate second check with a comment saying so, and no test was written to manufacture coverage for it.Gates re-run on rustc 1.96.0: fmt clean, clippy clean under
-D warnings,cargo test --all-features --locked92 passed / 0 failed,cargo auditexit 0,cargo deny check bans licenses sourcesok.