Skip to content

fix: Prevent agent HTTP auth redirect leaks#3978

Open
lox wants to merge 1 commit into
mainfrom
lox/agenthttp-auth-redirect
Open

fix: Prevent agent HTTP auth redirect leaks#3978
lox wants to merge 1 commit into
mainfrom
lox/agenthttp-auth-redirect

Conversation

@lox

@lox lox commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Authenticated agent HTTP clients currently inject credentials in the transport layer, after Go's redirect sanitizer has already decided whether to strip Authorization. If a trusted Buildkite endpoint ever redirected to a different host or scheme, the redirected request could receive the agent token or bearer credential.

This keeps the normal redirect behavior for unauthenticated clients, but authenticated clients now stop at redirects that change scheme or host and return the redirect response instead. Same-origin redirects still follow normally and continue to include the expected authorization header.

For example, an authenticated request from https://agent.buildkite.com/... to a Location: https://example.invalid/... response will no longer follow that redirect with credentials attached, while /old to /new on the same origin continues to work.

@lox lox marked this pull request as ready for review June 4, 2026 09:35
@lox lox requested review from a team as code owners June 4, 2026 09:35
@lox lox added the internal Non-user facing, internal change. label Jun 4, 2026
@quinn-diesel quinn-diesel requested a review from Copilot June 10, 2026 11:54
@quinn-diesel

Copy link
Copy Markdown
Contributor

@buildsworth-bk review please

@lox sorry didn't mean for Codex to review as well

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens internal/agenthttp authenticated HTTP clients against credential leakage across redirects by refusing to follow redirects that change scheme or host, while keeping default redirect behavior for unauthenticated clients.

Changes:

  • Add a CheckRedirect policy for authenticated clients to stop on cross-origin (scheme/host) redirects.
  • Add tests covering authenticated cross-origin redirect blocking, authenticated same-origin redirect following, and unauthenticated cross-origin redirect following.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
internal/agenthttp/client.go Adds an authenticated-only redirect policy to prevent credential leaks on cross-origin redirects.
internal/agenthttp/client_test.go Adds regression tests for redirect behavior with and without authentication.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +73
func checkAuthenticatedRedirect(req *http.Request, via []*http.Request) error {
if len(via) == 0 {
return nil
}

origin := via[0].URL
if req.URL.Scheme != origin.Scheme || req.URL.Host != origin.Host {
return http.ErrUseLastResponse
}

return nil
}

@buildsworth-bk-app buildsworth-bk-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This wires a CheckRedirect onto the authenticated clients so a redirect that changes scheme or host returns http.ErrUseLastResponse instead of being followed with the agent token/bearer attached — closing the leak that came from injecting credentials in the transport layer, below Go's redirect sanitizer. The approach is sound: only the authenticated clients (api.Client, bkgql) get the policy, so the unauthenticated artifact downloaders still follow their S3/GCS redirects, and the tests cover the cross-origin, same-origin, and unauthenticated cases well.

Two things to keep in mind:

  • One inline note about Go's default 10-redirect cap being dropped along with the default policy.
  • For authenticated clients a cross-origin redirect now surfaces to the caller as a 3xx (e.g. checkResponse treats it as an error) rather than being followed. That looks like the intended trade-off, and the description frames the scenario as hypothetical — just flagging it for the reviewer.

There's no risk: label on the PR; since this changes credential-handling and redirect behaviour, it's worth a human pass.

Want to dig deeper? The full session log is attached to this Buildkite build. Download the session file and open a new pi session with it:

Download the buildsworth logs from build 597, then answer my questions about the findings.

}
}

func checkAuthenticatedRedirect(req *http.Request, via []*http.Request) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: Setting a custom CheckRedirect replaces Go's default redirect policy entirely, including its built-in cap of 10 redirects — defaultCheckRedirect only runs when CheckRedirect is nil. Because this returns nil for every same-origin hop, an authenticated client following same-origin redirects now has no limit; the only bound is the client Timeout (60s for both the API and GraphQL clients), so a same-origin redirect loop would spin until the timeout rather than stopping after 10.

Worth re-adding the cap to preserve the previous behaviour:

if len(via) >= 10 {
    return fmt.Errorf("stopped after 10 redirects")
}

(needs an fmt/errors import). A small test for the loop case would lock it in.

@DrJosh9000

Copy link
Copy Markdown
Contributor

I had a thought about this, since I had a quick look yesterday, but didn't respond. Here it is.

If a trusted Buildkite endpoint ever redirected to a different host or scheme

On the surface this seems very unlikely. Redirecting HTTPS to HTTP seems remarkably unlikely. A different host is more plausible. To be safe, the backend would have to limit any such redirects to agent versions after this change, or until after all old agents are no longer used, which would limit the utility of such a redirect.

I don't think this is an argument against this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Non-user facing, internal change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants