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
107107 directly, so a refusal reaches the viewer as a message instead of
108108 being dropped. The watch button now cycles three states — default,
109109 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).
110113
111114* v1.36.0 — 2026-09-23
112115
internal/httpd/accounts.go +4
@@ -121,6 +121,10 @@ func (s *Server) loginSubmit(w http.ResponseWriter, r *http.Request) {
121121}
122122
123123func (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")
124128 token := r.URL.Query().Get("token")
125129 if token == "" {
126130 s.renderLogin(w, "", false, s.peekNext(r))
internal/httpd/logincookie_test.go +24
@@ -2,9 +2,11 @@ package httpd
22
33import (
44 "net/http"
5 "net/http/httptest"
56 "testing"
67
78 "gitbay.org/gitbay/internal/config"
9 "gitbay.org/gitbay/internal/store"
810)
911
1012// The session cookie must be Lax, not Strict. A login link clicked in a mail
@@ -36,3 +38,25 @@ func TestSessionCookieAttributes(t *testing.T) {
3638 }
3739 }
3840}
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}