This repository has no description
2.8 kB
108 lines
1package serververify
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "net/http"
8 "net/http/httptest"
9 "sync/atomic"
10 "testing"
11 "time"
12)
13
14const ssrfExpectedOwner = "did:plc:ssrfguardexpectedowner"
15
16func TestRunVerificationRejectsNonPublicDestinationsInProd(t *testing.T) {
17 loopbackDomain, loopbackHits := localOwnerEndpoint(t, "127.0.0.1")
18
19 cases := []struct {
20 name string
21 domain string
22 hits *atomic.Int32
23 }{
24 {
25 name: "loopback address with a real owner endpoint",
26 domain: loopbackDomain,
27 hits: loopbackHits,
28 },
29 {
30 name: "private address",
31 domain: "10.0.0.1:80",
32 },
33 {
34 name: "link-local metadata address",
35 domain: "169.254.169.254:80",
36 },
37 {
38 name: "reserved unspecified address",
39 domain: "0.0.0.0:80",
40 },
41 }
42
43 for _, tc := range cases {
44 t.Run(tc.name, func(t *testing.T) {
45 if tc.hits != nil {
46 tc.hits.Store(0)
47 }
48
49 ctx, cancel := context.WithTimeout(context.Background(), 750*time.Millisecond)
50 defer cancel()
51
52 started := time.Now()
53 err := RunVerification(ctx, tc.domain, ssrfExpectedOwner, false)
54 elapsed := time.Since(started)
55
56 if err == nil {
57 t.Fatalf("RunVerification(%q, dev=false) succeeded; non-public destinations must be refused", tc.domain)
58 }
59 if elapsed > 250*time.Millisecond {
60 t.Fatalf("RunVerification(%q, dev=false) took %s; want an immediate SSRF refusal, not network IO until timeout", tc.domain, elapsed)
61 }
62 if tc.hits != nil && tc.hits.Load() != 0 {
63 t.Fatalf("RunVerification(%q, dev=false) reached the owner endpoint %d time(s); guard must refuse before normal network IO", tc.domain, tc.hits.Load())
64 }
65 })
66 }
67}
68
69func TestRunVerificationAllowsNonPublicDestinationsInDev(t *testing.T) {
70 loopbackDomain, loopbackHits := localOwnerEndpoint(t, "127.0.0.1")
71
72 ctx, cancel := context.WithTimeout(context.Background(), 750*time.Millisecond)
73 defer cancel()
74
75 err := RunVerification(ctx, loopbackDomain, ssrfExpectedOwner, true)
76 if err != nil {
77 t.Fatalf("RunVerification(%q, dev=true) failed: %v", loopbackDomain, err)
78 }
79
80 if loopbackHits.Load() != 1 {
81 t.Fatalf("RunVerification(%q, dev=true) did not reach owner endpoint", loopbackDomain)
82 }
83}
84
85func localOwnerEndpoint(t *testing.T, host string) (string, *atomic.Int32) {
86 t.Helper()
87
88 ln, err := net.Listen("tcp", net.JoinHostPort(host, "0"))
89 if err != nil {
90 t.Fatalf("listen on %s: %v", host, err)
91 }
92
93 var hits atomic.Int32
94 server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
95 hits.Add(1)
96 if r.URL.Path != "/xrpc/sh.tangled.owner" {
97 http.NotFound(w, r)
98 return
99 }
100 w.Header().Set("Content-Type", "application/json")
101 fmt.Fprintf(w, `{"owner":%q}`, ssrfExpectedOwner)
102 }))
103 server.Listener = ln
104 server.Start()
105 t.Cleanup(server.Close)
106
107 return ln.Addr().String(), &hits
108}