| 1 | package connect |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/base64" |
| 6 | "encoding/json" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "testing" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | func TestHomeResourceReturnSurvivesSetupAndRefusesTamper(t *testing.T) { |
| 14 | _, n := navigationFixture(t) |
| 15 | now := time.Unix(1900000000, 0) |
| 16 | n.now = func() time.Time { return now } |
| 17 | const target = "/d/private-resource?view=compact" |
| 18 | w := httptest.NewRecorder() |
| 19 | if !n.rememberReturn(w, target) { |
| 20 | t.Fatal("return intent refused") |
| 21 | } |
| 22 | cookies := w.Result().Cookies() |
| 23 | if len(cookies) != 1 || !cookies[0].Secure || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteLaxMode || cookies[0].MaxAge != 600 { |
| 24 | t.Fatal("unsafe navigation cookie") |
| 25 | } |
| 26 | c := cookies[0] |
| 27 | r := httptest.NewRequest("GET", navigationOrigin+"/account/home", nil) |
| 28 | r.AddCookie(c) |
| 29 | if got := n.returnPath(r); got != target { |
| 30 | t.Fatal("resource lost across setup", got) |
| 31 | } |
| 32 | raw, _ := base64.RawURLEncoding.DecodeString(c.Value) |
| 33 | if bytes.Contains(raw, []byte("private-resource")) { |
| 34 | t.Fatal("resource stored as readable cookie bytes") |
| 35 | } |
| 36 | raw[len(raw)-1] ^= 1 |
| 37 | bad := *c |
| 38 | bad.Value = base64.RawURLEncoding.EncodeToString(raw) |
| 39 | tampered := httptest.NewRequest("GET", navigationOrigin+"/account/home", nil) |
| 40 | tampered.AddCookie(&bad) |
| 41 | if n.returnPath(tampered) != "/" { |
| 42 | t.Fatal("tampered return accepted") |
| 43 | } |
| 44 | other := *n |
| 45 | other.origin = "https://another.docs.example.com" |
| 46 | if other.returnPath(r) != "/" { |
| 47 | t.Fatal("other team return accepted") |
| 48 | } |
| 49 | duplicate := r.Clone(r.Context()) |
| 50 | duplicate.AddCookie(c) |
| 51 | if n.returnPath(duplicate) != "/" { |
| 52 | t.Fatal("ambiguous return accepted") |
| 53 | } |
| 54 | now = now.Add(600 * time.Second) |
| 55 | if n.returnPath(r) != "/" { |
| 56 | t.Fatal("expired return accepted") |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | func TestHomeResourceReturnValidatesSealedPathAndLifetime(t *testing.T) { |
| 61 | _, n := navigationFixture(t) |
| 62 | now := int64(1900000000) |
| 63 | n.now = func() time.Time { return time.Unix(now, 0) } |
| 64 | for _, v := range []homeReturnIntent{ |
| 65 | {"https://other.example", now, now + 600}, |
| 66 | {"//other.example", now, now + 600}, |
| 67 | {"/auth/start", now, now + 600}, |
| 68 | {"/d/ok", now + 1, now + 600}, |
| 69 | {"/d/ok", now, now + 601}, |
| 70 | } { |
| 71 | payload, _ := json.Marshal(v) |
| 72 | sealed, err := n.sealer.Seal(n.namespace+".home-return.v1:"+n.origin, payload) |
| 73 | if err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | r := httptest.NewRequest("GET", navigationOrigin+"/account/home", nil) |
| 77 | r.AddCookie(&http.Cookie{Name: homeReturnCookie, Value: base64.RawURLEncoding.EncodeToString(sealed)}) |
| 78 | if n.returnPath(r) != "/" { |
| 79 | t.Fatal("unsafe sealed navigation accepted") |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | |