From 602e311a83ed2ad9cd4406349ec85256e707c435 Mon Sep 17 00:00:00 2001 From: Manas Srivastava Date: Sat, 30 May 2026 14:16:23 +0530 Subject: [PATCH] sec(api): renderAuthError Cache-Control: no-store + lang="en" (BUG-API-257/404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OAuth / magic-link callback HTML is per-request session-bound state (the underlying magic-link / OAuth code has been consumed or expired by the time the error page renders). Two compounding gaps shipped: 1. BUG-API-404 — no Cache-Control. A back-button, service-worker re-fetch, or intermediate proxy could replay the body, leaking the "you tried this link" UX state across sessions. Stamp Cache-Control: no-store (RFC 9111 §5.2.2.5) which is the strongest stop-cache directive and matches the contract every other auth-result surface in the api already follows. 2. BUG-API-257 — shipped with no `lang` attribute. WCAG 3.1.1 ("Language of Page") requires a programmatically determinable primary language; assistive tech (VoiceOver, NVDA) falls back to the OS locale otherwise, mispronouncing English copy in non-English locales. Pin lang="en" to match the static English-only copy. Both fix at the single renderAuthError sink — fanning out across every OAuth / magic-link callback error path (~20 call sites in auth.go + magic_link.go) without scattering c.Set / template edits. Coverage block: Symptom: OAuth/magic-link error HTML missing Cache-Control: no-store (BUG-API-404) and lang attribute (BUG-API-257) Enumeration: rg -F 'renderAuthError' internal/handlers/ (~25 sites) All flow through a single emit point in auth.go:1113 Sites found: 1 emit function (~25 callers) Sites touched: 1 (single sink fix) Coverage test: TestAuth_RenderAuthError_StatusAndContentType now pins - Cache-Control: no-store - body contains `` so a future revert of either fails before merge. Live verified: pending auto-deploy + curl -sI 'https://api.instanode.dev/auth/email/callback?token=invalid' | grep -i cache-control Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/handlers/auth.go | 20 ++++++++++++++++++- .../handlers/auth_helpers_coverage_test.go | 12 +++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index c61460b3..30fd8a41 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -1131,8 +1131,26 @@ func (h *AuthHandler) consumeOAuthState(ctx context.Context, state string) bool // remember to escape — defense in depth. func renderAuthError(c *fiber.Ctx, status int, headline, detail string) error { c.Set("Content-Type", "text/html; charset=utf-8") + // BUG-API-404 (QA 2026-05-29): the OAuth / magic-link callback HTML + // is a per-request rendering of session-bound state — a back-button, + // browser-history-restore, or service-worker re-fetch must NOT replay + // it (the underlying token has been consumed or expired). Without + // Cache-Control, the body could be re-served by the browser fetch + // cache or any intermediary, which both leaks the "you tried this + // link" UX state across sessions AND, in the success-redirect cousin + // of this surface, would re-set the exchange cookie. `no-store` + // (RFC 9111 §5.2.2.5) is the strongest stop-cache directive and + // matches the contract every other auth-result surface in the api + // already follows. + c.Set(fiber.HeaderCacheControl, "no-store") + // BUG-API-257 (QA 2026-05-29): the element used to ship with + // no `lang` attribute. WCAG 3.1.1 "Language of Page" requires a + // programmatically determinable primary language; assistive tech + // (VoiceOver, NVDA) falls back to the OS locale otherwise, mis- + // pronouncing English copy in non-English locales. `lang="en"` is + // the correct value for the static English-only copy below. body := fmt.Sprintf(` - + Sign-in error

%s

diff --git a/internal/handlers/auth_helpers_coverage_test.go b/internal/handlers/auth_helpers_coverage_test.go index b901495a..7603b709 100644 --- a/internal/handlers/auth_helpers_coverage_test.go +++ b/internal/handlers/auth_helpers_coverage_test.go @@ -263,12 +263,24 @@ func TestAuth_RenderAuthError_StatusAndContentType(t *testing.T) { defer resp.Body.Close() assert.Equal(t, http.StatusBadRequest, resp.StatusCode) assert.Contains(t, resp.Header.Get("Content-Type"), "text/html") + // BUG-API-404 (QA 2026-05-29): the OAuth / magic-link callback HTML + // is per-request session-bound state — back-button or service-worker + // re-fetch must NOT replay it. Pin Cache-Control: no-store so a + // future regression that drops the c.Set call fails before merge. + assert.Equal(t, "no-store", resp.Header.Get("Cache-Control"), + "BUG-API-404: renderAuthError must stamp Cache-Control: no-store on every callback HTML response") buf := make([]byte, 1024) n, _ := resp.Body.Read(buf) body := string(buf[:n]) assert.Contains(t, body, "Sign-in error") assert.Contains(t, body, "Hello") assert.Contains(t, body, "Detail") + // BUG-API-257 (QA 2026-05-29): WCAG 3.1.1 — the <html> element MUST + // carry a lang attribute so assistive tech (VoiceOver, NVDA) doesn't + // fall back to the OS locale and mispronounce English copy. Pin the + // value here so a future revert to the bare <html> fails before merge. + assert.Contains(t, body, `<html lang="en">`, + "BUG-API-257: renderAuthError HTML must include lang=\"en\" on the <html> element (WCAG 3.1.1)") } // SEC-API FINDING-23 regression: renderAuthError must HTML-escape both