fix: refuse redirects that leave the configured base URL's authority - #25
Merged
meorphis-anthropic merged 1 commit intoAug 13, 2026
Merged
Conversation
meorphis-anthropic
marked this pull request as ready for review
August 11, 2026 19:59
Comment on lines
+53
to
+54
| /// Credentials are only ever sent to this scheme, host, and port: a | ||
| /// redirect elsewhere fails the request instead of being followed. |
Contributor
Author
There was a problem hiding this comment.
how come? I think it's relevant when you're initializing your client actually?
dtmeadows-ant
approved these changes
Aug 13, 2026
meorphis-anthropic
deleted the
claude/strip-credentials-on-cross-origin-redirect
branch
August 13, 2026 21:26
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.
Before — a gateway or proxy configured as
baseURLanswersPOST /v1/messageswith a307to another host, andURLSessionfollows it, re-sending the request there:After — the redirect is refused and the call throws
HTTPTransportError.crossOriginRedirect; nothing is sent to the other host. Redirects that stay on the same scheme, host, and port are followed exactly as before:Foundation only drops
Authorizationwhen it follows a redirect. Every other place this package puts a credential survives the hop:x-api-keyfor.apiKey, the developer's headers for.proxied, and the App Attest assertion / attestation object in the body of the OAuth calls (which also go through this transport). The request body (the prompt) goes along too. Since the base URL is the one authority the credential was issued for, an API client has no reason to follow a redirect anywhere else, soURLSessionTransportnow attaches a per-taskURLSessionTaskDelegateto bothdata(for:)andbytes(for:)that compares the redirect target's authority against the original request's (default ports considered equal to explicit ones, host and scheme case-insensitive; a scheme change such as https → http counts as a different authority) and declines anything else. Refusing rather than stripping specific headers means the transport does not need to know which header names carry credentials in each auth mode, and it covers the body as well. When a redirect is declined,URLSessioncompletes the task with the 3xx response itself, whichClaudeClientwould otherwise treat as a success (and the streaming path would parse as an empty event stream), so the delegate records the refusal and the transport turns it into an error on both paths.Tests cover the decision itself (same authority with an omitted vs. explicit default port and mixed-case host is followed; other host, other port, and scheme change are refused and reported; a request without an authority follows nothing) and the delegate wired through a real
URLSessionusing aURLProtocolstub that issues a 307 and records every request it serves: for bothdata(for:)andbytes(for:), a redirect to a second host throws and the second host receives no request, and a redirect on the same host is followed withx-api-keyand the proxy header still on the follow-up request.Note: this was written on a machine without a Swift toolchain and has not been compiled or run locally, and this repo has no build/test workflow on pull requests, so please run
swift test(or the Xcode test action) before merging.