Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

3.0 kB 91 lines
1package activitypub 2 3import ( 4 "context" 5 "fmt" 6 "net" 7 "net/http" 8 "net/url" 9 "time" 10) 11 12// maxResponseBody caps how much of a remote response this package will 13// ever read into memory — a hostile or misbehaving server (notably one 14// controlled by whoever sent us a Follow) gets cut off rather than able to 15// exhaust memory. 16const maxResponseBody = 1 << 20 // 1 MiB 17 18// ValidateExternalURL rejects non-https URLs and the most obvious 19// local/loopback hostnames before any network call is made. A cheap first 20// check ahead of the dialer-level one NewSafeClient performs — every 21// outbound target here (a Follow's actor, an actor's inbox URL, ...) is 22// attacker-influenced, so both checks matter: this one for a fast, clear 23// rejection; the dialer check to close the DNS-rebinding gap a 24// resolve-then-separately-connect check would otherwise leave open. 25func ValidateExternalURL(rawURL string) error { 26 u, err := url.Parse(rawURL) 27 if err != nil { 28 return fmt.Errorf("invalid URL: %w", err) 29 } 30 if u.Scheme != "https" { 31 return fmt.Errorf("only https URLs are allowed, got %q", u.Scheme) 32 } 33 host := u.Hostname() 34 if host == "" { 35 return fmt.Errorf("URL has no host") 36 } 37 if host == "localhost" { 38 return fmt.Errorf("refusing to connect to localhost") 39 } 40 if ip := net.ParseIP(host); ip != nil && isUnsafeIP(ip) { 41 return fmt.Errorf("refusing to connect to disallowed address %s", ip) 42 } 43 return nil 44} 45 46// NewSafeClient returns an http.Client hardened for outbound requests to 47// attacker-influenced URLs: no redirects are followed (a validated URL 48// could otherwise redirect somewhere unvalidated), and every connection's 49// actually-resolved IP is checked against private/loopback/link-local 50// ranges immediately before dialing it — not just at an earlier 51// validation step, which a DNS answer that changes between check and 52// connect could bypass. 53func NewSafeClient(timeout time.Duration) *http.Client { 54 dialer := &net.Dialer{Timeout: 10 * time.Second} 55 transport := &http.Transport{ 56 DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 57 host, port, err := net.SplitHostPort(addr) 58 if err != nil { 59 return nil, err 60 } 61 ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host) 62 if err != nil { 63 return nil, fmt.Errorf("resolve %s: %w", host, err) 64 } 65 var safe net.IP 66 for _, ip := range ips { 67 if isUnsafeIP(ip) { 68 continue 69 } 70 safe = ip 71 break 72 } 73 if safe == nil { 74 return nil, fmt.Errorf("refusing to connect to %s: no public address resolved", host) 75 } 76 return dialer.DialContext(ctx, network, net.JoinHostPort(safe.String(), port)) 77 }, 78 } 79 return &http.Client{ 80 Timeout: timeout, 81 Transport: transport, 82 CheckRedirect: func(req *http.Request, via []*http.Request) error { 83 return http.ErrUseLastResponse 84 }, 85 } 86} 87 88func isUnsafeIP(ip net.IP) bool { 89 return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || 90 ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast() 91}