Commit 8a379d4719

8a379d4719bb3447b0fcfe42fc5164fcc019696b

parent: d0e26d3df9

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 08:59 UTC

web: Cache-Control: no-store on the login-link request

Ref #261

Layout: unified · split

CHANGELOG.org +3
@@ -107,6 +107,9 @@ for the eighteen commands whose CLI path differs from the registry's
107 directly, so a refusal reaches the viewer as a message instead of 107 directly, so a refusal reaches the viewer as a message instead of
108 being dropped. The watch button now cycles three states — default, 108 being dropped. The watch button now cycles three states — default,
109 watching, muted — instead of two (#261). 109 watching, muted — instead of two (#261).
110- The response that consumes a login link's =?token== sends
111 =Cache-Control: no-store=, so no intermediary keeps a copy of the
112 single-use URL (#261).
110 113
111* v1.36.0 — 2026-09-23 114* v1.36.0 — 2026-09-23
112 115
internal/httpd/accounts.go +4
@@ -121,6 +121,10 @@ func (s *Server) loginSubmit(w http.ResponseWriter, r *http.Request) {
121} 121}
122 122
123func (s *Server) login(w http.ResponseWriter, r *http.Request) { 123func (s *Server) login(w http.ResponseWriter, r *http.Request) {
124 // token, when present, is a single-use secret in the query string —
125 // the documented exception to "never in a URL" (Threat-Model). No
126 // cache may keep a copy of this response.
127 w.Header().Set("Cache-Control", "no-store")
124 token := r.URL.Query().Get("token") 128 token := r.URL.Query().Get("token")
125 if token == "" { 129 if token == "" {
126 s.renderLogin(w, "", false, s.peekNext(r)) 130 s.renderLogin(w, "", false, s.peekNext(r))
internal/httpd/logincookie_test.go +24
@@ -2,9 +2,11 @@ package httpd
2 2
3import ( 3import (
4 "net/http" 4 "net/http"
5 "net/http/httptest"
5 "testing" 6 "testing"
6 7
7 "gitbay.org/gitbay/internal/config" 8 "gitbay.org/gitbay/internal/config"
9 "gitbay.org/gitbay/internal/store"
8) 10)
9 11
10// The session cookie must be Lax, not Strict. A login link clicked in a mail 12// The session cookie must be Lax, not Strict. A login link clicked in a mail
@@ -36,3 +38,25 @@ func TestSessionCookieAttributes(t *testing.T) {
36 } 38 }
37 } 39 }
38} 40}
41
42// The login link's token rides in the query string — the one
43// documented exception to "never in a URL" — so the response that
44// consumes it must never be cached by an intermediary that might log
45// or replay the URL (#261).
46func TestLoginNoStoreHeader(t *testing.T) {
47 st, err := store.Open(":memory:")
48 if err != nil {
49 t.Fatal(err)
50 }
51 defer st.Close()
52 if err := st.MigrateUp(); err != nil {
53 t.Fatal(err)
54 }
55 s := New(config.Default(), st)
56 rr := httptest.NewRecorder()
57 req := httptest.NewRequest("GET", "/login?token=bogus", nil)
58 s.login(rr, req)
59 if got := rr.Header().Get("Cache-Control"); got != "no-store" {
60 t.Errorf("Cache-Control = %q, want no-store", got)
61 }
62}