fetchSignedDownload (go/pkg/basecamp/download.go:16) builds its own *http.Client and sets no CheckRedirect, so the unauthenticated second hop of a download follows up to 10 redirects under net/http's default policy — cross-host, unbounded in destination.
Every other hop in the SDK that can be steered by a response suppresses redirects. fetchAPIDownload's authenticated hop sets http.ErrUseLastResponse twelve lines above this one; OAuth discovery does the same, and SPEC.md §16 requirement 3 names it as a hardening requirement. This hop is the exception, and it looks unintentional rather than argued — there is no comment either way, where the neighbouring decisions are all commented.
Reproduced with a throwaway test: an API host 302s to a "signed" host, which redirects 9 times and lands on a third server. The SDK followed the whole chain and returned the third server's body:
RESULT body="SECRET" signedHops=9 finalHits=1
The reach is narrower than SSRF: the first Location comes from the operator-configured API host, so the chain starts somewhere trusted, and no credentials are attached on this hop (TestDownload_SecondLegNoAuth). What is unbounded is where it ends up — a compromised or misconfigured object store decides that, and the caller receives the final body as if it were the requested file.
Not filed as part of it: the Timeout: 0 on the same client. That one is deliberate, commented (streaming owned by caller), and pinned by TestDownload_SecondLegNoTimeout. It should stay.
Not fixed here on purpose. The remedy is a product call, not a mechanical one, and it needs someone who knows what the fleet's storage backends actually do:
- Suppress outright (
ErrUseLastResponse), matching every sibling hop — but S3 region redirects and CloudFront chains are real, so this may break live downloads.
- Cap the chain (1–2 hops) and re-validate each target, keeping the common case working.
- Leave it and write down why, so the next reader does not re-open this.
Happy to implement whichever, once the object-store behaviour is settled. Filed separately from #804 by design — no dependency either way.
fetchSignedDownload(go/pkg/basecamp/download.go:16) builds its own*http.Clientand sets noCheckRedirect, so the unauthenticated second hop of a download follows up to 10 redirects under net/http's default policy — cross-host, unbounded in destination.Every other hop in the SDK that can be steered by a response suppresses redirects.
fetchAPIDownload's authenticated hop setshttp.ErrUseLastResponsetwelve lines above this one; OAuth discovery does the same, andSPEC.md§16 requirement 3 names it as a hardening requirement. This hop is the exception, and it looks unintentional rather than argued — there is no comment either way, where the neighbouring decisions are all commented.Reproduced with a throwaway test: an API host 302s to a "signed" host, which redirects 9 times and lands on a third server. The SDK followed the whole chain and returned the third server's body:
The reach is narrower than SSRF: the first Location comes from the operator-configured API host, so the chain starts somewhere trusted, and no credentials are attached on this hop (
TestDownload_SecondLegNoAuth). What is unbounded is where it ends up — a compromised or misconfigured object store decides that, and the caller receives the final body as if it were the requested file.Not filed as part of it: the
Timeout: 0on the same client. That one is deliberate, commented (streaming owned by caller), and pinned byTestDownload_SecondLegNoTimeout. It should stay.Not fixed here on purpose. The remedy is a product call, not a mechanical one, and it needs someone who knows what the fleet's storage backends actually do:
ErrUseLastResponse), matching every sibling hop — but S3 region redirects and CloudFront chains are real, so this may break live downloads.Happy to implement whichever, once the object-store behaviour is settled. Filed separately from #804 by design — no dependency either way.