Skip to content

Download hop 2 follows redirects in four SDKs: needs a cross-SDK destination policy, not a Go patch #805

Description

@jeremy

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions