Skip to content

fix: refuse redirects that leave the configured base URL's authority - #25

Merged
meorphis-anthropic merged 1 commit into
mainfrom
claude/strip-credentials-on-cross-origin-redirect
Aug 13, 2026
Merged

fix: refuse redirects that leave the configured base URL's authority#25
meorphis-anthropic merged 1 commit into
mainfrom
claude/strip-credentials-on-cross-origin-redirect

Conversation

@meorphis-anthropic

@meorphis-anthropic meorphis-anthropic commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Before — a gateway or proxy configured as baseURL answers POST /v1/messages with a 307 to another host, and URLSession follows it, re-sending the request there:

POST https://proxy.example/v1/messages   x-api-key: sk-…   X-App-Token: …
  → 307 Location: https://elsewhere.example/v1/messages
POST https://elsewhere.example/v1/messages   x-api-key: sk-…   X-App-Token: …   (+ body)

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:

POST https://proxy.example/v1/messages
  → 307 Location: https://elsewhere.example/v1/messages
✗ HTTPTransportError.crossOriginRedirect(to: https://elsewhere.example/…)

Foundation only drops Authorization when it follows a redirect. Every other place this package puts a credential survives the hop: x-api-key for .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, so URLSessionTransport now attaches a per-task URLSessionTaskDelegate to both data(for:) and bytes(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, URLSession completes the task with the 3xx response itself, which ClaudeClient would 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 URLSession using a URLProtocol stub that issues a 307 and records every request it serves: for both data(for:) and bytes(for:), a redirect to a second host throws and the second host receives no request, and a redirect on the same host is followed with x-api-key and 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.

@meorphis-anthropic
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we drop this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come? I think it's relevant when you're initializing your client actually?

@meorphis-anthropic
meorphis-anthropic merged commit fd965bf into main Aug 13, 2026
2 checks passed
@meorphis-anthropic
meorphis-anthropic deleted the claude/strip-credentials-on-cross-origin-redirect branch August 13, 2026 21:26
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.

3 participants