fix(webhook): block unspecified-address destinations in the SSRF guard (#6284)

Co-authored-by: hktitof <hktitof@users.noreply.github.com>
This commit is contained in:
Abdellatif Anaflous 2026-09-07 13:13:10 +01:00 committed by GitHub
parent 9da3765b19
commit 607dc28413
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 38 additions and 2 deletions

View file

@ -14,14 +14,17 @@ import (
// reservedNetworks lists IP ranges blocked by default for outbound webhook requests.
// Explicit allowlist entries or the deprecated blanket override may permit them. The
// ranges cover loopback, RFC-1918 private, link-local (including cloud IMDS at
// 169.254.169.254), and their IPv6 equivalents.
// ranges cover "this network" (the kernel treats it as the local host), loopback,
// RFC-1918 private, link-local (including cloud IMDS at 169.254.169.254), and their
// IPv6 equivalents.
var reservedNetworks = []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/8"), // "this network" (RFC 791), dials the local host
netip.MustParsePrefix("127.0.0.0/8"), // IPv4 loopback
netip.MustParsePrefix("10.0.0.0/8"), // RFC-1918 class A
netip.MustParsePrefix("172.16.0.0/12"), // RFC-1918 class B
netip.MustParsePrefix("192.168.0.0/16"), // RFC-1918 class C
netip.MustParsePrefix("169.254.0.0/16"), // Link-local / cloud IMDS
netip.MustParsePrefix("::/128"), // IPv6 unspecified address, dials the local host
netip.MustParsePrefix("::1/128"), // IPv6 loopback
netip.MustParsePrefix("fc00::/7"), // IPv6 unique local
netip.MustParsePrefix("fe80::/10"), // IPv6 link-local

View file

@ -300,3 +300,36 @@ func TestPostWithoutSecretSetsNoSignatureHeaders(t *testing.T) {
require.NoError(t, err)
require.False(t, hasSignatureHeaders, "no signature headers should be set when no secret is configured")
}
// TestPostRejectsUnspecifiedAddressDestination pins that a webhook URL pointing at the
// unspecified address is refused at dial time, bcs the kernel treats it as the local host.
func TestPostRejectsUnspecifiedAddressDestination(t *testing.T) {
resetPrivateDestinationPolicy(t)
var receivedRequest atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
receivedRequest.Store(true)
_, _ = w.Write([]byte(`{"code":0}`))
}))
defer server.Close()
_, port, err := net.SplitHostPort(strings.TrimPrefix(server.URL, "http://"))
require.NoError(t, err)
err = Post(&WebhookRequestPayload{
URL: "http://0.0.0.0:" + port,
ActivityType: "memos.memo.created",
Creator: "users/1",
})
require.Error(t, err)
require.False(t, receivedRequest.Load(), "the webhook must not reach a local service through the unspecified address")
}
// TestValidateURLRejectsUnspecifiedAddress ensures webhook URL validation also refuses
// the unspecified address, so such URLs never even reach the dispatch path.
func TestValidateURLRejectsUnspecifiedAddress(t *testing.T) {
resetPrivateDestinationPolicy(t)
require.Error(t, ValidateURL("http://0.0.0.0:8080/hook"))
require.Error(t, ValidateURL("http://[::]:8080/hook"))
}