oficina / suite Public

Clone
git clone https://amadan.net/oficina/suite

Plain git — no account needed to clone.

Download

Download this file

1package suite
2
3import (
4 "context"
5 "crypto/tls"
6 "crypto/x509"
7 "net"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12)
13
14// recordingDialer stands in for the network. It records every address a
15// client tried to reach and refuses to connect, so a test can assert on the
16// attempt rather than on the outcome. Gate 4 is about connections not made.
17type recordingDialer struct{ dialed []string }
18
19func (d *recordingDialer) dial(ctx context.Context, network, addr string) (net.Conn, error) {
20 d.dialed = append(d.dialed, addr)
21 return nil, errRefusedByTest
22}
23
24var errRefusedByTest = &net.OpError{Op: "dial", Err: errTest("no network in tests")}
25
26type errTest string
27
28func (e errTest) Error() string { return string(e) }
29
30// Gate 4. A host whose <team> label differs from the caller's own is refused
31// before any connection is attempted. §4.1: the barrier stops mis-provisioning
32// and accidental cross-team dialling, and it is claimed as nothing more.
33func TestSiblingClientRefusesForeignTeamLabel(t *testing.T) {
34 d := &recordingDialer{}
35 c := NewSiblingClient("acme", d.dial)
36
37 _, err := c.Get("https://acme-evil.docs.oficina.works/s3/doc-1")
38
39 if err == nil {
40 t.Fatal("got nil error dialling another team's label, want a refusal")
41 }
42 if len(d.dialed) != 0 {
43 t.Errorf("dialled %v, want no connection attempted at all", d.dialed)
44 }
45}
46
47// The suffix attack: a host that merely ends with something resembling the
48// suite's domain. `acme.docs.oficina.works.evil` is not in oficina.works at
49// all, and a naive prefix check on the first label would wave it through.
50func TestSiblingClientRefusesSuffixAttack(t *testing.T) {
51 d := &recordingDialer{}
52 c := NewSiblingClient("acme", d.dial)
53
54 _, err := c.Get("https://acme.docs.oficina.works.evil/s3/doc-1")
55
56 if err == nil {
57 t.Fatal("got nil error for a host outside the suite domain, want a refusal")
58 }
59 if len(d.dialed) != 0 {
60 t.Errorf("dialled %v, want no connection attempted at all", d.dialed)
61 }
62}
63
64// A same-team sibling is reached. Without this the guard could refuse
65// everything and still pass the two tests above.
66func TestSiblingClientReachesSameTeamHost(t *testing.T) {
67 d := &recordingDialer{}
68 c := NewSiblingClient("acme", d.dial)
69
70 _, _ = c.Get("https://acme.docs.oficina.works/s3/doc-1")
71
72 if len(d.dialed) != 1 {
73 t.Fatalf("dialled %v, want exactly one connection to the sibling", d.dialed)
74 }
75 if !strings.HasPrefix(d.dialed[0], "acme.docs.oficina.works:") {
76 t.Errorf("dialled %q, want the same-team host", d.dialed[0])
77 }
78}
79
80// The Host header is a second authority field, and the guard read only the
81// URL. Go sends req.Host as the Host header while dialling req.URL.Host, so a
82// request can dial our own team and announce itself as another team's — and a
83// platform router that routes on Host could hand the body and the consumer
84// token to that other team. Reported by the GPT-6 module review, finding 1.
85func TestSiblingClientRefusesForeignHostOverride(t *testing.T) {
86 d := &recordingDialer{}
87 c := NewSiblingClient("acme", d.dial)
88
89 req, err := http.NewRequest("POST", "https://acme.docs.oficina.works/suite/s3", nil)
90 if err != nil {
91 t.Fatalf("building request: %v", err)
92 }
93 req.Host = "other.docs.oficina.works"
94
95 if _, err := c.Do(req); err == nil {
96 t.Fatal("got nil error for a foreign Host override, want a refusal")
97 }
98 if len(d.dialed) != 0 {
99 t.Errorf("dialled %v, want no connection attempted at all", d.dialed)
100 }
101}
102
103// A Host override naming our own host is harmless and must still work, so the
104// check cannot simply ban the field.
105func TestSiblingClientAllowsMatchingHostOverride(t *testing.T) {
106 d := &recordingDialer{}
107 c := NewSiblingClient("acme", d.dial)
108
109 req, _ := http.NewRequest("POST", "https://acme.docs.oficina.works/suite/s3", nil)
110 req.Host = "acme.docs.oficina.works"
111
112 _, _ = c.Do(req)
113
114 if len(d.dialed) != 1 {
115 t.Errorf("dialled %v, want the request to proceed", d.dialed)
116 }
117}
118
119// Gate 4's redirect half, run against a real 302 rather than by calling
120// CheckRedirect directly. The fixture supplies the hosts, and the dialer
121// counts hops: exactly one connection, to the allowed host, and no follow-up
122// to the label it was pointed at. Reported by the GPT-6 module review,
123// finding 10 — the old test never produced a 3xx and never counted anything.
124func TestSiblingClientRefusesAFixtureRedirect(t *testing.T) {
125 f := loadHostileHosts(t)
126
127 // A server that answers every request with a redirect to another team.
128 srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129 http.Redirect(w, r, "http://"+f.Redirect.To+"/suite/s3/doc-1", http.StatusFound)
130 }))
131 defer srv.Close()
132 srvAddr := strings.TrimPrefix(srv.URL, "https://")
133
134 // Every name resolves to the test server, so the only thing stopping a
135 // second hop is the client's own refusal — not the absence of a host.
136 var dialed []string
137 dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
138 dialed = append(dialed, addr)
139 return net.Dial(network, srvAddr)
140 }
141
142 c := NewSiblingClient(f.Team, dial)
143 roots := x509.NewCertPool()
144 roots.AddCert(srv.Certificate())
145 c.Transport.(*teamBoundTransport).next.(*http.Transport).TLSClientConfig = &tls.Config{
146 RootCAs: roots, ServerName: srv.Certificate().DNSNames[0],
147 }
148 resp, err := c.Get("https://" + f.Redirect.From + "/suite/s3/doc-1")
149 if resp != nil {
150 resp.Body.Close()
151 }
152
153 if err == nil {
154 t.Error("got nil error following a redirect to another team, want a refusal")
155 } else if !strings.Contains(err.Error(), "refusing to follow a redirect") {
156 t.Errorf("request failed before reaching the redirect: %v", err)
157 }
158 if len(dialed) != 1 {
159 t.Fatalf("dialled %v, want exactly one hop — the allowed host, and no follow-up", dialed)
160 }
161 if !strings.HasPrefix(dialed[0], f.Redirect.From+":") {
162 t.Errorf("dialled %q, want the allowed host %q", dialed[0], f.Redirect.From)
163 }
164}
165
166// Redirects are never followed. An allowed host must be reached to be told to
167// redirect — that connection is legitimate and gate 4 counts follow-ups, not
168// first hops — but following one would let a sibling walk the client across
169// the team boundary the first check just enforced.
170func TestSiblingClientDoesNotFollowRedirects(t *testing.T) {
171 c := NewSiblingClient("acme", nil)
172
173 req, err := http.NewRequest("GET", "https://acme.docs.oficina.works/s3/doc-1", nil)
174 if err != nil {
175 t.Fatalf("building request: %v", err)
176 }
177 via := []*http.Request{req}
178
179 if err := c.CheckRedirect(req, via); err == nil {
180 t.Error("got nil from CheckRedirect, want every redirect refused")
181 }
182}
183