Skip to content

Comments

security: add CSRF token protection (signed double-submit cookie)#668

Open
mmcintosh wants to merge 1 commit intoSonicJs-Org:mainfrom
mmcintosh:security/csrf-protection-upstream
Open

security: add CSRF token protection (signed double-submit cookie)#668
mmcintosh wants to merge 1 commit intoSonicJs-Org:mainfrom
mmcintosh:security/csrf-protection-upstream

Conversation

@mmcintosh
Copy link
Contributor

security: CSRF Token Protection

Target: SonicJs-Org/sonicjs main
Branch: security/csrf-protection


Description

Add stateless CSRF token protection using the Signed Double-Submit Cookie pattern, providing defense-in-depth against cross-site request forgery beyond SameSite=Strict cookies alone.

This protects against same-site attacks (e.g., subdomain cookie tossing, browser bugs that bypass SameSite) with zero infrastructure overhead — no D1 tables, no KV, no session store. The token is a self-validating nonce.hmac_signature using HMAC-SHA256 keyed with the existing JWT_SECRET.

Fixes VULN-006 from the security audit.

Changes

Core Middleware

  • New csrf.ts middleware (281 lines) — generates and validates CSRF tokens using HMAC-SHA256 signed double-submit cookies
  • Token format: <32-byte-nonce>.<hmac_signature>, base64url-encoded, no padding
  • Check-then-reuse: validates existing cookie HMAC before regenerating (reduces unnecessary Set-Cookie headers)
  • Header-only validation (X-CSRF-Token) with form body fallback (_csrf hidden field) for regular HTML form POSTs
  • Mounted globally in app.ts before route handlers

Exempt Paths

  • Safe methods: GET, HEAD, OPTIONS
  • Auth routes that create sessions: /auth/login*, /auth/register*, /auth/seed-admin, /auth/accept-invitation, /auth/reset-password
  • Read-only search: /api/search*
  • Public form submissions: /forms/*, /api/forms/* (NOT /admin/forms/*)
  • Bearer-only or API-key-only requests (no auth_token cookie present)

Auth Integration

  • auth.ts: Set csrf_token cookie on login/register (all 7 handlers), clear on logout
  • Cookie attributes: HttpOnly, SameSite=Strict, Secure (production), Path=/

Admin Layout

  • Global CSRF script in admin-layout-catalyst.template.ts:
    • getCsrfToken() reads csrf_token from cookie (handles = in base64 values)
    • htmx:configRequest listener auto-attaches X-CSRF-Token header to all HTMX requests
    • fetch() wrapper auto-attaches header to all non-GET fetch calls
    • submit event listener injects hidden _csrf field into regular (non-HTMX) form submissions

Form Templates

  • Hidden _csrf input field added to form.template.ts and components/form.template.ts as defense-in-depth

Error Responses

  • Browser requests (Accept: text/html): HTML 403 page with "CSRF token missing or invalid"
  • API requests: JSON { error: "CSRF token missing", status: 403 }
  • Production warning logged when JWT_SECRET is not set

Technical Details

New Files:

File Lines Description
packages/core/src/middleware/csrf.ts 281 CSRF middleware implementation
packages/core/src/__tests__/middleware/csrf.test.ts 525 Unit tests (40 tests)

Modified Files:

File Description
packages/core/src/middleware/index.ts Export csrf middleware
packages/core/src/app.ts Mount csrf middleware globally
packages/core/src/routes/auth.ts Set/clear csrf_token on login/register/logout
packages/core/src/templates/layouts/admin-layout-catalyst.template.ts CSRF auto-attach script
packages/core/src/templates/form.template.ts Hidden _csrf field
packages/core/src/templates/components/form.template.ts Hidden _csrf field
tests/e2e/utils/test-helpers.ts CSRF helper functions for E2E tests
tests/e2e/02b-authentication-api.spec.ts Add CSRF headers to auth API tests
tests/e2e/08b-admin-collections-api.spec.ts Add CSRF headers to collections API tests
tests/e2e/13-migrations.spec.ts Add CSRF headers to migrations API tests
tests/e2e/14-database-tools.spec.ts Add CSRF headers to database tools API tests
tests/e2e/23-content-api-crud.spec.ts Add CSRF headers to content CRUD API tests

Testing

Unit Tests

  • 40 unit tests added (csrf.test.ts)
  • All unit tests passing

Unit test coverage:

  • Token generation: format, uniqueness (2 tests)
  • Token validation: valid, wrong secret, tampered nonce, tampered signature, empty, no dot, null/undefined (6 tests)
  • Base64url encoding: URL-safe, no padding (1 test)
  • Safe methods: GET, HEAD, OPTIONS pass through, cookie set on GET (3 tests)
  • Cookie reuse: skip regeneration for valid, regenerate for invalid (2 tests)
  • State-changing requests: valid POST/PUT/DELETE/PATCH, reject missing cookie, missing header, mismatched tokens, invalid signature (8 tests)
  • Exempt paths: all 8 auth routes, /forms/*, /api/forms/*, NOT /admin/forms/*, custom paths (9 tests)
  • Bearer/API-key exemption: no cookie = no CSRF required (2 tests)
  • Error responses: HTML for browsers, JSON for API (2 tests)
  • JWT_SECRET warning: production warning, no warning in dev (2 tests)

E2E Tests

  • 6 existing E2E test files updated for CSRF compatibility
  • All E2E tests passing (676+ tests across 3 CI shards)

Performance Impact

Metric Impact Notes
Latency per request ~0.1ms Single HMAC-SHA256 verify per state-changing request
Memory Negligible No session store, no D1/KV queries
Cookie overhead ~120 bytes One additional cookie (csrf_token)
GET requests No validation Only ensures cookie exists

Breaking Changes

Potentially breaking for custom integrations:

  • Any client making POST/PUT/DELETE/PATCH requests with cookie-based auth (auth_token cookie) must now include the X-CSRF-Token header matching the csrf_token cookie value
  • Bearer-only and API-key-only requests are NOT affected
  • Public form submissions (/forms/*, /api/forms/*) are NOT affected

Migration for API consumers using cookie auth:

// Read csrf_token from cookie
const csrfToken = document.cookie.match(/csrf_token=([^;\s]+)/)?.[1] || '';

// Include in state-changing requests
fetch('/admin/api/content', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': csrfToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(data)
});

Migration Notes

  • No database migrations required
  • No configuration changes required — uses existing JWT_SECRET binding
  • CSRF protection activates automatically on deployment
  • Existing admin UI (HTMX-based) automatically includes tokens via the global script
  • Custom frontends using cookie auth will need to add the X-CSRF-Token header

Known Issues

None.

Screenshots/Videos

N/A (middleware-only change, no UI changes)

Checklist

  • Code follows project conventions
  • Tests added/updated and passing (40 unit + 676+ E2E)
  • Type checking passes
  • No console errors or warnings
  • Documentation updated (inline JSDoc + code comments)
  • No breaking changes for default usage (admin UI auto-handles tokens)
  • Backward compatible for Bearer/API-key auth consumers

Add stateless CSRF protection using the Signed Double-Submit Cookie
pattern with HMAC-SHA256 signatures keyed on JWT_SECRET.

- New csrf.ts middleware: generates/validates signed tokens
- Token format: <nonce>.<hmac_signature>, base64url-encoded
- Header validation (X-CSRF-Token) with form body fallback (_csrf)
- Exempt: safe methods, auth routes, public forms, bearer-only requests
- Admin layout auto-attaches tokens to HTMX and fetch requests
- 40 unit tests + E2E test updates for CSRF compatibility

Fixes VULN-006
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant