返回 DeepSeek-Reasonix
ssrf.go
1 package installsource
2
3 import (
4 "context"
5 "fmt"
6 "net"
7 "net/http"
8 )
9
10 // ssrfGuardClient wraps base so every fetch refuses to connect to private,
11 // link-local, CGNAT, or unspecified addresses — the SSRF surface a prompt-
12 // injected install source would aim at (cloud metadata at 169.254.169.254,
13 // RFC1918 internal services). Loopback is allowed: the agent can already reach
14 // localhost via bash, and the install tests serve over 127.0.0.1. The check
15 // runs at dial time on the resolved IP and then dials that vetted IP, so a
16 // public host that DNS-rebinds to an internal address is caught too.
17 //
18 // When the transport routes through an HTTP/HTTPS proxy the dial-time check
19 // only sees the proxy address, so the request-level wrapper below also rejects
20 // IP-literal destinations before forwarding — web_fetch's proxy boundary.
21 //
22 // This mirrors web_fetch's guard (internal/tool/builtin/webfetch.go); the
23 // install_source tool fetches the same kind of untrusted URLs and must not be
24 // the one un-guarded path. Kept in sync by hand — both block the same set.
25 func ssrfGuardClient(base *http.Client) *http.Client {
26 guarded := *base // copy Timeout etc.
27 if t, ok := base.Transport.(*http.Transport); ok && t != nil {
28 ct := t.Clone()
29 inner := ct.DialContext
30 if inner == nil {
31 inner = (&net.Dialer{}).DialContext
32 }
33 ct.DialContext = ssrfDial(inner)
34 guarded.Transport = &ssrfRequestGuard{base: ct}
35 } else {
36 // Non-*http.Transport (or nil Transport): build a fresh guarded transport.
37 // The real paths — boot's netclient and the tests' httptest client — are
38 // always *http.Transport, so this branch only covers a bare &http.Client{}.
39 guarded.Transport = &ssrfRequestGuard{base: &http.Transport{DialContext: ssrfDial((&net.Dialer{}).DialContext)}}
40 }
41 return &guarded
42 }
43
44 // ssrfRequestGuard vetoes requests whose destination is a blocked IP literal
45 // before the transport dials anything — including, and mainly for, the proxy
46 // path, where the wrapped DialContext would otherwise validate only the proxy.
47 func (rt *ssrfRequestGuard) RoundTrip(req *http.Request) (*http.Response, error) {
48 if host := req.URL.Hostname(); host != "" {
49 if ip := net.ParseIP(host); ip != nil && blockedFetchIP(ip) {
50 return nil, fmt.Errorf("refusing to fetch internal address %s", host)
51 }
52 }
53 return rt.base.RoundTrip(req)
54 }
55
56 type ssrfRequestGuard struct{ base http.RoundTripper }
57
58 func ssrfDial(inner func(context.Context, string, string) (net.Conn, error)) func(context.Context, string, string) (net.Conn, error) {
59 return func(ctx context.Context, network, addr string) (net.Conn, error) {
60 host, port, err := net.SplitHostPort(addr)
61 if err != nil {
62 return nil, err
63 }
64 ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
65 if err != nil {
66 return nil, err
67 }
68 for _, ip := range ips {
69 if blockedFetchIP(ip.IP) {
70 return nil, fmt.Errorf("refusing to fetch internal address %s (resolves to %s)", host, ip.IP)
71 }
72 }
73 // Dial the vetted IP, not the hostname, so the connection can't re-resolve
74 // to a different (internal) address (DNS rebinding).
75 return inner(ctx, network, net.JoinHostPort(ips[0].IP.String(), port))
76 }
77 }
78
79 // cgnatRange is RFC 6598 shared address space (100.64.0.0/10). Go's IsPrivate
80 // doesn't cover it, yet some clouds host instance metadata there (Alibaba Cloud
81 // at 100.100.100.200), so it's an SSRF target to refuse too.
82 var cgnatRange = mustCIDR("100.64.0.0/10")
83
84 func mustCIDR(s string) *net.IPNet {
85 _, n, err := net.ParseCIDR(s)
86 if err != nil {
87 panic(err)
88 }
89 return n
90 }
91
92 // blockedFetchIP reports whether ip is an address install_source must not reach.
93 // Loopback is intentionally allowed (see ssrfGuardClient).
94 func blockedFetchIP(ip net.IP) bool {
95 return ip.IsPrivate() || // RFC1918 + IPv6 unique-local (fc00::/7)
96 ip.IsLinkLocalUnicast() || // 169.254.0.0/16 (incl. cloud metadata) + fe80::/10
97 ip.IsLinkLocalMulticast() ||
98 ip.IsUnspecified() || // 0.0.0.0 / ::
99 cgnatRange.Contains(ip) // 100.64.0.0/10 (incl. Alibaba Cloud metadata)
100 }
101
101 lines GO