diff --git a/modules/netcheck/cmd/netcheck/request.go b/modules/netcheck/cmd/netcheck/request.go index fb9287d3..af6327d1 100644 --- a/modules/netcheck/cmd/netcheck/request.go +++ b/modules/netcheck/cmd/netcheck/request.go @@ -82,6 +82,39 @@ func guardDial(_, address string, _ syscall.RawConn) error { return nil } +// shortFormAddress: a host made only of numeric labels (decimal, octal or 0x hex) that net/netip does not +// take as an address. The C resolver expands these (127.1 is 127.0.0.1, 2130706433 too), so the tool +// refuses them on its own words before any lookup rather than rely on the resolver. +func shortFormAddress(host string) bool { + if host == "" || strings.Contains(host, ":") { + return false + } + if _, err := netip.ParseAddr(host); err == nil { + return false + } + for _, label := range strings.Split(strings.TrimSuffix(host, "."), ".") { + digits := label + if len(label) > 2 && (label[:2] == "0x" || label[:2] == "0X") { + digits = label[2:] + for _, c := range digits { + if !strings.ContainsRune("0123456789abcdefABCDEF", c) { + return false + } + } + continue + } + if digits == "" { + return false + } + for _, c := range digits { + if c < '0' || c > '9' { + return false + } + } + } + return true +} + func runRequest(args map[string]any) (any, error) { raw, err := text(args, "url") if err != nil { @@ -97,6 +130,9 @@ func runRequest(args map[string]any) (any, error) { if u.Host == "" { return nil, fmt.Errorf("url %q has no host", raw) } + if shortFormAddress(u.Hostname()) { + return nil, fmt.Errorf("refused: host %q is an address in a short or legacy form (as 127.1, 2130706433 or 0x7f.0.0.1), which the resolver may read as loopback, unspecified or link-local; write the full address", u.Hostname()) + } method := "GET" if m, _ := args["method"].(string); strings.TrimSpace(m) != "" { method = strings.ToUpper(strings.TrimSpace(m)) diff --git a/modules/netcheck/cmd/netcheck/request_test.go b/modules/netcheck/cmd/netcheck/request_test.go index 15015a48..2bcb41e7 100644 --- a/modules/netcheck/cmd/netcheck/request_test.go +++ b/modules/netcheck/cmd/netcheck/request_test.go @@ -34,7 +34,7 @@ func TestRequestRefusesLocalAddresses(t *testing.T) { port := srv.Listener.Addr().(*net.TCPAddr).Port refusing(t) for _, host := range []string{ - "127.0.0.1", "localhost", "127.1", "[::1]", "[::ffff:127.0.0.1]", "0.0.0.0", "[::]", + "127.0.0.1", "localhost", "127.1", "2130706433", "0x7f.0.0.1", "0x7f.1", "0177.0.0.1", "[::1]", "[::ffff:127.0.0.1]", "0.0.0.0", "[::]", "169.254.169.254", "[fe80::1]", } { u := "http://" + host + ":" + strconv.Itoa(port) + "/" @@ -45,6 +45,20 @@ func TestRequestRefusesLocalAddresses(t *testing.T) { } } +func TestShortFormAddressIsRefusedOnItsOwnWords(t *testing.T) { + for _, host := range []string{"127.1", "2130706433", "0x7f.0.0.1", "0x7f.1", "0177.0.0.1"} { + _, err := asked(t, map[string]any{"url": "http://" + host + ":1/", "timeout_ms": float64(2000)}) + if err == nil || !strings.Contains(err.Error(), "short or legacy form") { + t.Errorf("%s: want the short-form refusal, got %v", host, err) + } + } + for _, host := range []string{"example.com", "127.0.0.1", "1.example.com", "[::1]", "10.0.0.1"} { + if shortFormAddress(strings.Trim(host, "[]")) { + t.Errorf("%s taken for a short form", host) + } + } +} + func TestRequestIgnoresProxyEnvironment(t *testing.T) { t.Setenv("HTTP_PROXY", "http://127.0.0.1:1") t.Setenv("http_proxy", "http://127.0.0.1:1")