fix(#123): address review feedback on SSRF defense
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 46s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m14s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 2m24s

- Clone http.DefaultTransport instead of bare &http.Transport{} to preserve
  ProxyFromEnvironment, TLSHandshakeTimeout, IdleConnTimeout, connection
  pooling, and HTTP/2 support (fixes transport regression).

- Add IPv6-mapped IPv4 normalization in action.yml Python SSRF checks to
  prevent bypass via ::ffff:10.0.0.1 style AAAA records.

- Reject URLs with user-info (user:pass@host) in action.yml Python checks
  to match validate-url subcommand behavior.

- Add test verifying DefaultTransport settings are preserved.
This commit is contained in:
claw
2026-05-14 04:49:21 -07:00
parent 5ac93bea70
commit 934c6728ee
3 changed files with 66 additions and 5 deletions
+50
View File
@@ -1369,3 +1369,53 @@ func TestSetHTTPClient_NilRestoresSafeTransport(t *testing.T) {
t.Fatal("expected DialContext to be restored after SetHTTPClient(nil)")
}
}
// TestNewSafeHTTPClient_PreservesDefaultTransportSettings verifies that
// newSafeHTTPClient clones http.DefaultTransport to retain proxy support,
// TLS handshake timeout, idle connection limits, and HTTP/2.
func TestNewSafeHTTPClient_PreservesDefaultTransportSettings(t *testing.T) {
c := NewClient("https://gitea.example.com", "token")
transport, ok := c.http.Transport.(*http.Transport)
if !ok {
t.Fatalf("expected *http.Transport, got %T", c.http.Transport)
}
defaults := http.DefaultTransport.(*http.Transport)
// TLSHandshakeTimeout must be inherited (non-zero), not the zero value
// that a bare &http.Transport{} would have.
if transport.TLSHandshakeTimeout == 0 {
t.Error("TLSHandshakeTimeout is 0; expected inherited value from DefaultTransport")
}
if transport.TLSHandshakeTimeout != defaults.TLSHandshakeTimeout {
t.Errorf("TLSHandshakeTimeout = %v, want %v", transport.TLSHandshakeTimeout, defaults.TLSHandshakeTimeout)
}
// IdleConnTimeout must be inherited.
if transport.IdleConnTimeout == 0 {
t.Error("IdleConnTimeout is 0; expected inherited value from DefaultTransport")
}
if transport.IdleConnTimeout != defaults.IdleConnTimeout {
t.Errorf("IdleConnTimeout = %v, want %v", transport.IdleConnTimeout, defaults.IdleConnTimeout)
}
// MaxIdleConns must be inherited.
if transport.MaxIdleConns == 0 {
t.Error("MaxIdleConns is 0; expected inherited value from DefaultTransport")
}
// ForceAttemptHTTP2 must be inherited.
if !transport.ForceAttemptHTTP2 {
t.Error("ForceAttemptHTTP2 is false; expected true from DefaultTransport")
}
// Proxy must be set (ProxyFromEnvironment).
if transport.Proxy == nil {
t.Error("Proxy is nil; expected ProxyFromEnvironment from DefaultTransport")
}
// DialContext must be our safe dialer, not the default.
if transport.DialContext == nil {
t.Error("DialContext is nil; expected safeDialContext")
}
}