Files
felhom.eu/hub/internal/web/r136_host_cookie_test.go
T
admin e221476ff5 R-136: operator session cookie renamed to __Host-hub_session (always Secure, Path=/, no Domain)
A sibling subdomain can no longer toss a session cookie the hub reads first. Operators are logged out
once; plain-HTTP browser login no longer holds a session; Basic auth for scripts is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-05 21:45:04 +02:00

76 lines
3.0 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// R-136: the operator session cookie is `__Host-hub_session` — Secure, Path=/, no Domain — even when the
// login itself arrived over plain HTTP (the browser would reject a non-Secure __Host- cookie; a sibling
// subdomain can never set one). The old `hub_session` name no longer opens a session (the one-time
// logout), and plain-HTTP Basic auth for scripts keeps working.
// RED-PROOF: set SessionCookieName back to "hub_session" → the name/Secure assertions fail.
func TestR136_LoginSetsHostPrefixedCookie(t *testing.T) {
s, _ := serverWithPassword(t, "op-pass")
h := s.RequireAuth(http.HandlerFunc(s.ServeHTTP))
r := httptest.NewRequest(http.MethodPost, "http://hub.local/login", strings.NewReader(url.Values{"password": {"op-pass"}}.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("login = %d", w.Code)
}
var sess *http.Cookie
for _, c := range w.Result().Cookies() {
if c.Name == SessionCookieName {
sess = c
}
}
if SessionCookieName != "__Host-hub_session" || sess == nil {
t.Fatalf("login set no %q cookie (const=%q, got %v)", "__Host-hub_session", SessionCookieName, w.Result().Cookies())
}
raw := w.Header().Get("Set-Cookie")
if !sess.Secure || sess.Path != "/" || sess.Domain != "" || strings.Contains(strings.ToLower(raw), "domain=") || !sess.HttpOnly {
t.Fatalf("__Host- preconditions broken (plain-HTTP login): %q", raw)
}
// The new cookie opens the session.
r = httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: SessionCookieName, Value: sess.Value})
w = httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code == http.StatusFound && w.Header().Get("Location") == "/login" {
t.Fatal("the __Host- session cookie did not authenticate")
}
// The same token under the OLD name (a tossed or stale cookie) does not.
r = httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "hub_session", Value: sess.Value})
w = httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusFound || w.Header().Get("Location") != "/login" {
t.Fatalf("old hub_session cookie = %d %q, want a redirect to /login", w.Code, w.Header().Get("Location"))
}
// Plain-HTTP Basic auth (scripts, no cookie) still reads pages and, with the CLI header, writes.
r = httptest.NewRequest(http.MethodGet, "http://hub.local/", nil)
r.SetBasicAuth("", "op-pass")
w = httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code == http.StatusFound || w.Code == http.StatusUnauthorized {
t.Fatalf("plain-HTTP Basic auth GET = %d, want through", w.Code)
}
if !s.validateCSRF(func() *http.Request {
r := httptest.NewRequest(http.MethodPost, "http://hub.local/configuration", nil)
r.SetBasicAuth("", "op-pass")
r.Header.Set(OperatorCLIHeader, "cli")
return r
}()) {
t.Fatal("plain-HTTP Basic auth + CLI header no longer passes the write gate")
}
}