package suite import ( "context" "crypto/tls" "crypto/x509" "net" "net/http" "net/http/httptest" "strings" "testing" ) // recordingDialer stands in for the network. It records every address a // client tried to reach and refuses to connect, so a test can assert on the // attempt rather than on the outcome. Gate 4 is about connections not made. type recordingDialer struct{ dialed []string } func (d *recordingDialer) dial(ctx context.Context, network, addr string) (net.Conn, error) { d.dialed = append(d.dialed, addr) return nil, errRefusedByTest } var errRefusedByTest = &net.OpError{Op: "dial", Err: errTest("no network in tests")} type errTest string func (e errTest) Error() string { return string(e) } // Gate 4. A host whose label differs from the caller's own is refused // before any connection is attempted. §4.1: the barrier stops mis-provisioning // and accidental cross-team dialling, and it is claimed as nothing more. func TestSiblingClientRefusesForeignTeamLabel(t *testing.T) { d := &recordingDialer{} c := NewSiblingClient("acme", d.dial) _, err := c.Get("https://acme-evil.docs.oficina.works/s3/doc-1") if err == nil { t.Fatal("got nil error dialling another team's label, want a refusal") } if len(d.dialed) != 0 { t.Errorf("dialled %v, want no connection attempted at all", d.dialed) } } // The suffix attack: a host that merely ends with something resembling the // suite's domain. `acme.docs.oficina.works.evil` is not in oficina.works at // all, and a naive prefix check on the first label would wave it through. func TestSiblingClientRefusesSuffixAttack(t *testing.T) { d := &recordingDialer{} c := NewSiblingClient("acme", d.dial) _, err := c.Get("https://acme.docs.oficina.works.evil/s3/doc-1") if err == nil { t.Fatal("got nil error for a host outside the suite domain, want a refusal") } if len(d.dialed) != 0 { t.Errorf("dialled %v, want no connection attempted at all", d.dialed) } } // A same-team sibling is reached. Without this the guard could refuse // everything and still pass the two tests above. func TestSiblingClientReachesSameTeamHost(t *testing.T) { d := &recordingDialer{} c := NewSiblingClient("acme", d.dial) _, _ = c.Get("https://acme.docs.oficina.works/s3/doc-1") if len(d.dialed) != 1 { t.Fatalf("dialled %v, want exactly one connection to the sibling", d.dialed) } if !strings.HasPrefix(d.dialed[0], "acme.docs.oficina.works:") { t.Errorf("dialled %q, want the same-team host", d.dialed[0]) } } // The Host header is a second authority field, and the guard read only the // URL. Go sends req.Host as the Host header while dialling req.URL.Host, so a // request can dial our own team and announce itself as another team's — and a // platform router that routes on Host could hand the body and the consumer // token to that other team. Reported by the GPT-6 module review, finding 1. func TestSiblingClientRefusesForeignHostOverride(t *testing.T) { d := &recordingDialer{} c := NewSiblingClient("acme", d.dial) req, err := http.NewRequest("POST", "https://acme.docs.oficina.works/suite/s3", nil) if err != nil { t.Fatalf("building request: %v", err) } req.Host = "other.docs.oficina.works" if _, err := c.Do(req); err == nil { t.Fatal("got nil error for a foreign Host override, want a refusal") } if len(d.dialed) != 0 { t.Errorf("dialled %v, want no connection attempted at all", d.dialed) } } // A Host override naming our own host is harmless and must still work, so the // check cannot simply ban the field. func TestSiblingClientAllowsMatchingHostOverride(t *testing.T) { d := &recordingDialer{} c := NewSiblingClient("acme", d.dial) req, _ := http.NewRequest("POST", "https://acme.docs.oficina.works/suite/s3", nil) req.Host = "acme.docs.oficina.works" _, _ = c.Do(req) if len(d.dialed) != 1 { t.Errorf("dialled %v, want the request to proceed", d.dialed) } } // Gate 4's redirect half, run against a real 302 rather than by calling // CheckRedirect directly. The fixture supplies the hosts, and the dialer // counts hops: exactly one connection, to the allowed host, and no follow-up // to the label it was pointed at. Reported by the GPT-6 module review, // finding 10 — the old test never produced a 3xx and never counted anything. func TestSiblingClientRefusesAFixtureRedirect(t *testing.T) { f := loadHostileHosts(t) // A server that answers every request with a redirect to another team. srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "http://"+f.Redirect.To+"/suite/s3/doc-1", http.StatusFound) })) defer srv.Close() srvAddr := strings.TrimPrefix(srv.URL, "https://") // Every name resolves to the test server, so the only thing stopping a // second hop is the client's own refusal — not the absence of a host. var dialed []string dial := func(ctx context.Context, network, addr string) (net.Conn, error) { dialed = append(dialed, addr) return net.Dial(network, srvAddr) } c := NewSiblingClient(f.Team, dial) roots := x509.NewCertPool() roots.AddCert(srv.Certificate()) c.Transport.(*teamBoundTransport).next.(*http.Transport).TLSClientConfig = &tls.Config{ RootCAs: roots, ServerName: srv.Certificate().DNSNames[0], } resp, err := c.Get("https://" + f.Redirect.From + "/suite/s3/doc-1") if resp != nil { resp.Body.Close() } if err == nil { t.Error("got nil error following a redirect to another team, want a refusal") } else if !strings.Contains(err.Error(), "refusing to follow a redirect") { t.Errorf("request failed before reaching the redirect: %v", err) } if len(dialed) != 1 { t.Fatalf("dialled %v, want exactly one hop — the allowed host, and no follow-up", dialed) } if !strings.HasPrefix(dialed[0], f.Redirect.From+":") { t.Errorf("dialled %q, want the allowed host %q", dialed[0], f.Redirect.From) } } // Redirects are never followed. An allowed host must be reached to be told to // redirect — that connection is legitimate and gate 4 counts follow-ups, not // first hops — but following one would let a sibling walk the client across // the team boundary the first check just enforced. func TestSiblingClientDoesNotFollowRedirects(t *testing.T) { c := NewSiblingClient("acme", nil) req, err := http.NewRequest("GET", "https://acme.docs.oficina.works/s3/doc-1", nil) if err != nil { t.Fatalf("building request: %v", err) } via := []*http.Request{req} if err := c.CheckRedirect(req, via); err == nil { t.Error("got nil from CheckRedirect, want every redirect refused") } }