From 615d9e67ebe15a08b43a50cb8ff2c9cdd66abf78 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Fri, 1 May 2026 22:53:37 +0800 Subject: [PATCH] fix: validate probe target host shape --- .../http/handler/tunnel_probe_target.go | 65 +++++++++++++++++-- .../http/handler/tunnel_probe_target_test.go | 33 ++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/go-backend/internal/http/handler/tunnel_probe_target.go b/go-backend/internal/http/handler/tunnel_probe_target.go index be8e234..6a03284 100644 --- a/go-backend/internal/http/handler/tunnel_probe_target.go +++ b/go-backend/internal/http/handler/tunnel_probe_target.go @@ -25,10 +25,6 @@ func defaultTunnelProbeTarget() tunnelProbeTarget { func normalizeTunnelProbeTarget(host string, port int) (tunnelProbeTarget, bool, error) { host = strings.TrimSpace(host) - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") - } - if host == "" && port == 0 { return defaultTunnelProbeTarget(), false, nil } @@ -38,13 +34,72 @@ func normalizeTunnelProbeTarget(host string, port int) (tunnelProbeTarget, bool, if port <= 0 || port > 65535 { return tunnelProbeTarget{}, false, errors.New("测试目标端口必须是 1-65535") } - if strings.Contains(host, "://") || isTunnelProbeTargetSchemeLikeHost(host) || strings.ContainsAny(host, "/?#") || strings.ContainsAny(host, " \t\r\n") { + if strings.Contains(host, "://") || strings.ContainsAny(host, "/?#") || strings.ContainsAny(host, " \t\r\n") || isTunnelProbeTargetSchemeLikeHost(host) { return tunnelProbeTarget{}, false, errors.New("测试目标 Host 不能包含协议或路径") } + if normalized, ok := normalizeTunnelProbeTargetHost(host); ok { + host = normalized + } else { + return tunnelProbeTarget{}, false, errors.New("测试目标 Host 格式无效") + } return tunnelProbeTarget{Host: host, Port: port}, true, nil } +func normalizeTunnelProbeTargetHost(host string) (string, bool) { + if strings.HasPrefix(host, "[") || strings.HasSuffix(host, "]") { + if !strings.HasPrefix(host, "[") || !strings.HasSuffix(host, "]") { + return "", false + } + inner := strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") + addr, err := netip.ParseAddr(inner) + if err != nil || !addr.Is6() { + return "", false + } + return inner, true + } + + if addr, err := netip.ParseAddr(host); err == nil { + return addr.String(), true + } + if strings.Contains(host, ":") || isTunnelProbeTargetIPv4Like(host) { + return "", false + } + if !isValidTunnelProbeTargetHost(host) { + return "", false + } + return host, true +} + +func isValidTunnelProbeTargetHost(host string) bool { + if host == "" || len(host) > 253 { + return false + } + for _, label := range strings.Split(host, ".") { + if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { + return false + } + for _, r := range label { + if !isASCIILetter(r) && !isASCIIDigit(r) && r != '-' { + return false + } + } + } + return true +} + +func isTunnelProbeTargetIPv4Like(host string) bool { + if host == "" { + return false + } + for _, r := range host { + if !isASCIIDigit(r) && r != '.' { + return false + } + } + return strings.Contains(host, ".") +} + func isTunnelProbeTargetSchemeLikeHost(host string) bool { if _, err := netip.ParseAddr(host); err == nil { return false diff --git a/go-backend/internal/http/handler/tunnel_probe_target_test.go b/go-backend/internal/http/handler/tunnel_probe_target_test.go index df0ecc9..ded971a 100644 --- a/go-backend/internal/http/handler/tunnel_probe_target_test.go +++ b/go-backend/internal/http/handler/tunnel_probe_target_test.go @@ -72,6 +72,39 @@ func TestNormalizeTunnelProbeTargetRejectsSchemePrefixButAllowsIPv6(t *testing.T } } +func TestNormalizeTunnelProbeTargetValidatesHostShape(t *testing.T) { + validHosts := []string{ + "example.com", + "localhost", + "api-1.example.co.uk", + "192.0.2.10", + "2001:db8::1", + "[2001:db8::1]", + } + for _, host := range validHosts { + if _, _, err := normalizeTunnelProbeTarget(host, 443); err != nil { + t.Fatalf("expected valid host %q: %v", host, err) + } + } + + invalidHosts := []string{ + "1:2:3", + "[2001:db8::1", + "2001:db8::1]", + "[example.com]", + "example..com", + "-example.com", + "example-.com", + "exa_mple.com", + "999.1.1.1", + } + for _, host := range invalidHosts { + if _, _, err := normalizeTunnelProbeTarget(host, 443); err == nil { + t.Fatalf("expected invalid host %q to be rejected", host) + } + } +} + func TestParseTunnelProbeTargetFromRequest(t *testing.T) { req := map[string]interface{}{ "probeTargetHost": "speed.example.com",