返回 DeepSeek-Reasonix
hostguard_test.go
根目录 / internal / serve / hostguard_test.go
1 package serve
2
3 import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8
9 "reasonix/internal/config"
10 "reasonix/internal/control"
11 )
12
13 // The DNS-rebinding scenario from the advisory: a page served from
14 // evil.example is re-pointed at 127.0.0.1, so its fetches arrive same-origin
15 // with Host: evil.example:8787. hostGuard must reject them before csrfGuard
16 // or any route sees the request — the content-type check is worthless once
17 // the attacker is same-origin.
18 func TestHostGuardBlocksReboundHost(t *testing.T) {
19 s := New(control.New(control.Options{}), NewBroadcaster(), config.ServeConfig{})
20 h := s.Handler()
21
22 for _, host := range []string{"evil.example", "evil.example:8787", "10.0.0.99", "[::ffff:10.0.0.99]:8787"} {
23 req := httptest.NewRequest(http.MethodPost, "/bypass", strings.NewReader(`{"on":true}`))
24 req.Host = host
25 req.Header.Set("Content-Type", "application/json")
26 rec := httptest.NewRecorder()
27 h.ServeHTTP(rec, req)
28 if rec.Code != http.StatusMisdirectedRequest {
29 t.Fatalf("POST /bypass with Host %q = %d, want %d", host, rec.Code, http.StatusMisdirectedRequest)
30 }
31
32 req = httptest.NewRequest(http.MethodGet, "/history", nil)
33 req.Host = host
34 rec = httptest.NewRecorder()
35 h.ServeHTTP(rec, req)
36 if rec.Code != http.StatusMisdirectedRequest {
37 t.Fatalf("GET /history with Host %q = %d, want %d", host, rec.Code, http.StatusMisdirectedRequest)
38 }
39 }
40 }
41
42 func TestHostGuardAllowsLoopbackHosts(t *testing.T) {
43 s := New(control.New(control.Options{}), NewBroadcaster(), config.ServeConfig{})
44 h := s.Handler()
45
46 for _, host := range []string{"127.0.0.1", "127.0.0.1:8787", "localhost", "localhost:8787", "[::1]:8787", ""} {
47 req := httptest.NewRequest(http.MethodGet, "/status", nil)
48 req.Host = host
49 rec := httptest.NewRecorder()
50 h.ServeHTTP(rec, req)
51 if rec.Code == http.StatusMisdirectedRequest {
52 t.Fatalf("GET /status with Host %q rejected as misdirected", host)
53 }
54 }
55 }
56
57 func TestHostGuardAllowsListenHostAndWildcardBind(t *testing.T) {
58 s := New(control.New(control.Options{}), NewBroadcaster(), config.ServeConfig{})
59 s.setListenAddr("10.1.2.3:8787")
60 h := s.Handler()
61
62 req := httptest.NewRequest(http.MethodGet, "/status", nil)
63 req.Host = "10.1.2.3:8787"
64 rec := httptest.NewRecorder()
65 h.ServeHTTP(rec, req)
66 if rec.Code == http.StatusMisdirectedRequest {
67 t.Fatal("request via the specific listen host was rejected")
68 }
69
70 req = httptest.NewRequest(http.MethodGet, "/status", nil)
71 req.Host = "192.168.0.20:8787"
72 rec = httptest.NewRecorder()
73 h.ServeHTTP(rec, req)
74 if rec.Code != http.StatusMisdirectedRequest {
75 t.Fatal("request via an unrelated LAN host was accepted")
76 }
77
78 // A wildcard bind deliberately exposes the server; Host policing is off.
79 wild := New(control.New(control.Options{}), NewBroadcaster(), config.ServeConfig{})
80 wild.setListenAddr("0.0.0.0:8787")
81 req = httptest.NewRequest(http.MethodGet, "/status", nil)
82 req.Host = "anything.example:8787"
83 rec = httptest.NewRecorder()
84 wild.Handler().ServeHTTP(rec, req)
85 if rec.Code == http.StatusMisdirectedRequest {
86 t.Fatal("wildcard bind rejected a foreign Host")
87 }
88 }
89
90 func TestHostGuardExemptsBehindProxy(t *testing.T) {
91 s := New(control.New(control.Options{}), NewBroadcaster(), config.ServeConfig{
92 AuthMode: "token",
93 Token: "serve-token",
94 BehindProxy: true,
95 })
96 s.setListenAddr("127.0.0.1:8787")
97 h := s.Handler()
98
99 // The public hostname passes the host guard; auth still applies.
100 req := httptest.NewRequest(http.MethodGet, "/status", nil)
101 req.Host = "agent.internal.example:8787"
102 rec := httptest.NewRecorder()
103 h.ServeHTTP(rec, req)
104 if rec.Code == http.StatusMisdirectedRequest {
105 t.Fatal("behind_proxy deployment rejected on public hostname")
106 }
107 }
108
109 func TestHostGuardDoesNotExemptUnauthenticatedProxyMode(t *testing.T) {
110 s := New(control.New(control.Options{}), NewBroadcaster(), config.ServeConfig{
111 AuthMode: "none",
112 BehindProxy: true,
113 })
114 s.setListenAddr("127.0.0.1:8787")
115
116 req := httptest.NewRequest(http.MethodGet, "/history", nil)
117 req.Host = "evil.example:8787"
118 rec := httptest.NewRecorder()
119 s.Handler().ServeHTTP(rec, req)
120 if rec.Code != http.StatusMisdirectedRequest {
121 t.Fatalf("GET /history = %d, want %d", rec.Code, http.StatusMisdirectedRequest)
122 }
123 }
124
125 func TestSetListenAddrBracketedIPv6(t *testing.T) {
126 s := New(control.New(control.Options{}), NewBroadcaster(), config.ServeConfig{})
127 s.setListenAddr("[::1]:8787")
128 if s.hostGate.listenHost != "::1" {
129 t.Fatalf("listenHost = %q, want ::1", s.hostGate.listenHost)
130 }
131 if s.hostGate.allowAny {
132 t.Fatal("::1 bind must not be treated as wildcard")
133 }
134 req := httptest.NewRequest(http.MethodGet, "/status", nil)
135 req.Host = "[::1]:8787"
136 rec := httptest.NewRecorder()
137 s.Handler().ServeHTTP(rec, req)
138 if rec.Code == http.StatusMisdirectedRequest {
139 t.Fatal("loopback IPv6 listen host rejected")
140 }
141 }
142
142 lines GO