Fix #138: add Secure attribute to viewer cookie (vwr) #154

Merged
fen merged 1 commits from fix-138 into dev 2026-09-10 13:43:21 +00:00
2 changed files with 71 additions and 1 deletions
Showing only changes of commit 521b6f8011 - Show all commits
+67
View File
@@ -0,0 +1,67 @@
package api
// #138: the vwr viewer cookie must carry the Secure attribute. Tests inspect
// the Set-Cookie header directly rather than relying on cookie round-tripping,
// because Go's HTTP client (and browsers) drop Secure cookies over plain HTTP,
// which is how tests and local dev run.
import (
"net/http/httptest"
"strings"
"testing"
"palette/internal/store"
"palette/internal/web"
)
func newTestServer138(t *testing.T) *httptest.ResponseRecorder {
t.Helper()
st, err := store.OpenStore(":memory:")
if err != nil {
t.Fatal(err)
}
ui, err := web.New()
if err != nil {
t.Fatal(err)
}
cfg := Config{MaxTextBytes: 5 * 1024 * 1024}
ss := NewTestSettingsStore(t, cfg)
globalSettingsFn = ss.get
t.Cleanup(func() { globalSettingsFn = nil })
a := &apiServer{store: st, cfg: cfg, ui: ui, settings: ss, adminKey: "test-admin-key"}
req := httptest.NewRequest("GET", "/history", nil)
rec := httptest.NewRecorder()
a.routes().ServeHTTP(rec, req)
return rec
}
func TestViewerCookieAttributes(t *testing.T) {
rec := newTestServer138(t)
var vwr *string
for _, c := range rec.Result().Cookies() {
if c.Name == "vwr" {
cc := c
vwr = &cc.Value
if !c.Secure {
t.Error("vwr cookie missing Secure attribute (#138)")
}
if !c.HttpOnly {
t.Error("vwr cookie missing HttpOnly attribute")
}
if c.Path != "/" {
t.Errorf("vwr cookie Path = %q, want /", c.Path)
}
if c.SameSite != 2 { // http.SameSiteLaxMode
t.Errorf("vwr cookie SameSite = %v, want Lax", c.SameSite)
}
}
}
if vwr == nil {
t.Fatal("no vwr cookie set")
}
// also confirm the raw header form spells out Secure
sc := rec.Header().Get("Set-Cookie")
if !strings.Contains(sc, "Secure") {
t.Errorf("Set-Cookie header %q lacks Secure", sc)
}
}
+4 -1
View File
@@ -141,9 +141,12 @@ func viewerCookieMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("vwr"); err != nil || c.Value == "" { if c, err := r.Cookie("vwr"); err != nil || c.Value == "" {
id := store.GenSlug(16) id := store.GenSlug(16)
// #138: Secure keeps the viewer id off plain-HTTP requests
// (all deployments are HTTPS-only behind traefik).
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "vwr", Value: id, Path: "/", Name: "vwr", Value: id, Path: "/",
MaxAge: 31536000, HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 31536000, HttpOnly: true, Secure: true,
SameSite: http.SameSiteLaxMode,
}) })
r.AddCookie(&http.Cookie{Name: "vwr", Value: id}) r.AddCookie(&http.Cookie{Name: "vwr", Value: id})
// remember that this cookie was minted here, not sent by the client // remember that this cookie was minted here, not sent by the client