From f8df1e9f1638082b0dbcbd9b68bbb16fce1866b7 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 13 Jul 2026 13:51:19 +0200 Subject: [PATCH] =?UTF-8?q?v0.126.3:=20storage=20wizard=20CSRF=20on=20clai?= =?UTF-8?q?med=20boxes=20=E2=80=94=20wizard=20pages=20rendered=20via=20raw?= =?UTF-8?q?=20render()=20shipped=20an=20EMPTY=20csrf-meta=20token,=20every?= =?UTF-8?q?=20/api/storage/init|attach=20POST=20died=20with=20'token=20mis?= =?UTF-8?q?match';=20latent=20until=20the=20claim=20arc=20(unclaimed=20box?= =?UTF-8?q?es=20skip=20CsrfProtect);=20fix=20=3D=20executeTemplate=20injec?= =?UTF-8?q?tion=20+=20session-backed=20regression=20test=20(red-proven)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++ controller/internal/web/storage_handlers.go | 6 ++- .../internal/web/storage_wizard_csrf_test.go | 42 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 controller/internal/web/storage_wizard_csrf_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e65d0b..9f67b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ## Changelog +### v0.126.3 — storage wizard on a CLAIMED box: the init/attach POST no longer dies on CSRF (2026-07-13) + +First live hit during the agent-0.87.0 drill wizard leg: /api/storage/init → "CSRF token missing +or invalid" (log: token mismatch). Root cause: `storageWizardPageHandler` rendered via raw +`render()` instead of `executeTemplate()`, so /storage/init + /storage/attach shipped an EMPTY +csrf-meta token — and the wizard's fetch() posts that token. LATENT until the claim arc: an +unclaimed box skips CsrfProtect entirely, so the wizard had never run against a password-gated +box before. Fix: executeTemplate (CSRF auto-injection); regression test renders both wizard +pages with a real session and asserts the meta carries the SESSION token (red-proven: swap back +to render() → both cases fail on the empty meta). + ### v0.126.2 — stylesheet cache-bust (2026-07-13) 0.126.1 live QA: Cloudflare edge-caches `/static/style.css` for 4h (`Cf-Cache-Status: HIT`), so diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go index cc0e009..1811af6 100644 --- a/controller/internal/web/storage_handlers.go +++ b/controller/internal/web/storage_handlers.go @@ -261,7 +261,11 @@ func (s *Server) storageWizardPageHandler(w http.ResponseWriter, r *http.Request title = "Meglévő meghajtó csatolása" } data := s.baseData(tmpl, title) - s.render(w, tmpl, data) + // executeTemplate, NOT render(): the wizard POSTs /api/storage/* with the csrf-meta token, + // and render() leaves {{.CSRFToken}} empty. Latent until the claim arc — on an unclaimed + // (auth-off) box CsrfProtect is skipped entirely, so the wizard had never run against a + // CLAIMED box before; first hit = the agent-0.87.0 drill wizard leg ("token mismatch"). + s.executeTemplate(w, r, tmpl, data) } // ServeStorageAPI dispatches /api/storage/* (guided init/attach/eject orchestration). diff --git a/controller/internal/web/storage_wizard_csrf_test.go b/controller/internal/web/storage_wizard_csrf_test.go new file mode 100644 index 0000000..267e7ea --- /dev/null +++ b/controller/internal/web/storage_wizard_csrf_test.go @@ -0,0 +1,42 @@ +package web + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// v0.126.3 — the storage wizard pages must render the session's CSRF token into the meta tag +// (the wizard's fetch() POSTs /api/storage/init|attach with csrfHeaders()). The pre-fix +// storageWizardPageHandler used raw render() (no CSRF injection) → empty meta → every init +// POST on a CLAIMED box died with "token mismatch". Latent until the claim arc because an +// unclaimed box skips CsrfProtect entirely; first live hit was the agent-0.87.0 drill wizard +// leg (2026-07-13). +// COMPANION red-proof: swap executeTemplate back to render() → both cases fail on the empty +// meta token. +func TestStorageWizardPages_CarrySessionCSRFToken(t *testing.T) { + s := testServer(t) + s.loadTemplates() + if s.sessions == nil { + s.sessions = map[string]*session{} // testServer skips the login wiring + } + + // A real session, exactly as login creates it. + tok := s.createSession() + want := s.csrfTokenForSession(tok) + if want == "" { + t.Fatal("test session has no csrf token") + } + + for _, page := range []string{"storage_init", "storage_attach"} { + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/storage/init", nil) + req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tok}) + s.storageWizardPageHandler(rr, req, page) + body := rr.Body.String() + if !strings.Contains(body, `name="csrf-token" content="`+want+`"`) { + t.Errorf("%s: the csrf meta must carry the SESSION token (got empty or wrong token)", page) + } + } +}