fix(#123): address all review findings from PR #129
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
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)
This commit is contained in:
@@ -206,7 +206,10 @@ runs:
|
||||
'try: rs=socket.getaddrinfo(h,None)' \
|
||||
'except socket.gaierror as e: print(f"DNS error: {e}",file=sys.stderr); sys.exit(1)' \
|
||||
'if not rs: print("Error: no addresses",file=sys.stderr); sys.exit(1)' \
|
||||
'[sys.exit(1) or print(f"blocked: {a}",file=sys.stderr) for _,_,_,_,(a,*_) in rs if (lambda ip:ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved)(ipaddress.ip_address(a))]' \
|
||||
'for _,_,_,_,(a,*_) in rs:' \
|
||||
' ip=ipaddress.ip_address(a)' \
|
||||
' if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved:' \
|
||||
' print(f"blocked: {a}",file=sys.stderr); sys.exit(1)' \
|
||||
> /tmp/_ssrf_check.py
|
||||
CHECK_URL="${SERVER_URL}" python3 /tmp/_ssrf_check.py || {
|
||||
echo "Error: SERVER_URL '${SERVER_URL}' resolves to a private/reserved IP address" >&2
|
||||
@@ -332,10 +335,30 @@ runs:
|
||||
ACTION_TOKEN="${ACTION_TOKEN:-}"
|
||||
BINARY="review-bot-${OS}-${ARCH}"
|
||||
|
||||
# SECURITY: SERVER_URL was validated (scheme + IP check) in "Determine version".
|
||||
# All curl calls in this step use the same SERVER_URL, so that pre-check covers them.
|
||||
# The installed binary uses safeDialContext for additional defense-in-depth in
|
||||
# the "Run review" step.
|
||||
# SECURITY: Re-validate SERVER_URL at the start of this step to mitigate DNS
|
||||
# rebinding attacks. A DNS TTL expiry between "Determine version" and here
|
||||
# could allow an attacker to change the resolved IP to a private/reserved
|
||||
# address, causing curl to send ACTION_TOKEN to an internal host.
|
||||
# Only needed on Gitea path (VCS_TYPE=gitea); GitHub/GHES uses platform-controlled URLs.
|
||||
if [ "$VCS_TYPE" = "gitea" ]; then
|
||||
printf '%s\n' \
|
||||
'import socket,ipaddress,sys,os' \
|
||||
'from urllib.parse import urlparse' \
|
||||
'u=os.environ["CHECK_URL"]; h=urlparse(u).hostname' \
|
||||
'(print("Error: no hostname",file=sys.stderr) or sys.exit(2)) if not h else None' \
|
||||
'try: rs=socket.getaddrinfo(h,None)' \
|
||||
'except socket.gaierror as e: print(f"DNS error: {e}",file=sys.stderr); sys.exit(1)' \
|
||||
'if not rs: print("Error: no addresses",file=sys.stderr); sys.exit(1)' \
|
||||
'for _,_,_,_,(a,*_) in rs:' \
|
||||
' ip=ipaddress.ip_address(a)' \
|
||||
' if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved:' \
|
||||
' print(f"blocked: {a}",file=sys.stderr); sys.exit(1)' \
|
||||
> /tmp/_ssrf_check_install.py
|
||||
CHECK_URL="${SERVER_URL}" python3 /tmp/_ssrf_check_install.py || {
|
||||
echo "Error: SERVER_URL '${SERVER_URL}' resolves to a private/reserved IP address" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ "$VCS_TYPE" = "github" ]; then
|
||||
# GitHub/GHES: Use REST API for release asset downloads.
|
||||
|
||||
Reference in New Issue
Block a user