返回 DeepSeek-Reasonix
ssrf_test.go
根目录 / internal / installsource / ssrf_test.go
1 package installsource
2
3 import (
4 "bufio"
5 "context"
6 "net"
7 "net/http"
8 "net/url"
9 "sync"
10 "testing"
11 "time"
12 )
13
14 // recordingProxy is a dummy HTTP proxy: it accepts absolute-form requests,
15 // records the destination the client asked it to reach, and answers 200. It
16 // never contacts the destination, so tests can assert what would have been
17 // forwarded without touching a real internal service.
18 type recordingProxy struct {
19 mu sync.Mutex
20 dests []string
21
22 listener net.Listener
23 }
24
25 func newRecordingProxy(t *testing.T) *recordingProxy {
26 t.Helper()
27 l, err := net.Listen("tcp", "127.0.0.1:0")
28 if err != nil {
29 t.Fatalf("listen: %v", err)
30 }
31 p := &recordingProxy{listener: l}
32 go func() {
33 for {
34 conn, err := l.Accept()
35 if err != nil {
36 return
37 }
38 go p.serve(conn)
39 }
40 }()
41 t.Cleanup(func() { _ = l.Close() })
42 return p
43 }
44
45 func (p *recordingProxy) serve(conn net.Conn) {
46 defer conn.Close()
47 _ = conn.SetDeadline(time.Now().Add(5 * time.Second))
48 req, err := http.ReadRequest(bufio.NewReader(conn))
49 if err != nil {
50 return
51 }
52 if req.URL != nil && req.URL.IsAbs() {
53 p.mu.Lock()
54 p.dests = append(p.dests, req.URL.String())
55 p.mu.Unlock()
56 }
57 _, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok"))
58 }
59
60 func (p *recordingProxy) destinations() []string {
61 p.mu.Lock()
62 defer p.mu.Unlock()
63 return append([]string(nil), p.dests...)
64 }
65
66 func (p *recordingProxy) URL() string {
67 return "http://" + p.listener.Addr().String()
68 }
69
70 func TestSSRFGuardRejectsBlockedIPLiteralThroughProxy(t *testing.T) {
71 proxy := newRecordingProxy(t)
72 transport := &http.Transport{Proxy: func(*http.Request) (*url.URL, error) {
73 return url.Parse(proxy.URL())
74 }}
75 client := ssrfGuardClient(&http.Client{Transport: transport, Timeout: 5 * time.Second})
76
77 for _, target := range []string{
78 "http://169.254.169.254/latest/meta-data/",
79 "http://10.0.0.1/mcp",
80 "http://192.168.0.1/mcp",
81 "http://100.64.0.1/mcp",
82 } {
83 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, target, nil)
84 if err != nil {
85 t.Fatalf("new request: %v", err)
86 }
87 resp, err := client.Do(req)
88 if err == nil {
89 _ = resp.Body.Close()
90 t.Fatalf("request to %s through proxy succeeded; want SSRF refusal", target)
91 }
92 }
93 if got := proxy.destinations(); len(got) != 0 {
94 t.Fatalf("proxy was asked to reach %v; want the guard to refuse before forwarding", got)
95 }
96 }
97
98 func TestSSRFGuardRejectsBlockedIPLiteralDirect(t *testing.T) {
99 client := ssrfGuardClient(&http.Client{Timeout: 2 * time.Second})
100 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://169.254.169.254/latest/meta-data/", nil)
101 if err != nil {
102 t.Fatalf("new request: %v", err)
103 }
104 if _, err := client.Do(req); err == nil {
105 t.Fatal("direct request to blocked IP succeeded; want SSRF refusal")
106 }
107 }
108
109 func TestSSRFGuardAllowsLoopbackThroughProxy(t *testing.T) {
110 proxy := newRecordingProxy(t)
111 transport := &http.Transport{Proxy: func(*http.Request) (*url.URL, error) {
112 return url.Parse(proxy.URL())
113 }}
114 client := ssrfGuardClient(&http.Client{Transport: transport, Timeout: 5 * time.Second})
115
116 // Loopback is deliberately allowed by this guard; the request must reach
117 // the proxy carrying its destination intact.
118 req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:8799/mcp", nil)
119 if err != nil {
120 t.Fatalf("new request: %v", err)
121 }
122 resp, err := client.Do(req)
123 if err != nil {
124 t.Fatalf("loopback request through proxy: %v", err)
125 }
126 _ = resp.Body.Close()
127 if got := proxy.destinations(); len(got) != 1 || got[0] != "http://127.0.0.1:8799/mcp" {
128 t.Fatalf("proxy destinations = %v; want exactly the loopback target", got)
129 }
130 }
131
131 lines GO