fix(phase3): skip templateless domains in scheduler; block CGNAT range in webhook SSRF guard
Domains imported without a template (TemplateID == nil) are a valid, unconfigured state, not a failure — RunOnce now skips them before calling checkDomain instead of letting LoadDomain's "no template" error turn into StatusError and a spammy unknown->error notification. isBlockedIP now also rejects 100.64.0.0/10 (RFC 6598 carrier-grade NAT), which net.IP.IsPrivate() does not cover, closing an SSRF gap in the webhook destination guard (both the pre-request check and the per-dial check use isBlockedIP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
@@ -3,6 +3,7 @@ package notify
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -195,6 +196,31 @@ func TestDialControlBlocksActualConnectingAddress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBlockedIPCGNATRange(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ip string
|
||||
blocked bool
|
||||
}{
|
||||
{"cgnat start", "100.64.0.1", true},
|
||||
{"cgnat end", "100.127.255.255", true},
|
||||
{"just below cgnat", "100.63.255.255", false},
|
||||
{"just above cgnat", "100.128.0.0", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ip := net.ParseIP(tc.ip)
|
||||
if ip == nil {
|
||||
t.Fatalf("failed to parse %q", tc.ip)
|
||||
}
|
||||
got := isBlockedIP(ip)
|
||||
if got != tc.blocked {
|
||||
t.Fatalf("isBlockedIP(%q) = %v, want %v", tc.ip, got, tc.blocked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialControlAllowsEverythingWhenAllowPrivate(t *testing.T) {
|
||||
control := dialControl(true)
|
||||
if err := control("tcp", "127.0.0.1:80", nil); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user