From 25678421c581ceb5b266f74c806b5532d5b9bb3b Mon Sep 17 00:00:00 2001 From: Matt Braun Date: Wed, 9 Sep 2026 12:10:57 -0500 Subject: [PATCH] Deny link-local and unspecified upstream addresses The dial guard covered private, loopback, and fdaa::/8 but not 169.254.0.0/16 and fe80::/10, which is where cloud metadata services live, nor 0.0.0.0 and ::, which the kernel treats as loopback. --- tokenizer.go | 17 +++++++++++------ tokenizer_test.go | 5 +++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tokenizer.go b/tokenizer.go index dd08b4d..6698233 100644 --- a/tokenizer.go +++ b/tokenizer.go @@ -51,8 +51,8 @@ type tokenizer struct { // RequireFlySrc will reject requests without a fly-src when set. RequireFlySrc bool - // allowPrivateUpstreams disables the denial of private, loopback, and - // fdaa::/8 upstream addresses. Only for tests. + // allowPrivateUpstreams disables the denial of private, loopback, + // unspecified, link-local, and fdaa::/8 upstream addresses. Only for tests. allowPrivateUpstreams bool // tokenizerHostnames is a list of hostnames where tokenizer can be reached. @@ -81,8 +81,9 @@ func OpenProxy() Option { } } -// AllowPrivateUpstreams permits dialing private, loopback, and fdaa::/8 -// upstream addresses. Only for tests that run their upstream on loopback. +// AllowPrivateUpstreams permits dialing private, loopback, unspecified, +// link-local, and fdaa::/8 upstream addresses. Only for tests that run their +// upstream on loopback. func AllowPrivateUpstreams() Option { return func(t *tokenizer) { t.allowPrivateUpstreams = true @@ -503,8 +504,8 @@ func errorResponse(err error) *http.Response { // dialFunc returns a function for dialing network addresses. Ours does a few // special things. -// - It denies connections to private, loopback, and fdaa::/8 addresses -// unless allowPrivate is set. +// - It denies connections to private, loopback, unspecified, link-local, and +// fdaa::/8 addresses unless allowPrivate is set. // - It denies connections to the given set of IP addresses. This is to prevent // circular requests back to tokenizer. Invalid IPs are ignored. // - It rejects requests for TLS upstreams. We need to see/modify requests, so @@ -543,6 +544,10 @@ func dialFunc(badAddrs []string, allowPrivate bool) func(string, string) (net.Co return fmt.Errorf("%w: dialing private address %s denied", ErrBadRequest, address) case ip.IsLoopback(): return fmt.Errorf("%w: dialing loopback address %s denied", ErrBadRequest, address) + case ip.IsUnspecified(): + return fmt.Errorf("%w: dialing unspecified address %s denied", ErrBadRequest, address) + case ip.IsLinkLocalUnicast(), ip.IsLinkLocalMulticast(), ip.IsInterfaceLocalMulticast(): + return fmt.Errorf("%w: dialing link-local address %s denied", ErrBadRequest, address) case fdaaNet.Contains(ip): return fmt.Errorf("%w: dialing fdaa::/8 address %s denied", ErrBadRequest, address) default: diff --git a/tokenizer_test.go b/tokenizer_test.go index d3934c9..429fe2e 100644 --- a/tokenizer_test.go +++ b/tokenizer_test.go @@ -821,6 +821,11 @@ func TestDialFuncDeniesInternalAddresses(t *testing.T) { {"fdaa without bad addrs", nil, false, "[fdaa::1]:1"}, {"loopback with bad addrs", []string{"203.0.113.5"}, false, "127.0.0.1:1"}, {"bad addr", []string{"203.0.113.5"}, false, "203.0.113.5:1"}, + {"link-local v4", nil, false, "169.254.169.254:1"}, + {"link-local v6", nil, false, "[fe80::1]:1"}, + {"unspecified v4", nil, false, "0.0.0.0:1"}, + {"unspecified v6", nil, false, "[::]:1"}, + {"ipv4-mapped loopback", nil, false, "[::ffff:127.0.0.1]:1"}, {"bad addr with private allowed", []string{"203.0.113.5"}, true, "203.0.113.5:1"}, }