// kafka — a privacy-respecting metasearch engine // Copyright (C) 2026-present metamorphosis-dev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License,// or (at your option) any later version. package util import ( "fmt" "net" "net/url" "strings" ) // SafeURLScheme validates that a URL is well-formed and uses an acceptable scheme. // Returns the parsed URL on success, or an error. func SafeURLScheme(raw string) (*url.URL, error) { u, err := url.Parse(raw) if err != nil { return nil, err } if u.Scheme != "http" && u.Scheme != "https" { return nil, fmt.Errorf("URL must use http or https, got %q", u.Scheme) } return u, nil } // IsPrivateIP returns true if the IP address is in a private, loopback, // link-local, or otherwise non-routable range. func IsPrivateIP(host string) bool { // Strip port if present. h, _, err := net.SplitHostPort(host) if err != nil { h = host } // Resolve hostname to IPs. ips, err := net.LookupIP(h) if err != nil || len(ips) == 0 { // If we can't resolve, reject to be safe. return true } for _, ip := range ips { if isPrivateIPAddr(ip) { return true } } return false } func isPrivateIPAddr(ip net.IP) bool { privateRanges := []struct { network *net.IPNet }{ // Loopback {mustParseCIDR("127.0.0.0/8")}, {mustParseCIDR("::1/128")}, // RFC 1918 {mustParseCIDR("10.0.0.0/8")}, {mustParseCIDR("172.16.0.0/12")}, {mustParseCIDR("192.168.0.0/16")}, // RFC 6598 (Carrier-grade NAT) {mustParseCIDR("100.64.0.0/10")}, // Link-local {mustParseCIDR("169.254.0.0/16")}, {mustParseCIDR("fe80::/10")}, // IPv6 unique local {mustParseCIDR("fc00::/7")}, // IPv4-mapped IPv6 loopback {mustParseCIDR("::ffff:127.0.0.0/104")}, } for _, r := range privateRanges { if r.network.Contains(ip) { return true } } return false } func mustParseCIDR(s string) *net.IPNet { _, network, err := net.ParseCIDR(s) if err != nil { panic(fmt.Sprintf("validate: invalid CIDR %q: %v", s, err)) } return network } // ValidatePublicURL checks that a URL is well-formed, uses http or https, // and does not point to a private/reserved IP range. func ValidatePublicURL(raw string) error { u, err := url.Parse(raw) if err != nil { return fmt.Errorf("invalid URL: %w", err) } if u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("URL must use http or https, got %q", u.Scheme) } if u.Host == "" { return fmt.Errorf("URL must have a host") } if IsPrivateIP(u.Host) { return fmt.Errorf("URL points to a private or reserved address: %s", u.Host) } return nil } // SanitizeResultURL ensures a URL is safe for rendering in an href attribute. // It rejects javascript:, data:, vbscript: and other dangerous schemes. func SanitizeResultURL(raw string) string { if raw == "" { return "" } u, err := url.Parse(raw) if err != nil { return "" } switch strings.ToLower(u.Scheme) { case "http", "https", "": return raw default: return "" } }