| 1 | diff --git a/internal/daemon/callbacks.go b/internal/daemon/callbacks.go |
| 2 | index e997353..f18e355 100644 |
| 3 | --- a/internal/daemon/callbacks.go |
| 4 | +++ b/internal/daemon/callbacks.go |
| 5 | @@ -205,12 +205,15 @@ func (t *Tenant) checkCallbackURL(raw string) error { |
| 6 | return nil |
| 7 | } |
| 8 | host := strings.ToLower(parsed.Hostname()) |
| 9 | + if ip := net.ParseIP(host); ip != nil { |
| 10 | + if privateAddress(ip) { |
| 11 | + return errors.New("invalid: url must not name a private or loopback address") |
| 12 | + } |
| 13 | + return nil |
| 14 | + } |
| 15 | if host == "localhost" || strings.HasSuffix(host, ".localhost") || strings.HasSuffix(host, ".local") || strings.HasSuffix(host, ".internal") || !strings.Contains(host, ".") { |
| 16 | return errors.New("invalid: url must name a public host") |
| 17 | } |
| 18 | - if ip := net.ParseIP(host); ip != nil && privateAddress(ip) { |
| 19 | - return errors.New("invalid: url must not name a private or loopback address") |
| 20 | - } |
| 21 | return nil |
| 22 | } |
| 23 | |
| 24 | diff --git a/internal/daemon/callbacks_test.go b/internal/daemon/callbacks_test.go |
| 25 | index c8448b9..4ab3713 100644 |
| 26 | --- a/internal/daemon/callbacks_test.go |
| 27 | +++ b/internal/daemon/callbacks_test.go |
| 28 | @@ -100,6 +100,8 @@ func TestCallbackRegistrationValidatesURLFilterScopeAndCap(t *testing.T) { |
| 29 | {"credentials", member, "https://user:pw@hooks.example/wake", `{"kinds":[1]}`, "invalid: url must be https"}, |
| 30 | {"private address", member, "https://10.0.0.1/wake", `{"kinds":[1]}`, "invalid: url must not name a private"}, |
| 31 | {"loopback address", member, "https://127.0.0.1/wake", `{"kinds":[1]}`, "invalid: url must not name a private"}, |
| 32 | + {"loopback ipv6", member, "https://[::1]/wake", `{"kinds":[1]}`, "invalid: url must not name a private"}, |
| 33 | + {"shared address", member, "https://100.64.0.9/wake", `{"kinds":[1]}`, "invalid: url must not name a private"}, |
| 34 | {"localhost", member, "https://localhost/wake", `{"kinds":[1]}`, "invalid: url must name a public host"}, |
| 35 | {"bare host", member, "https://relay/wake", `{"kinds":[1]}`, "invalid: url must name a public host"}, |
| 36 | {"no kinds", member, "https://hooks.example/wake", `{"#p":["` + member + `"]}`, "invalid: filter must name at least one kind"}, |
| 37 | @@ -145,6 +147,8 @@ func TestCallbackRegistrationValidatesURLFilterScopeAndCap(t *testing.T) { |
| 38 | for i := 0; i < 6; i++ { |
| 39 | addCallback(t, tenant, owner, "https://hooks.example/owner/"+strconv.Itoa(i), `{"kinds":[1]}`) |
| 40 | } |
| 41 | + // A public address literal is a host like any other. |
| 42 | + addCallback(t, tenant, owner, "https://[2001:db8::10]/wake", `{"kinds":[1]}`) |
| 43 | // The secret shows once: listings and the table row never repeat it. |
| 44 | listed, err := tenant.Execute(ctx, agent, "listcallbacks", nil) |
| 45 | if err != nil { |