Set up Vitest with server (node) and client (jsdom) test projects. 79 tests across 8 files verify all Phase 0-1 security controls: escapeHtml sanitization, DOMPurify markdown XSS prevention, CSRF enforcement, security headers, rate limiting, auth guards, profile field allowlist, and login anti-enumeration. Updated SECURITY_EVALUATION.md with remediation status, implementation summary, and automated test coverage details.
127 lines
4 KiB
JavaScript
127 lines
4 KiB
JavaScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { createMockEvent } from '../helpers/createMockEvent.js'
|
|
|
|
// Import the handler — defineEventHandler is a global passthrough,
|
|
// so the default export is the raw handler function.
|
|
import csrfMiddleware from '../../../server/middleware/01.csrf.js'
|
|
|
|
describe('CSRF middleware', () => {
|
|
describe('safe methods bypass', () => {
|
|
it.each(['GET', 'HEAD', 'OPTIONS'])('%s requests pass through', (method) => {
|
|
const event = createMockEvent({ method, path: '/api/test' })
|
|
expect(() => csrfMiddleware(event)).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('non-API paths bypass', () => {
|
|
it('POST to non-/api/ path passes through', () => {
|
|
const event = createMockEvent({ method: 'POST', path: '/some/page' })
|
|
expect(() => csrfMiddleware(event)).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('exempt routes bypass', () => {
|
|
it.each([
|
|
'/api/helcim/webhook',
|
|
'/api/slack/webhook',
|
|
'/api/auth/verify'
|
|
])('POST to %s passes through', (path) => {
|
|
const event = createMockEvent({ method: 'POST', path })
|
|
expect(() => csrfMiddleware(event)).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('CSRF enforcement on state-changing API requests', () => {
|
|
it('throws 403 when POST to /api/* has no x-csrf-token header', () => {
|
|
const event = createMockEvent({
|
|
method: 'POST',
|
|
path: '/api/members/profile',
|
|
cookies: { 'csrf-token': 'abc123' }
|
|
})
|
|
|
|
expect(() => csrfMiddleware(event)).toThrowError(
|
|
expect.objectContaining({
|
|
statusCode: 403,
|
|
statusMessage: 'CSRF token missing or invalid'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('throws 403 when header and cookie tokens do not match', () => {
|
|
const event = createMockEvent({
|
|
method: 'POST',
|
|
path: '/api/members/profile',
|
|
cookies: { 'csrf-token': 'cookie-token' },
|
|
headers: { 'x-csrf-token': 'different-token' }
|
|
})
|
|
|
|
expect(() => csrfMiddleware(event)).toThrowError(
|
|
expect.objectContaining({
|
|
statusCode: 403,
|
|
statusMessage: 'CSRF token missing or invalid'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('passes when header and cookie tokens match', () => {
|
|
const token = 'matching-token-value'
|
|
const event = createMockEvent({
|
|
method: 'POST',
|
|
path: '/api/members/profile',
|
|
cookies: { 'csrf-token': token },
|
|
headers: { 'x-csrf-token': token }
|
|
})
|
|
|
|
expect(() => csrfMiddleware(event)).not.toThrow()
|
|
})
|
|
|
|
it('enforces CSRF on DELETE requests', () => {
|
|
const event = createMockEvent({
|
|
method: 'DELETE',
|
|
path: '/api/admin/events/123',
|
|
cookies: { 'csrf-token': 'abc' }
|
|
})
|
|
|
|
expect(() => csrfMiddleware(event)).toThrowError(
|
|
expect.objectContaining({ statusCode: 403 })
|
|
)
|
|
})
|
|
|
|
it('enforces CSRF on PATCH requests', () => {
|
|
const event = createMockEvent({
|
|
method: 'PATCH',
|
|
path: '/api/members/profile',
|
|
cookies: { 'csrf-token': 'abc' }
|
|
})
|
|
|
|
expect(() => csrfMiddleware(event)).toThrowError(
|
|
expect.objectContaining({ statusCode: 403 })
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('cookie provisioning', () => {
|
|
it('sets csrf-token cookie when none exists', () => {
|
|
const event = createMockEvent({ method: 'GET', path: '/api/test' })
|
|
csrfMiddleware(event)
|
|
|
|
// The middleware calls setCookie which sets a Set-Cookie header
|
|
const setCookieHeader = event._testSetHeaders['set-cookie']
|
|
expect(setCookieHeader).toBeDefined()
|
|
expect(setCookieHeader).toContain('csrf-token=')
|
|
})
|
|
|
|
it('does not set a new cookie when one already exists', () => {
|
|
const event = createMockEvent({
|
|
method: 'GET',
|
|
path: '/api/test',
|
|
cookies: { 'csrf-token': 'existing-token' }
|
|
})
|
|
csrfMiddleware(event)
|
|
|
|
const setCookieHeader = event._testSetHeaders['set-cookie']
|
|
// Should not have set a new cookie
|
|
expect(setCookieHeader).toBeUndefined()
|
|
})
|
|
})
|
|
})
|