mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
Add 143 security tests covering all security modules: Unit Tests (125 tests): - rate-limiter.unit.spec.ts: limiter creation, request tracking, blocking, window reset, IP extraction, header generation - csrf.unit.spec.ts: token generation/validation, origin checking, double submit cookie pattern, referer validation - ip-allowlist.unit.spec.ts: CIDR matching, wildcards, endpoint- specific allowlist/blocklist rules, IP extraction - data-masking.unit.spec.ts: field detection, pattern matching, recursive masking, JWT/connection string/private key handling API Integration Tests (18 tests): - security-api.int.spec.ts: rate limiting responses, IP blocking, CSRF protection on state-changing endpoints Test Infrastructure: - tests/helpers/security-test-utils.ts: CSRF token generators, mock request builders, environment setup utilities - vitest.config.mts: updated to include unit tests - package.json: added test:unit and test:security scripts - .github/workflows/security.yml: added security-tests CI job Also updated detect-secrets.sh to ignore .spec.ts and .test.ts files which may contain example secrets for testing purposes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
375 lines
10 KiB
TypeScript
375 lines
10 KiB
TypeScript
/**
|
|
* Security Test Utilities
|
|
*
|
|
* Helper functions for testing security features.
|
|
* Provides mock request builders, CSRF token utilities, and environment setup.
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { createHmac, randomBytes } from 'crypto'
|
|
|
|
// ============================================================================
|
|
// CSRF Token Utilities
|
|
// ============================================================================
|
|
|
|
const DEFAULT_CSRF_SECRET = 'test-csrf-secret-for-testing-only'
|
|
|
|
/**
|
|
* Generate a valid CSRF token for testing
|
|
*/
|
|
export function generateTestCsrfToken(secret: string = DEFAULT_CSRF_SECRET): string {
|
|
const timestamp = Date.now().toString(36)
|
|
const random = randomBytes(16).toString('hex')
|
|
const data = `${timestamp}:${random}`
|
|
|
|
const hmac = createHmac('sha256', secret)
|
|
hmac.update(data)
|
|
const signature = hmac.digest('hex').substring(0, 16)
|
|
|
|
return `${data}:${signature}`
|
|
}
|
|
|
|
/**
|
|
* Generate an expired CSRF token for testing
|
|
*/
|
|
export function generateExpiredCsrfToken(secret: string = DEFAULT_CSRF_SECRET): string {
|
|
// Use timestamp from 2 hours ago
|
|
const timestamp = (Date.now() - 2 * 60 * 60 * 1000).toString(36)
|
|
const random = randomBytes(16).toString('hex')
|
|
const data = `${timestamp}:${random}`
|
|
|
|
const hmac = createHmac('sha256', secret)
|
|
hmac.update(data)
|
|
const signature = hmac.digest('hex').substring(0, 16)
|
|
|
|
return `${data}:${signature}`
|
|
}
|
|
|
|
/**
|
|
* Generate an invalid CSRF token (wrong signature)
|
|
*/
|
|
export function generateInvalidCsrfToken(): string {
|
|
const timestamp = Date.now().toString(36)
|
|
const random = randomBytes(16).toString('hex')
|
|
return `${timestamp}:${random}:invalidsignature`
|
|
}
|
|
|
|
// ============================================================================
|
|
// Mock Request Builders
|
|
// ============================================================================
|
|
|
|
export interface MockRequestOptions {
|
|
method?: string
|
|
url?: string
|
|
headers?: Record<string, string>
|
|
body?: unknown
|
|
cookies?: Record<string, string>
|
|
ip?: string
|
|
}
|
|
|
|
/**
|
|
* Create a mock NextRequest for testing
|
|
*/
|
|
export function createMockRequest(options: MockRequestOptions = {}): NextRequest {
|
|
const {
|
|
method = 'GET',
|
|
url = 'https://test.example.com/api/test',
|
|
headers = {},
|
|
ip,
|
|
} = options
|
|
|
|
const requestHeaders = new Headers()
|
|
|
|
// Set IP headers if provided
|
|
if (ip) {
|
|
requestHeaders.set('x-forwarded-for', ip)
|
|
requestHeaders.set('x-real-ip', ip)
|
|
}
|
|
|
|
// Set custom headers
|
|
Object.entries(headers).forEach(([key, value]) => {
|
|
requestHeaders.set(key, value)
|
|
})
|
|
|
|
const request = new NextRequest(url, {
|
|
method,
|
|
headers: requestHeaders,
|
|
})
|
|
|
|
// Mock cookies
|
|
if (options.cookies) {
|
|
Object.defineProperty(request, 'cookies', {
|
|
value: {
|
|
get: (name: string) => {
|
|
const value = options.cookies?.[name]
|
|
return value ? { value } : undefined
|
|
},
|
|
getAll: () =>
|
|
Object.entries(options.cookies || {}).map(([name, value]) => ({ name, value })),
|
|
},
|
|
})
|
|
}
|
|
|
|
return request
|
|
}
|
|
|
|
/**
|
|
* Create a mock request with CSRF protection
|
|
*/
|
|
export function createCsrfProtectedRequest(
|
|
method: string,
|
|
options: Omit<MockRequestOptions, 'method'> = {},
|
|
): NextRequest {
|
|
const token = generateTestCsrfToken()
|
|
|
|
return createMockRequest({
|
|
...options,
|
|
method,
|
|
headers: {
|
|
...options.headers,
|
|
'X-CSRF-Token': token,
|
|
origin: 'https://test.example.com',
|
|
},
|
|
cookies: {
|
|
...options.cookies,
|
|
'csrf-token': token,
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Create a mock request simulating server-to-server call
|
|
*/
|
|
export function createServerToServerRequest(
|
|
method: string,
|
|
options: Omit<MockRequestOptions, 'method'> = {},
|
|
): NextRequest {
|
|
return createMockRequest({
|
|
...options,
|
|
method,
|
|
headers: {
|
|
...options.headers,
|
|
authorization: 'Bearer test-api-token',
|
|
'content-type': 'application/json',
|
|
// No origin header for server-to-server
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Create a mock request from specific IP
|
|
*/
|
|
export function createRequestFromIp(ip: string, options: MockRequestOptions = {}): NextRequest {
|
|
return createMockRequest({
|
|
...options,
|
|
ip,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Environment Setup
|
|
// ============================================================================
|
|
|
|
export interface SecurityTestEnv {
|
|
CSRF_SECRET?: string
|
|
PAYLOAD_SECRET?: string
|
|
BLOCKED_IPS?: string
|
|
SEND_EMAIL_ALLOWED_IPS?: string
|
|
ADMIN_ALLOWED_IPS?: string
|
|
WEBHOOK_ALLOWED_IPS?: string
|
|
REDIS_HOST?: string
|
|
REDIS_PORT?: string
|
|
}
|
|
|
|
/**
|
|
* Setup environment variables for security tests
|
|
*/
|
|
export function setupSecurityEnv(env: SecurityTestEnv): void {
|
|
Object.entries(env).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
process.env[key] = value
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Clear security-related environment variables
|
|
*/
|
|
export function clearSecurityEnv(): void {
|
|
const securityEnvVars = [
|
|
'CSRF_SECRET',
|
|
'BLOCKED_IPS',
|
|
'SEND_EMAIL_ALLOWED_IPS',
|
|
'ADMIN_ALLOWED_IPS',
|
|
'WEBHOOK_ALLOWED_IPS',
|
|
]
|
|
|
|
securityEnvVars.forEach((key) => {
|
|
delete process.env[key]
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Response Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Extract rate limit info from response headers
|
|
*/
|
|
export function extractRateLimitInfo(response: NextResponse): {
|
|
limit: number | null
|
|
remaining: number | null
|
|
reset: number | null
|
|
retryAfter: number | null
|
|
} {
|
|
return {
|
|
limit: response.headers.get('X-RateLimit-Limit')
|
|
? parseInt(response.headers.get('X-RateLimit-Limit')!, 10)
|
|
: null,
|
|
remaining: response.headers.get('X-RateLimit-Remaining')
|
|
? parseInt(response.headers.get('X-RateLimit-Remaining')!, 10)
|
|
: null,
|
|
reset: response.headers.get('X-RateLimit-Reset')
|
|
? parseInt(response.headers.get('X-RateLimit-Reset')!, 10)
|
|
: null,
|
|
retryAfter: response.headers.get('Retry-After')
|
|
? parseInt(response.headers.get('Retry-After')!, 10)
|
|
: null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse JSON response body
|
|
*/
|
|
export async function parseJsonResponse<T>(response: NextResponse): Promise<T> {
|
|
const text = await response.text()
|
|
return JSON.parse(text) as T
|
|
}
|
|
|
|
// ============================================================================
|
|
// Rate Limit Testing
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Make multiple requests to test rate limiting
|
|
*/
|
|
export async function exhaustRateLimit(
|
|
requestFn: () => Promise<NextResponse>,
|
|
maxRequests: number,
|
|
): Promise<NextResponse[]> {
|
|
const responses: NextResponse[] = []
|
|
|
|
for (let i = 0; i <= maxRequests; i++) {
|
|
responses.push(await requestFn())
|
|
}
|
|
|
|
return responses
|
|
}
|
|
|
|
// ============================================================================
|
|
// Assertion Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Assert response has security headers
|
|
*/
|
|
export function assertSecurityHeaders(response: NextResponse): void {
|
|
const headers = response.headers
|
|
|
|
// Rate limit headers should be present
|
|
const hasRateLimitHeaders =
|
|
headers.has('X-RateLimit-Limit') || headers.has('X-RateLimit-Remaining')
|
|
|
|
if (!hasRateLimitHeaders) {
|
|
throw new Error('Response missing rate limit headers')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert response is rate limited (429)
|
|
*/
|
|
export function assertRateLimited(response: NextResponse): void {
|
|
if (response.status !== 429) {
|
|
throw new Error(`Expected 429 status, got ${response.status}`)
|
|
}
|
|
|
|
if (!response.headers.has('Retry-After')) {
|
|
throw new Error('Rate limited response missing Retry-After header')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert response is CSRF blocked (403)
|
|
*/
|
|
export function assertCsrfBlocked(response: NextResponse): void {
|
|
if (response.status !== 403) {
|
|
throw new Error(`Expected 403 status for CSRF block, got ${response.status}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert response is IP blocked (403)
|
|
*/
|
|
export function assertIpBlocked(response: NextResponse): void {
|
|
if (response.status !== 403) {
|
|
throw new Error(`Expected 403 status for IP block, got ${response.status}`)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Data Generators
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Generate a random IP address
|
|
*/
|
|
export function randomIp(): string {
|
|
return `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`
|
|
}
|
|
|
|
/**
|
|
* Generate a unique test identifier
|
|
*/
|
|
export function uniqueTestId(prefix: string = 'test'): string {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
}
|
|
|
|
/**
|
|
* Generate test email
|
|
*/
|
|
export function testEmail(domain: string = 'test.example.com'): string {
|
|
return `${uniqueTestId('user')}@${domain}`
|
|
}
|
|
|
|
// ============================================================================
|
|
// Mock Payload Instance
|
|
// ============================================================================
|
|
|
|
export interface MockPayloadInstance {
|
|
auth: ReturnType<typeof import('vitest').vi.fn>
|
|
find: ReturnType<typeof import('vitest').vi.fn>
|
|
create: ReturnType<typeof import('vitest').vi.fn>
|
|
update: ReturnType<typeof import('vitest').vi.fn>
|
|
delete: ReturnType<typeof import('vitest').vi.fn>
|
|
login: ReturnType<typeof import('vitest').vi.fn>
|
|
}
|
|
|
|
/**
|
|
* Create a mock Payload instance for testing
|
|
*/
|
|
export function createMockPayload(
|
|
vi: typeof import('vitest').vi,
|
|
): MockPayloadInstance {
|
|
return {
|
|
auth: vi.fn().mockResolvedValue({ user: null }),
|
|
find: vi.fn().mockResolvedValue({ docs: [], totalDocs: 0 }),
|
|
create: vi.fn().mockResolvedValue({ id: 1 }),
|
|
update: vi.fn().mockResolvedValue({ id: 1 }),
|
|
delete: vi.fn().mockResolvedValue({ id: 1 }),
|
|
login: vi.fn().mockResolvedValue({
|
|
user: { id: 1, email: 'test@example.com' },
|
|
token: 'mock-token',
|
|
exp: Math.floor(Date.now() / 1000) + 7200,
|
|
}),
|
|
}
|
|
}
|