fix: address review feedback on redirect policy
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 31s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 52s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m24s
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 31s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 52s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m24s
- Replace Unicode arrows (→) with ASCII (->) in error messages and comments for log compatibility (gpt-review NITs #19626, #19628) - Improve guard comment to clarify it exists for testability, not runtime safety (sonnet-review NIT #19619) - Add cross-reference comments noting intentional duplication between gitea/client.go and github/client.go (sonnet-review #19618, gpt-review #19625, #19627) Pushed back on: - internal/ package for dedup: structural overhead not warranted for a single ~25-line function - strings.EqualFold for scheme: Go's url.Parse normalizes schemes to lowercase, making case-insensitive comparison unnecessary
This commit is contained in:
+8
-6
@@ -79,27 +79,29 @@ type Client struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// defaultCheckRedirect is the redirect policy used by NewClient.
|
// defaultCheckRedirect is the redirect policy used by NewClient.
|
||||||
// It rejects HTTPS→HTTP protocol downgrades (to prevent plaintext leakage)
|
// NOTE: This function is intentionally duplicated in github/client.go (and vice versa)
|
||||||
|
// because the packages are separate. Changes here must be mirrored there.
|
||||||
|
// It rejects HTTPS->HTTP protocol downgrades (to prevent plaintext leakage)
|
||||||
// and cross-host redirects (to prevent following responses from untrusted
|
// and cross-host redirects (to prevent following responses from untrusted
|
||||||
// endpoints). Same-host, same-or-upgraded-scheme redirects are allowed.
|
// endpoints). Same-host, same-or-upgraded-scheme redirects are allowed.
|
||||||
func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
|
func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
|
||||||
if len(via) >= 10 {
|
if len(via) >= 10 {
|
||||||
return fmt.Errorf("stopped after 10 redirects")
|
return fmt.Errorf("stopped after 10 redirects")
|
||||||
}
|
}
|
||||||
// Guard: net/http guarantees len(via) >= 1 but defend against
|
// Guard for direct invocation in tests and any future callers;
|
||||||
// zero-length to avoid panic on index out of range.
|
// net/http guarantees len(via) >= 1 during actual redirects.
|
||||||
if len(via) == 0 {
|
if len(via) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
prev := via[len(via)-1]
|
prev := via[len(via)-1]
|
||||||
// Reject protocol downgrade: HTTPS→HTTP leaks request metadata over plaintext.
|
// Reject protocol downgrade: HTTPS->HTTP leaks request metadata over plaintext.
|
||||||
if prev.URL.Scheme == "https" && req.URL.Scheme == "http" {
|
if prev.URL.Scheme == "https" && req.URL.Scheme == "http" {
|
||||||
return fmt.Errorf("refusing redirect: HTTPS to HTTP downgrade (%s → %s)", prev.URL.Host, req.URL.Host)
|
return fmt.Errorf("refusing redirect: HTTPS to HTTP downgrade (%s -> %s)", prev.URL.Host, req.URL.Host)
|
||||||
}
|
}
|
||||||
// Reject cross-host redirect entirely to avoid consuming responses
|
// Reject cross-host redirect entirely to avoid consuming responses
|
||||||
// from untrusted endpoints.
|
// from untrusted endpoints.
|
||||||
if req.URL.Host != prev.URL.Host {
|
if req.URL.Host != prev.URL.Host {
|
||||||
return fmt.Errorf("refusing redirect: cross-host (%s → %s)", prev.URL.Host, req.URL.Host)
|
return fmt.Errorf("refusing redirect: cross-host (%s -> %s)", prev.URL.Host, req.URL.Host)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1176,7 +1176,7 @@ func TestDefaultCheckRedirect_RejectsHTTPSToHTTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := defaultCheckRedirect(req, []*http.Request{prev})
|
err := defaultCheckRedirect(req, []*http.Request{prev})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error on HTTPS→HTTP redirect")
|
t.Fatal("expected error on HTTPS->HTTP redirect")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "HTTPS to HTTP downgrade") {
|
if !strings.Contains(err.Error(), "HTTPS to HTTP downgrade") {
|
||||||
t.Errorf("unexpected error message: %v", err)
|
t.Errorf("unexpected error message: %v", err)
|
||||||
|
|||||||
+8
-6
@@ -108,27 +108,29 @@ type Client struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// defaultCheckRedirect is the redirect policy used by NewClient.
|
// defaultCheckRedirect is the redirect policy used by NewClient.
|
||||||
// It rejects HTTPS→HTTP protocol downgrades (to prevent plaintext leakage)
|
// NOTE: This function is intentionally duplicated in gitea/client.go (and vice versa)
|
||||||
|
// because the packages are separate. Changes here must be mirrored there.
|
||||||
|
// It rejects HTTPS->HTTP protocol downgrades (to prevent plaintext leakage)
|
||||||
// and cross-host redirects (to prevent following responses from untrusted
|
// and cross-host redirects (to prevent following responses from untrusted
|
||||||
// endpoints). Same-host, same-or-upgraded-scheme redirects are allowed.
|
// endpoints). Same-host, same-or-upgraded-scheme redirects are allowed.
|
||||||
func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
|
func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
|
||||||
if len(via) >= 10 {
|
if len(via) >= 10 {
|
||||||
return fmt.Errorf("stopped after 10 redirects")
|
return fmt.Errorf("stopped after 10 redirects")
|
||||||
}
|
}
|
||||||
// Guard: net/http guarantees len(via) >= 1 but defend against
|
// Guard for direct invocation in tests and any future callers;
|
||||||
// zero-length to avoid panic on index out of range.
|
// net/http guarantees len(via) >= 1 during actual redirects.
|
||||||
if len(via) == 0 {
|
if len(via) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
prev := via[len(via)-1]
|
prev := via[len(via)-1]
|
||||||
// Reject protocol downgrade: HTTPS→HTTP leaks request metadata over plaintext.
|
// Reject protocol downgrade: HTTPS->HTTP leaks request metadata over plaintext.
|
||||||
if prev.URL.Scheme == "https" && req.URL.Scheme == "http" {
|
if prev.URL.Scheme == "https" && req.URL.Scheme == "http" {
|
||||||
return fmt.Errorf("refusing redirect: HTTPS to HTTP downgrade (%s → %s)", prev.URL.Host, req.URL.Host)
|
return fmt.Errorf("refusing redirect: HTTPS to HTTP downgrade (%s -> %s)", prev.URL.Host, req.URL.Host)
|
||||||
}
|
}
|
||||||
// Reject cross-host redirect entirely to avoid consuming responses
|
// Reject cross-host redirect entirely to avoid consuming responses
|
||||||
// from untrusted endpoints.
|
// from untrusted endpoints.
|
||||||
if req.URL.Host != prev.URL.Host {
|
if req.URL.Host != prev.URL.Host {
|
||||||
return fmt.Errorf("refusing redirect: cross-host (%s → %s)", prev.URL.Host, req.URL.Host)
|
return fmt.Errorf("refusing redirect: cross-host (%s -> %s)", prev.URL.Host, req.URL.Host)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ func TestDefaultCheckRedirect_RejectsHTTPSToHTTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := defaultCheckRedirect(req, []*http.Request{prev})
|
err := defaultCheckRedirect(req, []*http.Request{prev})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error on HTTPS→HTTP redirect")
|
t.Fatal("expected error on HTTPS->HTTP redirect")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "HTTPS to HTTP downgrade") {
|
if !strings.Contains(err.Error(), "HTTPS to HTTP downgrade") {
|
||||||
t.Errorf("unexpected error message: %v", err)
|
t.Errorf("unexpected error message: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user