plumbing: transport/http, harden redirect handling to match canonical git by aymanbagabas · Pull Request #1997 · go-git/go-git · GitHub
Skip to content

plumbing: transport/http, harden redirect handling to match canonical git - #1997

Merged
pjbgf merged 3 commits into
go-git:mainfrom
aymanbagabas:http-hardening
Apr 16, 2026
Merged

plumbing: transport/http, harden redirect handling to match canonical git#1997
pjbgf merged 3 commits into
go-git:mainfrom
aymanbagabas:http-hardening

Conversation

@aymanbagabas

Copy link
Copy Markdown
Member

Summary

The HTTP transport's redirect handling had several security gaps compared to canonical git's default http.followRedirects = initial policy. This PR closes them.

Problem

When go-git's HTTP transport follows server redirects during Handshake (the GET /info/refs discovery request), several things could go wrong:

  1. No redirect policy — Go's default http.Client follows redirects on every request (GET, POST, etc.). A malicious server could redirect a pack-data POST to an attacker-controlled host and the client would silently follow it. Canonical git only follows redirects on the initial /info/refs request and treats 3xx on subsequent requests as errors.

  2. No protocol/scheme restriction — A redirect to file://, gopher://, or any other scheme was accepted. This is an SSRF vector in server-side environments. Canonical git restricts redirect targets to http/https/ftp/ftps via CURLOPT_REDIR_PROTOCOLS.

  3. Silent fallback on path mismatch — If the redirect target's path didn't end with /info/refs, applyRedirect silently returned the original base URL instead of erroring. Canonical git's update_url_from_redirect() calls die() here because a tail mismatch could indicate a malicious redirect attempting to rewrite the base URL to an unrelated repository.

  4. Credential leakage to redirect target — Both the URL-embedded Userinfo credentials and the Authorizer callback were stored in the session and re-applied on every subsequent POST. After a cross-host redirect, the original host's credentials would be sent to the new host. Canonical git re-derives credentials from the new URL via credential_from_url(), effectively wiping the old ones.

Changes

  • common.goapplyRedirect now returns (*url.URL, error). Validates the redirect target's scheme is http or https. Returns a hard error when the redirected path doesn't end with /info/refs (matching canonical git's die() behavior).

  • http.go — Adds a CheckRedirect policy (checkRedirect) implementing canonical git's initial mode: only the /info/refs GET (tagged via context key) is allowed to follow redirects. Non-initial requests, unsupported schemes, and >10 hops are rejected. Go's stdlib already strips Authorization on cross-host redirects and preserves it for same-host redirects, so no manual header manipulation is needed.

  • handshake.go — Tags the /info/refs request with withInitialRequest(ctx). After applyRedirect, clears both URL.User and the Authorizer callback when the host changed — both are credential sources that would otherwise leak to the redirect target.

Canonical git reference

  • Redirect policy: http.c:130 (HTTP_FOLLOW_INITIAL default), http.c:1583-1591 (CURLOPT_FOLLOWLOCATION per-request)
  • Protocol whitelist: http.c:991-1013 (get_curl_allowed_protocols)
  • Tail validation: http.c:2243-2293 (update_url_from_redirect)
  • Credential wipe: http.c:2354-2360 (credential_from_url after redirect)

Test plan

  • TestCheckRedirectPolicy — unit tests for the checkRedirect function: blocks non-initial requests, allows initial requests, blocks unsupported schemes, blocks >10 redirects
  • TestApplyRedirect — unit tests for applyRedirect: no-op detection, host/scheme/path updates, scheme rejection, tail mismatch error
  • TestRedirectPostBlocked — integration test: server proxies GET but 307-redirects POST; verifies fetch fails with "non-initial request"
  • TestRedirectStripsCredentials — integration test: cross-host redirect clears URL.User from the session's base URL
  • TestRedirectPath, TestRedirectSchema, TestRedirectPathWithFetch — existing integration tests still pass (legitimate redirects during handshake continue to work)
  • Full go test ./plumbing/transport/http/... passes

@aymanbagabas
aymanbagabas force-pushed the http-hardening branch 2 times, most recently from 37bba5a to 86775cd Compare April 15, 2026 02:05
pjbgf added a commit that referenced this pull request Apr 16, 2026
Back-port from #1997.

Signed-off-by: Paulo Gomes <paulo@entire.io>
aymanbagabas and others added 2 commits April 16, 2026 11:54
This commit adds security checks to the HTTP transport's redirect
handling to prevent SSRF and credential leakage. The applyRedirect
function now validates the redirect target's scheme and path, and the
checkRedirect policy allows redirects only on the initial /info/refs
request. Tests are added to verify that non-initial redirects are
blocked, credentials are stripped on cross-host redirects, and
unsupported schemes are rejected. This hardening aligns with canonical
git's behavior and mitigates risks from malicious servers during
repository discovery and fetch operations.
Signed-off-by: Paulo Gomes <paulo@entire.io>
Entire-Checkpoint: 2b7603909172

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

Hardens the HTTP transport’s redirect behavior to align with canonical git’s http.followRedirects = initial default, reducing redirect-based credential/SSRF risks during smart/dumb HTTP discovery and subsequent RPC calls.

Changes:

  • Introduces a redirect policy (FollowRedirects) and enforces it via http.Client.CheckRedirect (default: only follow redirects on the initial /info/refs request).
  • Tightens applyRedirect to validate redirect targets and return errors for inconsistent redirect URLs, and clears session credentials on cross-host redirects.
  • Adds unit + integration tests covering redirect policy enforcement, redirected POST blocking, and credential stripping.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
plumbing/transport/http/http.go Adds redirect policy types, context tagging, and CheckRedirect enforcement logic.
plumbing/transport/http/handshake.go Tags the discovery request as “initial”, applies redirect-derived base URL, clears credentials on host change.
plumbing/transport/http/common.go Changes applyRedirect to return (*url.URL, error) and enforce scheme/tail validation.
plumbing/transport/http/common_test.go Expands applyRedirect tests to cover scheme/path/tail validation and downgrade rejection.
plumbing/transport/http/redirect_test.go Adds integration + unit tests for redirect policy, POST redirect blocking, and credential stripping.
plumbing/transport/http/tls_test.go Updates resolveClient tests to assert redirect policy wrapping behavior for custom clients.

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

Comment thread plumbing/transport/http/http.go Outdated
Comment thread plumbing/transport/http/http.go Outdated
Comment thread plumbing/transport/http/common.go
Comment thread plumbing/transport/http/http.go Outdated
pjbgf added a commit that referenced this pull request Apr 16, 2026
Back-port from #1997.

Signed-off-by: Paulo Gomes <paulo@entire.io>
pjbgf added a commit that referenced this pull request Apr 16, 2026
Back-port from #1997.

Signed-off-by: Paulo Gomes <paulo@entire.io>
…th upstream

Expands the previous logic that had 'initial' as the hard-coded logic. This enables users to opt-in into the other
two options that Git supports: false (blind block) and true (blind allow).

Signed-off-by: Paulo Gomes <paulo@entire.io>

@aymanbagabas aymanbagabas left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM

@pjbgf
pjbgf merged commit 137874f into go-git:main Apr 16, 2026
16 checks passed
pjbgf added a commit that referenced this pull request Apr 16, 2026
Back-port from #1997.

Signed-off-by: Paulo Gomes <paulo@entire.io>
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