Commit e3cd3c4236
Verified · cmc
Layout: unified · split
internal/httpd/checkorigin_test.go added +54
| @@ -0,0 +1,54 @@ | ||
| 1 | package httpd | |
| 2 | ||
| 3 | import ( | |
| 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: | |
| 19 | var 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 | ||
| 31 | func TestMutatingRoutesRequireCheckOrigin(t *testing.T) { | |
| 32 | cfg := config.Default() | |
| 33 | cfg.Web.Mode = "accounts" // superset of routes | |
| 34 | cfg.API.Enabled = true | |
| 35 | s := New(cfg, nil) | |
| 36 | ||
| 37 | for _, r := range s.Routes() { | |
| 38 | if !r.Mutating { | |
| 39 | continue | |
| 40 | } | |
| 41 | key := r.Method + " " + r.Pattern | |
| 42 | if _, exempt := checkOriginAllowlist[key]; exempt { | |
| 43 | continue | |
| 44 | } | |
| 45 | req := httptest.NewRequest(r.Method, "http://example.com/", nil) | |
| 46 | req.Header.Set("Origin", "https://evil.example") | |
| 47 | rr := httptest.NewRecorder() | |
| 48 | r.Handler(rr, req) | |
| 49 | if rr.Code != http.StatusForbidden { | |
| 50 | t.Errorf("%s: cross-site Origin got status %d, want %d (missing checkOrigin?)", | |
| 51 | key, rr.Code, http.StatusForbidden) | |
| 52 | } | |
| 53 | } | |
| 54 | } | |
internal/httpd/logindisabled_test.go added +56
| @@ -0,0 +1,56 @@ | ||
| 1 | package httpd | |
| 2 | ||
| 3 | import ( | |
| 4 | "net/http/httptest" | |
| 5 | "strings" | |
| 6 | "testing" | |
| 7 | "time" | |
| 8 | ||
| 9 | "gitbay.org/gitbay/internal/config" | |
| 10 | "gitbay.org/gitbay/internal/store" | |
| 11 | ) | |
| 12 | ||
| 13 | // login() re-reads the user after ConsumeLoginToken and refuses a disabled | |
| 14 | // account with the same badLoginToken page a bad token gets (#155, #156). | |
| 15 | // SetUserDisabled also deletes login_tokens, which would mask this guard if | |
| 16 | // the test went through it, so the token is inserted directly and the row | |
| 17 | // is disabled with a bare UPDATE — bypassing SetUserDisabled entirely. | |
| 18 | func TestLoginRefusesTokenForDisabledAccount(t *testing.T) { | |
| 19 | st, err := store.Open(":memory:") | |
| 20 | if err != nil { | |
| 21 | t.Fatal(err) | |
| 22 | } | |
| 23 | defer st.Close() | |
| 24 | if err := st.MigrateUp(); err != nil { | |
| 25 | t.Fatal(err) | |
| 26 | } | |
| 27 | ||
| 28 | uid, err := st.CreateUser("alice", false) | |
| 29 | if err != nil { | |
| 30 | t.Fatal(err) | |
| 31 | } | |
| 32 | tok, hash, err := store.NewToken() | |
| 33 | if err != nil { | |
| 34 | t.Fatal(err) | |
| 35 | } | |
| 36 | if err := st.CreateLoginToken(uid, hash, time.Hour); err != nil { | |
| 37 | t.Fatal(err) | |
| 38 | } | |
| 39 | if _, err := st.DB.Exec("UPDATE users SET disabled = 1 WHERE id = ?", uid); err != nil { | |
| 40 | t.Fatal(err) | |
| 41 | } | |
| 42 | ||
| 43 | s := New(config.Default(), st) | |
| 44 | rr := httptest.NewRecorder() | |
| 45 | req := httptest.NewRequest("GET", "/login?token="+tok, nil) | |
| 46 | s.login(rr, req) | |
| 47 | ||
| 48 | for _, c := range rr.Result().Cookies() { | |
| 49 | if c.Name == sessionCookie { | |
| 50 | t.Fatalf("login set a session cookie for a disabled account: %+v", c) | |
| 51 | } | |
| 52 | } | |
| 53 | if !strings.Contains(rr.Body.String(), badLoginToken) { | |
| 54 | t.Errorf("body = %q, want the bad-token page", rr.Body.String()) | |
| 55 | } | |
| 56 | } | |