Commit 7673081613

76730816139e07cc540bfa576596dfeda68c56b6

parent: 01856d444d

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 08:37 UTC

webhook: CheckAddrs for callers that resolve before connecting

Ref #279

Layout: unified · split

internal/webhook/webhook.go +15 −1
@@ -41,9 +41,23 @@ func ValidateURL(raw string, allowLocal bool) error {
4141 if err != nil {
4242 return fmt.Errorf("cannot resolve %s: %w", u.Hostname(), err)
4343 }
44 if err := CheckAddrs(u.Hostname(), ips, allowLocal); err != nil {
45 return fmt.Errorf("webhook target %s resolves to a private or local address; refusing (SSRF)", u.Hostname())
46 }
47 return nil
48}
49
50// CheckAddrs refuses host when any of its resolved addresses is
51// loopback, private or link-local, unless allowLocal. A caller resolves
52// immediately before connecting and connects only to the addresses it
53// checked.
54func CheckAddrs(host string, ips []net.IP, allowLocal bool) error {
55 if allowLocal {
56 return nil
57 }
4458 for _, ip := range ips {
4559 if isForbidden(ip) {
46 return fmt.Errorf("webhook target %s resolves to a private or local address; refusing (SSRF)", u.Hostname())
60 return fmt.Errorf("%s resolves to private or local address %s; refusing (SSRF)", host, ip)
4761 }
4862 }
4963 return nil
internal/webhook/webhook_test.go added +21
@@ -0,0 +1,21 @@
1package webhook
2
3import (
4 "net"
5 "strings"
6 "testing"
7)
8
9func TestCheckAddrs(t *testing.T) {
10 public := []net.IP{net.ParseIP("203.0.113.5")}
11 mixed := []net.IP{net.ParseIP("203.0.113.5"), net.ParseIP("10.1.2.3")}
12 if err := CheckAddrs("git.example", public, false); err != nil {
13 t.Fatalf("public: %v", err)
14 }
15 if err := CheckAddrs("git.example", mixed, false); err == nil || !strings.Contains(err.Error(), "10.1.2.3") {
16 t.Fatalf("mixed: %v", err)
17 }
18 if err := CheckAddrs("git.example", mixed, true); err != nil {
19 t.Fatalf("allow_local: %v", err)
20 }
21}