url_safety.go 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "net/url"
  7. "strings"
  8. "time"
  9. "github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
  10. )
  11. // SanitizeHTTPURL validates and normalizes an http(s) URL without resolving
  12. // DNS. Use SanitizePublicHTTPURL at the point of an outbound request.
  13. func SanitizeHTTPURL(raw string) (string, error) {
  14. raw = strings.TrimSpace(raw)
  15. if raw == "" {
  16. return "", nil
  17. }
  18. u, err := url.Parse(raw)
  19. if err != nil {
  20. return "", err
  21. }
  22. if u.Scheme != "http" && u.Scheme != "https" {
  23. return "", fmt.Errorf("unsupported URL scheme %q", u.Scheme)
  24. }
  25. if u.Host == "" || u.Hostname() == "" {
  26. return "", fmt.Errorf("URL host is required")
  27. }
  28. clean := &url.URL{
  29. Scheme: u.Scheme,
  30. Host: u.Host,
  31. Path: u.Path,
  32. RawPath: u.RawPath,
  33. RawQuery: u.RawQuery,
  34. Fragment: u.Fragment,
  35. }
  36. return clean.String(), nil
  37. }
  38. // SanitizePublicHTTPURL validates and normalizes an http(s) URL, then blocks
  39. // private/internal targets unless the caller explicitly allows them.
  40. func SanitizePublicHTTPURL(raw string, allowPrivate bool) (string, error) {
  41. clean, err := SanitizeHTTPURL(raw)
  42. if err != nil || clean == "" {
  43. return clean, err
  44. }
  45. if allowPrivate {
  46. return clean, nil
  47. }
  48. u, err := url.Parse(clean)
  49. if err != nil {
  50. return "", err
  51. }
  52. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  53. defer cancel()
  54. if err := rejectPrivateHost(ctx, u.Hostname()); err != nil {
  55. return "", err
  56. }
  57. return clean, nil
  58. }
  59. func rejectPrivateHost(ctx context.Context, hostname string) error {
  60. if ip := net.ParseIP(hostname); ip != nil {
  61. if isBlockedIP(ip) {
  62. return fmt.Errorf("blocked private/internal address %s", ip.String())
  63. }
  64. return nil
  65. }
  66. ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
  67. if err != nil {
  68. return fmt.Errorf("cannot resolve host %s: %w", hostname, err)
  69. }
  70. if len(ips) == 0 {
  71. return fmt.Errorf("host %s has no IP addresses", hostname)
  72. }
  73. for _, ipAddr := range ips {
  74. if isBlockedIP(ipAddr.IP) {
  75. return fmt.Errorf("host %s resolves to blocked private/internal address %s", hostname, ipAddr.IP.String())
  76. }
  77. }
  78. return nil
  79. }
  80. func isBlockedIP(ip net.IP) bool {
  81. return netsafe.IsBlockedIP(ip)
  82. }