Add Vitest security test suite and update security evaluation doc
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.
This commit is contained in:
parent
d5c95ace0a
commit
29c96a207e
14 changed files with 2454 additions and 3 deletions
127
tests/server/middleware/csrf.test.js
Normal file
127
tests/server/middleware/csrf.test.js
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue