fix: protect browser mutations from CSRF and retire cli auth #298

Merged
erik merged 1 commit from fix/task-b7cd625f-browser-csrf into main 2026-10-05 11:36:50 -05:00
Owner

Summary

  • Apply shared browser-only CSRF protection before setup gates and handlers for settings, setup, login and logout namespaces. Require exact trusted Origin plus HMAC-SHA256-signed token bound to purpose, session/anonymous context, canonical origin, expiry and random nonce. Tokens expire within one hour and no later than their context/session; missing, malformed, expired, tampered, cross-session and duplicate form values fail before business mutations.
  • Protect anonymous magic-link requests and passkey authentication POSTs with a server-signed HttpOnly context cookie; production cookie uses Secure/Path=/ and __Host- prefix, with SameSite=Lax. Login/logout clear anonymous context; session-bound tokens cannot cross anonymous/authenticated or session rotation. Existing session cookie protections remain.
  • Explicitly wire hidden tokens into current settings/user/actor/passkey/API-key/setup/logout/login forms and headers into passkey fetches. No arbitrary HTML rewriting or unsafe script allowances. Browser responses are no-store.
  • GET /logout is non-mutating; protected POST deletes only the callers valid session, including deactivated users. Keep expiring, single-use magic-link GET callback as a documented capability-based exception, not ambient cookie authorization.
  • Remove legacy /cli/auth route, obsolete template and tests outright. No redirect, replacement endpoint, old-CLI shim, client-version gate or duplicate credential issuance. Current example uses protected /settings/api-keys and API-only CLI login. User policy: newer clients may support older APIs; sites do not preserve legacy CLI-specific behavior.
  • Origin precedence: operator SECURITY_PUBLIC_ORIGIN, persisted site URL, request origin for unconfigured/local bootstrap. No Forwarded/X-Forwarded-* trust. Canonical origin also drives email links and WebAuthn request-origin fallback behind HTTPS-to-HTTP proxies (including clearing inherited internal ports).
  • Optional 32-byte hexadecimal CSRF_SIGNING_KEY permits shared replica/restart signing; unset means random per app instance and open forms reload on restart. Invalid format fails app creation. Document key/rotation, origin configuration, threat model, cookies, magic-link exception, rollout and custom-route extension.
  • Bearer API/CLI and federation remain outside browser-only scope; Authorization headers cannot bypass protection on cookie-authenticated browser routes. No OpenAPI, API operation, dependency, migration, CI/workflow, deployed-site or release/publication changes.

Verification

  • New baseline regressions: 31 failed and bearer-only operation passed before implementation.
  • 59 deterministic CSRF cases cover every registered browser mutation family (explicit runtime inventory), same-origin missing tokens and cross-origin requests with Authorization bypass attempts, complete DB snapshots, wrong/expired/tampered tokens and cookies, purpose and anonymous/session rotation, duplicate fields, parallel forms/session isolation, valid form/multipart operations and zero storage upload before validation, logout/deactivated-user behavior, retired route GET/POST404 with no redirect, read-only GETs, local ports, configured HTTPS over internal HTTP, spoofed forwarding, bootstrap email/WebAuthn origin, signer sharing/rotation/configuration and unchanged bearer reads/writes.
  • Existing business-workflow tests retain assertions and use a small browser client helper that obtains real rendered tokens/cookies. Raw-app security tests do not use that helper.
  • Template archive regression verifies byte-for-byte shipping of CSRF implementation/tests/helper/auth guidance and absence of retired route/page/test.
  • ./scripts/pre-pr.sh passed generated CSS, format/lint, all workspace types and all1,226 local tests (539CLI,637site,50packages); full workspace build passed.
  • npm run test:ci passed unit-only coverage (533CLI,558site,50packages).
  • All37 local browser cases passed, including two new actual form/key-revoke/logout and same-site cross-origin attack cases (ambient session + valid token still403/no key), actual virtual-device WebAuthn registration/login, existing CSP/Swagger and all visual flows. Local fixture only; base URL now matches canonical localhost/WebAuthn identity. No personal credentials, deployed sites or extra servers; synthetic second-origin page fulfilled in Playwright.
  • Evidence: /tmp/slugkit-csrf-red.log, /tmp/slugkit-csrf-green.log, /tmp/slugkit-csrf-workflows.log, /tmp/slugkit-csrf-pre-pr.log, /tmp/slugkit-csrf-unit-ci.log, /tmp/slugkit-csrf-build.log and /tmp/slugkit-csrf-browser.log.

NEVER browser/container/federation integration tests or test runtime provisioning in CI/release workflows. Browser and integration verification stays local. Existing generated/deployed sites require separately approved upgrade/backport and deployment to receive these changes.

Task: #task-b7cd625f
Related completed CLI fix: task-695242ef / PR #297.

Awaiting CI, independent review and explicit human merge approval.

## Summary - Apply shared browser-only CSRF protection before setup gates and handlers for settings, setup, login and logout namespaces. Require exact trusted Origin plus HMAC-SHA256-signed token bound to purpose, session/anonymous context, canonical origin, expiry and random nonce. Tokens expire within one hour and no later than their context/session; missing, malformed, expired, tampered, cross-session and duplicate form values fail before business mutations. - Protect anonymous magic-link requests and passkey authentication POSTs with a server-signed HttpOnly context cookie; production cookie uses Secure/Path=/ and __Host- prefix, with SameSite=Lax. Login/logout clear anonymous context; session-bound tokens cannot cross anonymous/authenticated or session rotation. Existing session cookie protections remain. - Explicitly wire hidden tokens into current settings/user/actor/passkey/API-key/setup/logout/login forms and headers into passkey fetches. No arbitrary HTML rewriting or unsafe script allowances. Browser responses are no-store. - GET /logout is non-mutating; protected POST deletes only the callers valid session, including deactivated users. Keep expiring, single-use magic-link GET callback as a documented capability-based exception, not ambient cookie authorization. - Remove legacy /cli/auth route, obsolete template and tests outright. No redirect, replacement endpoint, old-CLI shim, client-version gate or duplicate credential issuance. Current example uses protected /settings/api-keys and API-only CLI login. User policy: newer clients may support older APIs; sites do not preserve legacy CLI-specific behavior. - Origin precedence: operator SECURITY_PUBLIC_ORIGIN, persisted site URL, request origin for unconfigured/local bootstrap. No Forwarded/X-Forwarded-* trust. Canonical origin also drives email links and WebAuthn request-origin fallback behind HTTPS-to-HTTP proxies (including clearing inherited internal ports). - Optional 32-byte hexadecimal CSRF_SIGNING_KEY permits shared replica/restart signing; unset means random per app instance and open forms reload on restart. Invalid format fails app creation. Document key/rotation, origin configuration, threat model, cookies, magic-link exception, rollout and custom-route extension. - Bearer API/CLI and federation remain outside browser-only scope; Authorization headers cannot bypass protection on cookie-authenticated browser routes. No OpenAPI, API operation, dependency, migration, CI/workflow, deployed-site or release/publication changes. ## Verification - New baseline regressions: 31 failed and bearer-only operation passed before implementation. - 59 deterministic CSRF cases cover every registered browser mutation family (explicit runtime inventory), same-origin missing tokens and cross-origin requests with Authorization bypass attempts, complete DB snapshots, wrong/expired/tampered tokens and cookies, purpose and anonymous/session rotation, duplicate fields, parallel forms/session isolation, valid form/multipart operations and zero storage upload before validation, logout/deactivated-user behavior, retired route GET/POST404 with no redirect, read-only GETs, local ports, configured HTTPS over internal HTTP, spoofed forwarding, bootstrap email/WebAuthn origin, signer sharing/rotation/configuration and unchanged bearer reads/writes. - Existing business-workflow tests retain assertions and use a small browser client helper that obtains real rendered tokens/cookies. Raw-app security tests do not use that helper. - Template archive regression verifies byte-for-byte shipping of CSRF implementation/tests/helper/auth guidance and absence of retired route/page/test. - ./scripts/pre-pr.sh passed generated CSS, format/lint, all workspace types and all1,226 local tests (539CLI,637site,50packages); full workspace build passed. - npm run test:ci passed unit-only coverage (533CLI,558site,50packages). - All37 local browser cases passed, including two new actual form/key-revoke/logout and same-site cross-origin attack cases (ambient session + valid token still403/no key), actual virtual-device WebAuthn registration/login, existing CSP/Swagger and all visual flows. Local fixture only; base URL now matches canonical localhost/WebAuthn identity. No personal credentials, deployed sites or extra servers; synthetic second-origin page fulfilled in Playwright. - Evidence: /tmp/slugkit-csrf-red.log, /tmp/slugkit-csrf-green.log, /tmp/slugkit-csrf-workflows.log, /tmp/slugkit-csrf-pre-pr.log, /tmp/slugkit-csrf-unit-ci.log, /tmp/slugkit-csrf-build.log and /tmp/slugkit-csrf-browser.log. NEVER browser/container/federation integration tests or test runtime provisioning in CI/release workflows. Browser and integration verification stays local. Existing generated/deployed sites require separately approved upgrade/backport and deployment to receive these changes. Task: #task-b7cd625f Related completed CLI fix: task-695242ef / PR #297. Awaiting CI, independent review and explicit human merge approval.
fix: protect cookie-authenticated browser mutations from CSRF
All checks were successful
CI / build-lint-test (pull_request) Successful in 59s
67ccef84c8
Require expiring session-bound signed tokens and a trusted Origin before browser mutations, with signed anonymous login context and canonical proxy-safe authentication URLs. Keep bearer APIs and federation outside browser protection.

Retire /cli/auth outright instead of preserving an undocumented legacy CLI flow. Make GET logout non-mutating, wire current forms and passkey fetches, and document token/origin operation and custom route safeguards.

Task: #task-b7cd625f
Author
Owner

Review update

  • PR: #298 is open at 67ccef84c86a99ca3d9788348f5da9a042dd4108.
  • Result: approved.
  • Follow-up: wait for explicit human merge approval.

Summary

Independent review compared the complete PR diff, task task-b7cd625f, and supplied evidence artifacts against all 9 acceptance criteria. The changes implement a browser-only shared CSRF boundary for current cookie-authenticated site flows, retire /cli/auth without compatibility scaffolding, keep bearer API/CLI behavior unchanged, and document origin/key/rotation/custom-route guidance.

Acceptance criteria

  1. met — Cross-site form submissions are blocked before mutations/uploads by strict trusted Origin plus signed token validation; tests snapshot DB state and assert no storage upload before validation.
  2. met — Missing, malformed, invalid, expired, attacker-supplied, wrong-session, duplicate, and anonymous-context attacks return non-sensitive 403 responses.
  3. met — Current settings, users/actors, setup, logout, passkey, API-key, and email-login/passkey-login flows are wired with rendered tokens or fetch headers and covered by unit/browser evidence.
  4. met — Bearer API/CLI and federation remain outside the browser guard; /auth/check and bearer writes remain token-free, while Authorization headers do not bypass cookie-route CSRF checks.
  5. met — Proxy/origin behavior is tested for local HTTP, canonical HTTPS over internal HTTP, null/malformed/cross-origin Origins, local ports, and spoofed forwarded headers.
  6. met — Tests cover the explicit runtime mutation inventory, token expiry, cross-session reuse, anonymous/authenticated rotation, and concurrent form/session isolation.
  7. met — GET /logout is non-mutating; /cli/auth route/template/tests are deleted; GET/POST retired route behavior is 404 with no redirect or API-key creation.
  8. met — CLI/site guidance now directs key issuance through /settings/api-keys and API-only slug login; no older-CLI browser-flow compatibility promise or shim remains.
  9. met — Security/auth docs explain threat model, cookie defense-in-depth, token lifetime/key rotation, origin precedence, magic-link GET exception, and custom browser-route extension pattern.

Review findings

  • No blocking issues found.
  • No warnings found.
  • Scope/policy check: no migrations, dependencies, OpenAPI, workflow/CI, release, deployment, browser-version gate, or legacy compatibility endpoint changes were introduced.
  • Verification evidence reviewed: /tmp/slugkit-csrf-pre-pr.log (CSS/format/lint/types + 1,226 local tests), /tmp/slugkit-csrf-unit-ci.log (1,141 unit-only tests), /tmp/slugkit-csrf-build.log (workspace build), /tmp/slugkit-csrf-browser.log (37 browser cases), plus PR diff/commits/files. No tests or builds were rerun during review.

Verdict

Approved. Human merge approval is still required.

### Review update - PR: #298 is open at `67ccef84c86a99ca3d9788348f5da9a042dd4108`. - Result: approved. - Follow-up: wait for explicit human merge approval. ### Summary Independent review compared the complete PR diff, task `task-b7cd625f`, and supplied evidence artifacts against all 9 acceptance criteria. The changes implement a browser-only shared CSRF boundary for current cookie-authenticated site flows, retire `/cli/auth` without compatibility scaffolding, keep bearer API/CLI behavior unchanged, and document origin/key/rotation/custom-route guidance. ### Acceptance criteria 1. **met** — Cross-site form submissions are blocked before mutations/uploads by strict trusted `Origin` plus signed token validation; tests snapshot DB state and assert no storage upload before validation. 2. **met** — Missing, malformed, invalid, expired, attacker-supplied, wrong-session, duplicate, and anonymous-context attacks return non-sensitive 403 responses. 3. **met** — Current settings, users/actors, setup, logout, passkey, API-key, and email-login/passkey-login flows are wired with rendered tokens or fetch headers and covered by unit/browser evidence. 4. **met** — Bearer API/CLI and federation remain outside the browser guard; `/auth/check` and bearer writes remain token-free, while Authorization headers do not bypass cookie-route CSRF checks. 5. **met** — Proxy/origin behavior is tested for local HTTP, canonical HTTPS over internal HTTP, null/malformed/cross-origin Origins, local ports, and spoofed forwarded headers. 6. **met** — Tests cover the explicit runtime mutation inventory, token expiry, cross-session reuse, anonymous/authenticated rotation, and concurrent form/session isolation. 7. **met** — GET `/logout` is non-mutating; `/cli/auth` route/template/tests are deleted; GET/POST retired route behavior is 404 with no redirect or API-key creation. 8. **met** — CLI/site guidance now directs key issuance through `/settings/api-keys` and API-only `slug login`; no older-CLI browser-flow compatibility promise or shim remains. 9. **met** — Security/auth docs explain threat model, cookie defense-in-depth, token lifetime/key rotation, origin precedence, magic-link GET exception, and custom browser-route extension pattern. ### Review findings - No blocking issues found. - No warnings found. - Scope/policy check: no migrations, dependencies, OpenAPI, workflow/CI, release, deployment, browser-version gate, or legacy compatibility endpoint changes were introduced. - Verification evidence reviewed: `/tmp/slugkit-csrf-pre-pr.log` (CSS/format/lint/types + 1,226 local tests), `/tmp/slugkit-csrf-unit-ci.log` (1,141 unit-only tests), `/tmp/slugkit-csrf-build.log` (workspace build), `/tmp/slugkit-csrf-browser.log` (37 browser cases), plus PR diff/commits/files. No tests or builds were rerun during review. ### Verdict Approved. Human merge approval is still required.
erik merged commit 4853d91b0a into main 2026-10-05 11:36:50 -05:00
erik deleted branch fix/task-b7cd625f-browser-csrf 2026-10-05 11:36:50 -05:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
erik/slugkit!298
No description provided.