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:
parent
9da3765b19
commit
607dc28413
2 changed files with 38 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue