feat(#123): add IP-level SSRF defense to Gitea client and action
CI / test (pull_request) Successful in 18s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 44s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m57s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 2m21s

## Changes

### Go: IP-level SSRF protection in gitea.Client (primary defense)
- Add gitea/ipcheck.go with IsBlockedIP() covering all blocked CIDR ranges:
  loopback (127.0.0.0/8, ::1), RFC1918 (10/8, 172.16/12, 192.168/16),
  link-local (169.254/16, fe80::/10), ULA (fc00::/7), CGN (100.64/10),
  multicast, reserved, and unspecified ranges.
- IPv6-mapped IPv4 addresses (::ffff:x.x.x.x) are normalized before checking.
- Add safeDialContext to gitea.Client: resolves DNS, rejects any IP in a
  blocked CIDR, then dials the resolved IP directly to narrow the DNS rebinding
  window. NewClient now uses this safe transport by default.
- Add WithUnsafeDialer() for test code using httptest.Server (127.0.0.1).
- Update NewTestClient helper in export_test.go for all gitea unit tests.
- Update SetHTTPClient(nil) to restore the safe transport (not the plain one).

### Go: validate-url subcommand (defense-in-depth for future bash callers)
- Add 'review-bot validate-url <url>' subcommand: validates https scheme,
  no user-info, resolves hostname, rejects any blocked IP.
- Exit 0=safe, 1=blocked, 2=validation error/dns failure.
- Add outWriter/errWriter vars to main.go for testable output capture.

### action.yml: Python3 IP check in 'Determine version' step
- After the https scheme validation, resolve SERVER_URL hostname with
  socket.getaddrinfo and reject any result where
  ipaddress.ip_address(ip).is_private/is_loopback/is_link_local/etc. is true.
- python3 is required on ubuntu-* runners (noted in existing comments).
- Covers the version-check curl that sends ACTION_TOKEN to SERVER_URL.
- SERVER_URL for install-step curls is covered by the same pre-check.

### Tests
- gitea/ipcheck_test.go: 30+ cases covering all blocked families + public IPs
- gitea/client_test.go: safe transport presence, WithUnsafeDialer, SSRF blocking
- cmd/review-bot/validateurl_test.go: scheme validation, user-info, exit codes

Closes #123
This commit is contained in:
2026-05-14 07:38:29 +00:00
parent 50facefdd6
commit 8c8f3ab4b3
12 changed files with 952 additions and 51 deletions
+2 -2
View File
@@ -31,7 +31,7 @@ func TestPostReview_WithComments(t *testing.T) {
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client := NewTestClient(server.URL, "test-token")
comments := []ReviewComment{
{Path: "main.go", NewPosition: 42, Body: "[MAJOR] Something bad"},
{Path: "util.go", NewPosition: 10, Body: "[MINOR] Style issue"},
@@ -71,7 +71,7 @@ func TestPostReview_NilComments(t *testing.T) {
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client := NewTestClient(server.URL, "test-token")
_, err := client.PostReview(context.Background(), "owner", "repo", 1, "APPROVED", "all good", "", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)