f84cc3bbcf
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 34s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m25s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m30s
MAJOR fixes: - gitea/ipcheck.go: replace startup panic with init()+error list pattern Hard-coded CIDRs that fail to parse now recorded in blockedCIDRParseErrors instead of panicking. TestBlockedCIDRsValid catches programming errors in CI without violating CONVENTIONS.md 'never panic' rule. - .gitea/actions/review/action.yml: re-validate SERVER_URL at start of 'Install review-bot' step to close DNS rebinding window between 'Determine version' and install-step curl calls. MINOR fixes: - gitea/client.go: add Timeout: 10*time.Second to net.Dialer per PLAN.md spec - cmd/review-bot/validateurl.go: switch isValidateError to errors.As so wrapped *validateError values are also detected - gitea/ipcheck_test.go: clarify 198.51.100.1 (RFC5737 TEST-NET-2) comment; add TestBlockedCIDRsValid to surface CIDR parse errors as test failures NIT fixes: - .gitea/actions/review/action.yml: refactor Python list comprehension in SSRF check to for-loop (avoids side-effect-only comprehension, runner compat) - gitea/export_test.go: expand comment explaining white-box test pattern (why package gitea not gitea_test, Go stdlib precedent) Remove PLAN.md (implementation complete)
92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
// Package gitea provides a client for the Gitea API.
|
|
// ipcheck.go implements IP-level SSRF protection by checking resolved addresses
|
|
// against known blocked CIDR ranges (RFC1918, loopback, link-local, etc.).
|
|
package gitea
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
)
|
|
|
|
// blockedCIDRStrings is the canonical list of CIDR strings that should never
|
|
// be contacted by review-bot. See IsBlockedIP for the full list of covered
|
|
// address families.
|
|
//
|
|
// These are hard-coded literals: any parse failure is a programming error.
|
|
// Validity is verified by TestBlockedCIDRsValid in ipcheck_test.go.
|
|
var blockedCIDRStrings = []string{
|
|
// IPv4 loopback
|
|
"127.0.0.0/8",
|
|
// IPv4 unspecified / "this network"
|
|
"0.0.0.0/8",
|
|
// RFC1918 private ranges
|
|
"10.0.0.0/8",
|
|
"172.16.0.0/12",
|
|
"192.168.0.0/16",
|
|
// IPv4 link-local (APIPA, also used by AWS instance metadata 169.254.169.254)
|
|
"169.254.0.0/16",
|
|
// IPv4 shared address space (RFC6598, carrier-grade NAT)
|
|
"100.64.0.0/10",
|
|
// IPv4 multicast
|
|
"224.0.0.0/4",
|
|
// IPv4 reserved / broadcast
|
|
"240.0.0.0/4",
|
|
// IPv6 loopback
|
|
"::1/128",
|
|
// IPv6 unspecified
|
|
"::/128",
|
|
// IPv6 link-local
|
|
"fe80::/10",
|
|
// IPv6 unique local (ULA) — RFC4193
|
|
"fc00::/7",
|
|
// IPv6 multicast
|
|
"ff00::/8",
|
|
}
|
|
|
|
// blockedCIDRs is the parsed form of blockedCIDRStrings.
|
|
// Any entry that fails to parse is recorded in blockedCIDRParseErrors instead
|
|
// of panicking; tests verify this slice is always empty via TestBlockedCIDRsValid.
|
|
var (
|
|
blockedCIDRs []*net.IPNet
|
|
blockedCIDRParseErrors []string
|
|
)
|
|
|
|
func init() {
|
|
blockedCIDRs = make([]*net.IPNet, 0, len(blockedCIDRStrings))
|
|
for _, r := range blockedCIDRStrings {
|
|
_, cidr, err := net.ParseCIDR(r)
|
|
if err != nil {
|
|
// Record the error rather than panicking; TestBlockedCIDRsValid
|
|
// will catch this during tests, and the CI build will fail.
|
|
blockedCIDRParseErrors = append(blockedCIDRParseErrors,
|
|
fmt.Sprintf("ipcheck: invalid built-in CIDR %q: %v", r, err))
|
|
continue
|
|
}
|
|
blockedCIDRs = append(blockedCIDRs, cidr)
|
|
}
|
|
}
|
|
|
|
// IsBlockedIP reports whether ip is in a blocked address range.
|
|
// It is exported for use by the validate-url subcommand and tests outside
|
|
// this package.
|
|
//
|
|
// IPv6-mapped IPv4 addresses (e.g. ::ffff:192.168.1.1) are normalized to their
|
|
// IPv4 form before checking so that IPv4 CIDRs catch them.
|
|
//
|
|
// Based on:
|
|
// - RFC1918 private ranges
|
|
// - RFC5735 / RFC4193 special-use IPv4/IPv6 ranges
|
|
// - RFC4291 IPv6 link-local / loopback
|
|
func IsBlockedIP(ip net.IP) bool {
|
|
// Normalize IPv6-mapped IPv4 addresses (::ffff:x.x.x.x) to plain IPv4.
|
|
if v4 := ip.To4(); v4 != nil {
|
|
ip = v4
|
|
}
|
|
for _, cidr := range blockedCIDRs {
|
|
if cidr.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|