internal/httpd/checkorigin_test.go
62 lines · 2409 bytes
1package httpd
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "testing"
7
8 "gitbay.org/gitbay/internal/config"
9)
10
11// TestMutatingRoutesRequireCheckOrigin pins the invariant checkOrigin's doc
12// comment rests on (#155, #157): the session cookie is SameSite=Lax, which
13// withholds it from a cross-site POST but not a cross-site top-level GET, so
14// checkOrigin has to be the thing that refuses every other mutating route.
15// checkOrigin is the outermost wrapper, so a cross-site Origin is refused
16// with 403 before any handler logic runs — no session or repository needed.
17//
18// Two routes are exempt on purpose, not by omission:
19var checkOriginAllowlist = map[string]string{
20 // Authenticates only via "Authorization: Bearer ...". A cross-site
21 // browser request cannot attach one, so there is no cookie for
22 // checkOrigin to protect here.
23 "POST /api/v1/cmd": "bearer-token auth, no cookie in play",
24 // Consumes a single-use token from the query string and sets no
25 // cookie on failure; browsers withhold Origin from a cross-site
26 // top-level GET, which is the only way this route is ever reached
27 // cross-site.
28 "GET /login": "single-use token GET, no cookie read",
29}
30
31func TestMutatingRoutesRequireCheckOrigin(t *testing.T) {
32 cfg := config.Default()
33 cfg.Web.Mode = "accounts" // superset of accounts-mode routes
34 cfg.API.Enabled = true
35 // /register is gated on registration.mode != "closed" (routes.go), which
36 // config.Default() leaves at "closed" — the production instance runs
37 // "open", and that is the one deployed value the route table hides its
38 // routes behind if this test's cfg does not open it too. "open" and
39 // "invite" gate the route identically (both are just != "closed"), so
40 // there is no second code path in routes.go for looping over both to
41 // reach; "open" alone matches production and is enough.
42 cfg.Registration.Mode = "open"
43 s := New(cfg, nil, nil)
44
45 for _, r := range s.Routes() {
46 if !r.Mutating {
47 continue
48 }
49 key := r.Method + " " + r.Pattern
50 if _, exempt := checkOriginAllowlist[key]; exempt {
51 continue
52 }
53 req := httptest.NewRequest(r.Method, "http://example.com/", nil)
54 req.Header.Set("Origin", "https://evil.example")
55 rr := httptest.NewRecorder()
56 r.Handler(rr, req)
57 if rr.Code != http.StatusForbidden {
58 t.Errorf("%s: cross-site Origin got status %d, want %d (missing checkOrigin?)",
59 key, rr.Code, http.StatusForbidden)
60 }
61 }
62}