- Move CI/CD configs to config/ci/ directory - Reorganize scripts into categorized directories (deployment, monitoring, testing, utils) - Consolidate documentation into docs/ directory with proper structure - Update linting and testing configurations - Remove obsolete test reports and performance summaries - Add new documentation for code quality tools and contact form security - Improve project organization and maintainability - Fix lint-staged config to only lint JS/TS files - Disable react/react-in-jsx-scope rule for Next.js compatibility - Ignore scripts and test config directories in ESLint
59 KiB
Contact Form Security Enhancement Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Enhance contact form security with multi-layer rate limiting, improved captcha, and anomaly detection to prevent spam, bot attacks, and DDoS threats.
Architecture: Implement security middleware with Redis-based rate limiting, enhanced captcha generation/validation, input sanitization, and comprehensive security logging. Use existing contact form structure as foundation.
Tech Stack: Next.js 14, TypeScript, Redis (for rate limiting), Zod (validation), existing Resend email service
Task 1: Create Security Configuration Module
Files:
- Create:
src/lib/security/config.ts - Test:
src/lib/security/config.test.ts
Step 1: Write the failing test
import { getSecurityConfig, validateSecurityConfig } from '../config';
describe('Security Config', () => {
test('should return default security configuration', () => {
const config = getSecurityConfig();
expect(config).toBeDefined();
expect(config.rateLimit.ip.maxRequests).toBe(10);
expect(config.rateLimit.ip.windowMinutes).toBe(60);
expect(config.captcha.complexity).toBe('medium');
});
test('should validate security config structure', () => {
const config = getSecurityConfig();
const isValid = validateSecurityConfig(config);
expect(isValid).toBe(true);
});
test('should reject invalid config', () => {
const invalidConfig = { rateLimit: { ip: { maxRequests: -1 } } };
const isValid = validateSecurityConfig(invalidConfig);
expect(isValid).toBe(false);
});
});
Step 2: Run test to verify it fails
Run: npm test -- src/lib/security/config.test.ts
Expected: FAIL with "Cannot find module '../config'"
Step 3: Write minimal implementation
export interface SecurityConfig {
captcha: {
complexity: 'simple' | 'medium' | 'complex';
expiryMinutes: number;
maxAttempts: number;
};
rateLimit: {
ip: {
maxRequests: number;
windowMinutes: number;
};
email: {
maxRequests: number;
windowHours: number;
};
global: {
maxRequests: number;
windowMinutes: number;
};
};
protection: {
enableCSRF: boolean;
enableInputSanitization: boolean;
enableBehaviorDetection: boolean;
blockSuspiciousIPs: boolean;
};
logging: {
enableSecurityLogs: boolean;
logRetentionDays: number;
alertThreshold: number;
};
}
const defaultConfig: SecurityConfig = {
captcha: {
complexity: 'medium',
expiryMinutes: 5,
maxAttempts: 3,
},
rateLimit: {
ip: {
maxRequests: 10,
windowMinutes: 60,
},
email: {
maxRequests: 3,
windowHours: 24,
},
global: {
maxRequests: 50,
windowMinutes: 1,
},
},
protection: {
enableCSRF: true,
enableInputSanitization: true,
enableBehaviorDetection: true,
blockSuspiciousIPs: true,
},
logging: {
enableSecurityLogs: true,
logRetentionDays: 30,
alertThreshold: 5,
},
};
export function getSecurityConfig(): SecurityConfig {
return { ...defaultConfig };
}
export function validateSecurityConfig(config: any): boolean {
if (!config || typeof config !== 'object') return false;
if (!config.rateLimit || !config.captcha || !config.protection) return false;
if (config.rateLimit.ip?.maxRequests < 0) return false;
if (config.captcha.expiryMinutes <= 0) return false;
return true;
}
Step 4: Run test to verify it passes
Run: npm test -- src/lib/security/config.test.ts
Expected: PASS
Step 5: Commit
git add src/lib/security/config.ts src/lib/security/config.test.ts
git commit -m "feat: add security configuration module"
Task 2: Implement Enhanced Captcha System
Files:
- Create:
src/lib/security/captcha.ts - Test:
src/lib/security/captcha.test.ts
Step 1: Write the failing test
import { generateCaptcha, validateCaptcha, CaptchaResult } from '../captcha';
describe('Enhanced Captcha System', () => {
test('should generate captcha with medium complexity', () => {
const result = generateCaptcha('medium');
expect(result).toBeDefined();
expect(result.question).toBeDefined();
expect(result.answer).toBeDefined();
expect(result.hash).toBeDefined();
expect(result.timestamp).toBeDefined();
expect(typeof result.question).toBe('string');
expect(typeof result.answer).toBe('number');
});
test('should generate different captcha each time', () => {
const result1 = generateCaptcha('medium');
const result2 = generateCaptcha('medium');
expect(result1.hash).not.toBe(result2.hash);
});
test('should validate correct captcha answer', () => {
const captcha = generateCaptcha('medium');
const isValid = validateCaptcha(captcha.hash, captcha.answer, captcha.timestamp);
expect(isValid).toBe(true);
});
test('should reject incorrect captcha answer', () => {
const captcha = generateCaptcha('medium');
const isValid = validateCaptcha(captcha.hash, captcha.answer + 1, captcha.timestamp);
expect(isValid).toBe(false);
});
test('should reject expired captcha', () => {
const captcha = generateCaptcha('medium');
const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago
const isValid = validateCaptcha(captcha.hash, captcha.answer, expiredTimestamp);
expect(isValid).toBe(false);
});
test('should generate complex captcha', () => {
const result = generateCaptcha('complex');
expect(result).toBeDefined();
const numbers = result.question.match(/\d+/g);
expect(numbers?.length).toBeGreaterThan(2);
});
});
Step 2: Run test to verify it fails
Run: npm test -- src/lib/security/captcha.test.ts
Expected: FAIL with "Cannot find module '../captcha'"
Step 3: Write minimal implementation
import { getSecurityConfig } from './config';
export interface CaptchaResult {
question: string;
answer: number;
hash: string;
timestamp: number;
}
function generateSimpleCaptcha(): CaptchaResult {
const num1 = Math.floor(Math.random() * 10) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
const answer = num1 + num2;
const question = `${num1} + ${num2} = ?`;
return {
question,
answer,
hash: '',
timestamp: Date.now(),
};
}
function generateMediumCaptcha(): CaptchaResult {
const operations = ['+', '-', '*'];
const operation = operations[Math.floor(Math.random() * operations.length)];
const num1 = Math.floor(Math.random() * 20) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
let answer: number;
let question: string;
switch (operation) {
case '+':
answer = num1 + num2;
question = `${num1} + ${num2} = ?`;
break;
case '-':
answer = num1 - num2;
question = `${num1} - ${num2} = ?`;
break;
case '*':
answer = num1 * num2;
question = `${num1} × ${num2} = ?`;
break;
default:
answer = num1 + num2;
question = `${num1} + ${num2} = ?`;
}
return {
question,
answer,
hash: '',
timestamp: Date.now(),
};
}
function generateComplexCaptcha(): CaptchaResult {
const num1 = Math.floor(Math.random() * 15) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
const num3 = Math.floor(Math.random() * 5) + 1;
const operations = ['+', '-', '*'];
const op1 = operations[Math.floor(Math.random() * operations.length)];
const op2 = operations[Math.floor(Math.random() * operations.length)];
let answer: number;
let question: string;
const temp1 = op1 === '+' ? num1 + num2 : op1 === '-' ? num1 - num2 : num1 * num2;
answer = op2 === '+' ? temp1 + num3 : op2 === '-' ? temp1 - num3 : temp1 * num3;
question = `(${num1} ${op1} ${num2}) ${op2} ${num3} = ?`;
return {
question,
answer,
hash: '',
timestamp: Date.now(),
};
}
function generateHash(answer: number, timestamp: number): string {
const secret = process.env.CAPTCHA_SECRET || 'default-secret-key';
const data = `${answer}-${timestamp}-${secret}`;
return Buffer.from(data).toString('base64');
}
export function generateCaptcha(complexity: 'simple' | 'medium' | 'complex' = 'medium'): CaptchaResult {
let captcha: CaptchaResult;
switch (complexity) {
case 'simple':
captcha = generateSimpleCaptcha();
break;
case 'complex':
captcha = generateComplexCaptcha();
break;
case 'medium':
default:
captcha = generateMediumCaptcha();
break;
}
captcha.hash = generateHash(captcha.answer, captcha.timestamp);
return captcha;
}
export function validateCaptcha(hash: string, answer: number, timestamp: number): boolean {
const config = getSecurityConfig();
const now = Date.now();
const ageMinutes = (now - timestamp) / (1000 * 60);
if (ageMinutes > config.captcha.expiryMinutes) {
return false;
}
const expectedHash = generateHash(answer, timestamp);
return hash === expectedHash;
}
Step 4: Run test to verify it passes
Run: npm test -- src/lib/security/captcha.test.ts
Expected: PASS
Step 5: Commit
git add src/lib/security/captcha.ts src/lib/security/captcha.test.ts
git commit -m "feat: implement enhanced captcha system"
Task 3: Implement Rate Limiting System
Files:
- Create:
src/lib/security/rate-limiter.ts - Test:
src/lib/security/rate-limiter.test.ts
Step 1: Write the failing test
import { RateLimiter, RateLimitResult } from '../rate-limiter';
describe('Rate Limiting System', () => {
let rateLimiter: RateLimiter;
beforeEach(() => {
rateLimiter = new RateLimiter();
});
test('should allow request within limit', async () => {
const result = await rateLimiter.checkIP('192.168.1.1');
expect(result.allowed).toBe(true);
expect(result.remaining).toBeGreaterThan(0);
});
test('should block request when limit exceeded', async () => {
const ip = '192.168.1.2';
for (let i = 0; i < 10; i++) {
await rateLimiter.checkIP(ip);
}
const result = await rateLimiter.checkIP(ip);
expect(result.allowed).toBe(false);
});
test('should reset limit after window expires', async () => {
const ip = '192.168.1.3';
for (let i = 0; i < 10; i++) {
await rateLimiter.checkIP(ip);
}
await rateLimiter.checkIP(ip);
jest.useFakeTimers();
jest.advanceTimersByTime(61 * 60 * 1000);
const result = await rateLimiter.checkIP(ip);
expect(result.allowed).toBe(true);
jest.useRealTimers();
});
test('should track email rate limits separately', async () => {
const email = 'test@example.com';
for (let i = 0; i < 3; i++) {
await rateLimiter.checkEmail(email);
}
const result = await rateLimiter.checkEmail(email);
expect(result.allowed).toBe(false);
});
test('should enforce global rate limit', async () => {
const promises = [];
for (let i = 0; i < 51; i++) {
promises.push(rateLimiter.checkGlobal());
}
const results = await Promise.all(promises);
const blockedCount = results.filter(r => !r.allowed).length;
expect(blockedCount).toBeGreaterThan(0);
});
});
Step 2: Run test to verify it fails
Run: npm test -- src/lib/security/rate-limiter.test.ts
Expected: FAIL with "Cannot find module '../rate-limiter'"
Step 3: Write minimal implementation
import { getSecurityConfig } from './config';
export interface RateLimitResult {
allowed: boolean;
remaining: number;
resetTime: number;
limit: number;
}
interface RequestRecord {
count: number;
windowStart: number;
}
class InMemoryRateLimiter {
private ipRequests = new Map<string, RequestRecord>();
private emailRequests = new Map<string, RequestRecord>();
private globalRequests: RequestRecord = { count: 0, windowStart: Date.now() };
async checkIP(ip: string): Promise<RateLimitResult> {
const config = getSecurityConfig();
const now = Date.now();
const windowMs = config.rateLimit.ip.windowMinutes * 60 * 1000;
let record = this.ipRequests.get(ip);
if (!record || now - record.windowStart > windowMs) {
record = { count: 0, windowStart: now };
this.ipRequests.set(ip, record);
}
const allowed = record.count < config.rateLimit.ip.maxRequests;
if (allowed) {
record.count++;
}
return {
allowed,
remaining: Math.max(0, config.rateLimit.ip.maxRequests - record.count),
resetTime: record.windowStart + windowMs,
limit: config.rateLimit.ip.maxRequests,
};
}
async checkEmail(email: string): Promise<RateLimitResult> {
const config = getSecurityConfig();
const now = Date.now();
const windowMs = config.rateLimit.email.windowHours * 60 * 60 * 1000;
let record = this.emailRequests.get(email);
if (!record || now - record.windowStart > windowMs) {
record = { count: 0, windowStart: now };
this.emailRequests.set(email, record);
}
const allowed = record.count < config.rateLimit.email.maxRequests;
if (allowed) {
record.count++;
}
return {
allowed,
remaining: Math.max(0, config.rateLimit.email.maxRequests - record.count),
resetTime: record.windowStart + windowMs,
limit: config.rateLimit.email.maxRequests,
};
}
async checkGlobal(): Promise<RateLimitResult> {
const config = getSecurityConfig();
const now = Date.now();
const windowMs = config.rateLimit.global.windowMinutes * 60 * 1000;
if (now - this.globalRequests.windowStart > windowMs) {
this.globalRequests = { count: 0, windowStart: now };
}
const allowed = this.globalRequests.count < config.rateLimit.global.maxRequests;
if (allowed) {
this.globalRequests.count++;
}
return {
allowed,
remaining: Math.max(0, config.rateLimit.global.maxRequests - this.globalRequests.count),
resetTime: this.globalRequests.windowStart + windowMs,
limit: config.rateLimit.global.maxRequests,
};
}
clear(): void {
this.ipRequests.clear();
this.emailRequests.clear();
this.globalRequests = { count: 0, windowStart: Date.now() };
}
}
export class RateLimiter {
private limiter: InMemoryRateLimiter;
constructor() {
this.limiter = new InMemoryRateLimiter();
}
async checkIP(ip: string): Promise<RateLimitResult> {
return this.limiter.checkIP(ip);
}
async checkEmail(email: string): Promise<RateLimitResult> {
return this.limiter.checkEmail(email);
}
async checkGlobal(): Promise<RateLimitResult> {
return this.limiter.checkGlobal();
}
clear(): void {
this.limiter.clear();
}
}
Step 4: Run test to verify it passes
Run: npm test -- src/lib/security/rate-limiter.test.ts
Expected: PASS
Step 5: Commit
git add src/lib/security/rate-limiter.ts src/lib/security/rate-limiter.test.ts
git commit -m "feat: implement rate limiting system"
Task 4: Implement Input Sanitization
Files:
- Create:
src/lib/security/sanitizer.ts - Test:
src/lib/security/sanitizer.test.ts
Step 1: Write the failing test
import { sanitizeInput, sanitizeFormData, detectMaliciousContent } from '../sanitizer';
describe('Input Sanitization', () => {
test('should remove XSS attempts', () => {
const malicious = '<script>alert("xss")</script>Hello';
const sanitized = sanitizeInput(malicious);
expect(sanitized).not.toContain('<script>');
expect(sanitized).toContain('Hello');
});
test('should remove SQL injection attempts', () => {
const malicious = "'; DROP TABLE users; --";
const sanitized = sanitizeInput(malicious);
expect(sanitized).not.toContain('DROP TABLE');
});
test('should detect malicious links', () => {
const content = 'Visit http://malicious.com for free money';
const isMalicious = detectMaliciousContent(content);
expect(isMalicious).toBe(true);
});
test('should sanitize form data', () => {
const formData = {
name: '<script>alert(1)</script>John',
email: 'test@example.com',
message: 'Click http://evil.com',
};
const sanitized = sanitizeFormData(formData);
expect(sanitized.name).not.toContain('<script>');
expect(sanitized.email).toBe('test@example.com');
});
test('should preserve legitimate content', () => {
const legitimate = 'Hello, I need help with my order #12345';
const sanitized = sanitizeInput(legitimate);
expect(sanitized).toBe(legitimate);
});
});
Step 2: Run test to verify it fails
Run: npm test -- src/lib/security/sanitizer.test.ts
Expected: FAIL with "Cannot find module '../sanitizer'"
Step 3: Write minimal implementation
export interface SanitizedFormData {
name: string;
email: string;
phone?: string;
subject: string;
message: string;
}
const XSS_PATTERNS = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi,
/javascript:/gi,
/on\w+\s*=/gi,
/<[^>]*>/g,
];
const SQL_INJECTION_PATTERNS = [
/('|(\\')|(;)|(\-\-)|(\s+(or|and)\s+.*=)|(exec(\s|\+)+(s|x)p\w+)/gi,
/(union\s+select)|(drop\s+table)|(delete\s+from)|(insert\s+into)/gi,
];
const MALICIOUS_URL_PATTERNS = [
/(https?:\/\/[^\s]+\.ru\b)/gi,
/(https?:\/\/[^\s]+\.tk\b)/gi,
/(https?:\/\/[^\s]+\.ml\b)/gi,
/(https?:\/\/[^\s]+\.ga\b)/gi,
/(https?:\/\/bit\.ly\/[^\s]+)/gi,
];
export function sanitizeInput(input: string): string {
if (!input || typeof input !== 'string') {
return '';
}
let sanitized = input.trim();
XSS_PATTERNS.forEach(pattern => {
sanitized = sanitized.replace(pattern, '');
});
SQL_INJECTION_PATTERNS.forEach(pattern => {
sanitized = sanitized.replace(pattern, '');
});
return sanitized;
}
export function sanitizeFormData(formData: any): SanitizedFormData {
return {
name: sanitizeInput(formData.name || ''),
email: formData.email || '',
phone: formData.phone ? sanitizeInput(formData.phone) : undefined,
subject: sanitizeInput(formData.subject || ''),
message: sanitizeInput(formData.message || ''),
};
}
export function detectMaliciousContent(content: string): boolean {
if (!content || typeof content !== 'string') {
return false;
}
const lowerContent = content.toLowerCase();
const suspiciousKeywords = [
'free money',
'bitcoin',
'cryptocurrency',
'lottery',
'winner',
'prize',
'inheritance',
'nigerian prince',
'urgent',
'act now',
'limited time',
];
for (const keyword of suspiciousKeywords) {
if (lowerContent.includes(keyword)) {
return true;
}
}
for (const pattern of MALICIOUS_URL_PATTERNS) {
if (pattern.test(content)) {
return true;
}
}
return false;
}
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
export function validatePhone(phone: string): boolean {
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
}
Step 4: Run test to verify it passes
Run: npm test -- src/lib/security/sanitizer.test.ts
Expected: PASS
Step 5: Commit
git add src/lib/security/sanitizer.ts src/lib/security/sanitizer.test.ts
git commit -m "feat: implement input sanitization"
Task 5: Implement Security Logging System
Files:
- Create:
src/lib/security/logger.ts - Test:
src/lib/security/logger.test.ts
Step 1: Write the failing test
import { SecurityLogger, SecurityEventType } from '../logger';
describe('Security Logging System', () => {
let logger: SecurityLogger;
beforeEach(() => {
logger = new SecurityLogger();
});
test('should log security events', () => {
logger.logEvent({
type: SecurityEventType.CAPTCHA_FAILED,
ip: '192.168.1.1',
email: 'test@example.com',
details: { reason: 'Invalid answer' },
});
const logs = logger.getRecentLogs(10);
expect(logs.length).toBe(1);
expect(logs[0].type).toBe(SecurityEventType.CAPTCHA_FAILED);
});
test('should detect suspicious activity', () => {
for (let i = 0; i < 6; i++) {
logger.logEvent({
type: SecurityEventType.CAPTCHA_FAILED,
ip: '192.168.1.2',
email: 'suspicious@example.com',
details: {},
});
}
const suspiciousIPs = logger.getSuspiciousIPs();
expect(suspiciousIPs).toContain('192.168.1.2');
});
test('should clear old logs', () => {
logger.logEvent({
type: SecurityEventType.RATE_LIMIT_EXCEEDED,
ip: '192.168.1.3',
details: {},
});
logger.clearOldLogs(31);
const logs = logger.getRecentLogs(10);
expect(logs.length).toBe(0);
});
});
Step 2: Run test to verify it fails
Run: npm test -- src/lib/security/logger.test.ts
Expected: FAIL with "Cannot find module '../logger'"
Step 3: Write minimal implementation
export enum SecurityEventType {
CAPTCHA_FAILED = 'captcha_failed',
CAPTCHA_EXPIRED = 'captcha_expired',
RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded',
MALICIOUS_CONTENT = 'malicious_content',
XSS_ATTEMPT = 'xss_attempt',
SQL_INJECTION_ATTEMPT = 'sql_injection_attempt',
SUSPICIOUS_IP = 'suspicious_ip',
BLOCKED_REQUEST = 'blocked_request',
}
export interface SecurityEvent {
type: SecurityEventType;
timestamp: number;
ip?: string;
email?: string;
userAgent?: string;
details?: Record<string, any>;
}
export interface SuspiciousActivity {
ip: string;
eventCount: number;
lastSeen: number;
eventTypes: SecurityEventType[];
}
class InMemorySecurityLogger {
private events: SecurityEvent[] = [];
private ipActivity = new Map<string, number>();
logEvent(event: Omit<SecurityEvent, 'timestamp'>): void {
const fullEvent: SecurityEvent = {
...event,
timestamp: Date.now(),
};
this.events.push(fullEvent);
if (event.ip) {
const currentCount = this.ipActivity.get(event.ip) || 0;
this.ipActivity.set(event.ip, currentCount + 1);
}
console.log(`[Security] ${event.type}:`, event);
}
getRecentLogs(limit: number = 100): SecurityEvent[] {
return this.events.slice(-limit);
}
getSuspiciousIPs(threshold: number = 5): string[] {
const suspicious: string[] = [];
for (const [ip, count] of this.ipActivity.entries()) {
if (count >= threshold) {
suspicious.push(ip);
}
}
return suspicious;
}
clearOldLogs(daysToKeep: number = 30): void {
const cutoffTime = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000);
this.events = this.events.filter(event => event.timestamp > cutoffTime);
}
getActivityByIP(ip: string): number {
return this.ipActivity.get(ip) || 0;
}
clear(): void {
this.events = [];
this.ipActivity.clear();
}
}
export class SecurityLogger {
private logger: InMemorySecurityLogger;
constructor() {
this.logger = new InMemorySecurityLogger();
}
logEvent(event: Omit<SecurityEvent, 'timestamp'>): void {
this.logger.logEvent(event);
}
getRecentLogs(limit?: number): SecurityEvent[] {
return this.logger.getRecentLogs(limit);
}
getSuspiciousIPs(threshold?: number): string[] {
return this.logger.getSuspiciousIPs(threshold);
}
clearOldLogs(daysToKeep?: number): void {
this.logger.clearOldLogs(daysToKeep);
}
getActivityByIP(ip: string): number {
return this.logger.getActivityByIP(ip);
}
clear(): void {
this.logger.clear();
}
}
Step 4: Run test to verify it passes
Run: npm test -- src/lib/security/logger.test.ts
Expected: PASS
Step 5: Commit
git add src/lib/security/logger.ts src/lib/security/logger.test.ts
git commit -m "feat: implement security logging system"
Task 6: Create Security Middleware
Files:
- Create:
src/lib/security/middleware.ts - Test:
src/lib/security/middleware.test.ts
Step 1: Write the failing test
import { SecurityMiddleware, SecurityCheckResult } from '../middleware';
describe('Security Middleware', () => {
let middleware: SecurityMiddleware;
beforeEach(() => {
middleware = new SecurityMiddleware();
});
test('should pass valid request', async () => {
const result = await middleware.checkRequest({
ip: '192.168.1.1',
email: 'valid@example.com',
captchaHash: 'valid-hash',
captchaAnswer: 5,
captchaTimestamp: Date.now(),
});
expect(result.allowed).toBe(true);
expect(result.errors).toHaveLength(0);
});
test('should block request with invalid captcha', async () => {
const result = await middleware.checkRequest({
ip: '192.168.1.2',
email: 'test@example.com',
captchaHash: 'invalid-hash',
captchaAnswer: 999,
captchaTimestamp: Date.now(),
});
expect(result.allowed).toBe(false);
expect(result.errors).toContain('Invalid captcha');
});
test('should block request exceeding rate limit', async () => {
const ip = '192.168.1.3';
for (let i = 0; i < 11; i++) {
await middleware.checkRequest({
ip,
email: `test${i}@example.com`,
captchaHash: 'valid-hash',
captchaAnswer: 5,
captchaTimestamp: Date.now(),
});
}
const result = await middleware.checkRequest({
ip,
email: 'test@example.com',
captchaHash: 'valid-hash',
captchaAnswer: 5,
captchaTimestamp: Date.now(),
});
expect(result.allowed).toBe(false);
expect(result.errors).toContain('Rate limit exceeded');
});
test('should block request with malicious content', async () => {
const result = await middleware.checkRequest({
ip: '192.168.1.4',
email: 'test@example.com',
captchaHash: 'valid-hash',
captchaAnswer: 5,
captchaTimestamp: Date.now(),
message: '<script>alert("xss")</script>',
});
expect(result.allowed).toBe(false);
expect(result.errors.some(e => e.includes('malicious'))).toBe(true);
});
});
Step 2: Run test to verify it fails
Run: npm test -- src/lib/security/middleware.test.ts
Expected: FAIL with "Cannot find module '../middleware'"
Step 3: Write minimal implementation
import { RateLimiter } from './rate-limiter';
import { validateCaptcha } from './captcha';
import { sanitizeFormData, detectMaliciousContent, validateEmail } from './sanitizer';
import { SecurityLogger, SecurityEventType } from './logger';
import { getSecurityConfig } from './config';
export interface SecurityCheckRequest {
ip: string;
email?: string;
name?: string;
phone?: string;
subject?: string;
message?: string;
captchaHash: string;
captchaAnswer: number;
captchaTimestamp: number;
userAgent?: string;
}
export interface SecurityCheckResult {
allowed: boolean;
errors: string[];
warnings: string[];
}
export class SecurityMiddleware {
private rateLimiter: RateLimiter;
private logger: SecurityLogger;
private config: any;
constructor() {
this.rateLimiter = new RateLimiter();
this.logger = new SecurityLogger();
this.config = getSecurityConfig();
}
async checkRequest(request: SecurityCheckRequest): Promise<SecurityCheckResult> {
const errors: string[] = [];
const warnings: string[] = [];
if (this.config.protection.enableInputSanitization) {
if (request.message && detectMaliciousContent(request.message)) {
errors.push('Message contains malicious content');
this.logger.logEvent({
type: SecurityEventType.MALICIOUS_CONTENT,
ip: request.ip,
email: request.email,
userAgent: request.userAgent,
details: { message: request.message },
});
}
}
if (!validateCaptcha(request.captchaHash, request.captchaAnswer, request.captchaTimestamp)) {
errors.push('Invalid captcha');
this.logger.logEvent({
type: SecurityEventType.CAPTCHA_FAILED,
ip: request.ip,
email: request.email,
userAgent: request.userAgent,
details: { answer: request.captchaAnswer },
});
}
const ipRateLimit = await this.rateLimiter.checkIP(request.ip);
if (!ipRateLimit.allowed) {
errors.push('Rate limit exceeded for IP');
this.logger.logEvent({
type: SecurityEventType.RATE_LIMIT_EXCEEDED,
ip: request.ip,
email: request.email,
details: { limit: ipRateLimit.limit },
});
}
if (request.email) {
const emailRateLimit = await this.rateLimiter.checkEmail(request.email);
if (!emailRateLimit.allowed) {
errors.push('Rate limit exceeded for email');
this.logger.logEvent({
type: SecurityEventType.RATE_LIMIT_EXCEEDED,
ip: request.ip,
email: request.email,
details: { limit: emailRateLimit.limit },
});
}
}
const globalRateLimit = await this.rateLimiter.checkGlobal();
if (!globalRateLimit.allowed) {
warnings.push('Global rate limit nearly exceeded');
}
const suspiciousIPs = this.logger.getSuspiciousIPs();
if (suspiciousIPs.includes(request.ip)) {
errors.push('Suspicious activity detected from this IP');
this.logger.logEvent({
type: SecurityEventType.SUSPICIOUS_IP,
ip: request.ip,
email: request.email,
details: { activityCount: this.logger.getActivityByIP(request.ip) },
});
}
return {
allowed: errors.length === 0,
errors,
warnings,
};
}
sanitizeRequest(request: SecurityCheckRequest): any {
return sanitizeFormData({
name: request.name || '',
email: request.email || '',
phone: request.phone,
subject: request.subject || '',
message: request.message || '',
});
}
getSecurityLogs(limit?: number) {
return this.logger.getRecentLogs(limit);
}
getSuspiciousIPs(threshold?: number) {
return this.logger.getSuspiciousIPs(threshold);
}
}
Step 4: Run test to verify it passes
Run: npm test -- src/lib/security/middleware.test.ts
Expected: PASS
Step 5: Commit
git add src/lib/security/middleware.ts src/lib/security/middleware.test.ts
git commit -m "feat: implement security middleware"
Task 7: Update Contact Form API with Security
Files:
- Modify:
src/app/api/contact/route.ts - Test:
src/app/api/contact/route.test.ts
Step 1: Update existing tests to include security checks
import { POST } from '../route';
describe('Contact API Security', () => {
test('should reject request without valid captcha', async () => {
const request = new Request('http://localhost:3000/api/contact', {
method: 'POST',
body: JSON.stringify({
name: 'Test User',
email: 'test@example.com',
subject: 'Test',
message: 'Test message',
captchaHash: 'invalid',
captchaAnswer: 999,
captchaTimestamp: Date.now(),
}),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain('captcha');
});
test('should reject request with malicious content', async () => {
const request = new Request('http://localhost:3000/api/contact', {
method: 'POST',
body: JSON.stringify({
name: '<script>alert(1)</script>',
email: 'test@example.com',
subject: 'Test',
message: 'Test message',
captchaHash: 'valid-hash',
captchaAnswer: 5,
captchaTimestamp: Date.now(),
}),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
});
});
Step 2: Run test to verify it fails
Run: npm test -- src/app/api/contact/route.test.ts
Expected: FAIL with security validation errors
Step 3: Update contact route implementation
import { NextRequest } from 'next/server';
import { Resend } from 'resend';
import { z } from 'zod';
import { SecurityMiddleware, SecurityCheckRequest } from '@/lib/security/middleware';
import { generateCaptcha } from '@/lib/security/captcha';
const resend = new Resend(process.env.RESEND_API_KEY);
const companyEmail = process.env.COMPANY_EMAIL || 'contact@novalon.cn';
const securityMiddleware = new SecurityMiddleware();
export async function GET(request: NextRequest) {
const captcha = generateCaptcha('medium');
return Response.json({
question: captcha.question,
hash: captcha.hash,
timestamp: captcha.timestamp,
});
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const securityRequest: SecurityCheckRequest = {
ip: request.headers.get('x-forwarded-for') || 'unknown',
email: body.email,
name: body.name,
phone: body.phone,
subject: body.subject,
message: body.message,
captchaHash: body.captchaHash,
captchaAnswer: body.captchaAnswer,
captchaTimestamp: body.captchaTimestamp,
userAgent: request.headers.get('user-agent'),
};
const securityResult = await securityMiddleware.checkRequest(securityRequest);
if (!securityResult.allowed) {
return Response.json(
{
success: false,
error: securityResult.errors[0] || 'Security validation failed',
warnings: securityResult.warnings,
},
{ status: 400 }
);
}
const sanitizedData = securityMiddleware.sanitizeRequest(securityRequest);
if (sanitizedData.website) {
return Response.json({ success: true, message: '消息已发送' });
}
const emailContent = `
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: sans-serif; line-height: 1.6; color: #1C1C1C; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #C41E3A; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; }
.info-row { margin-bottom: 10px; }
.info-label { font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>新的客户咨询</h1>
</div>
<div class="content">
<div class="info-row"><span class="info-label">姓名:</span> ${sanitizedData.name}</div>
<div class="info-row"><span class="info-label">邮箱:</span> ${sanitizedData.email}</div>
${sanitizedData.phone ? `<div class="info-row"><span class="info-label">电话:</span> ${sanitizedData.phone}</div>` : ''}
<div class="info-row"><span class="info-label">主题:</span> ${sanitizedData.subject}</div>
<div class="info-row"><span class="info-label">留言:</span> ${sanitizedData.message}</div>
</div>
</div>
</body>
</html>
`;
const result = await resend.emails.send({
from: '睿新致远官网 <onboarding@resend.dev>',
to: [companyEmail],
subject: `${sanitizedData.subject} - ${sanitizedData.name}`,
html: emailContent,
replyTo: sanitizedData.email,
});
if (result.error) {
console.error('Resend API error:', result.error);
return Response.json(
{ success: false, error: '邮件发送失败,请稍后重试' },
{ status: 500 }
);
}
return Response.json({ success: true, message: '消息已发送' });
} catch (error) {
console.error('Contact form submission error:', error);
return Response.json(
{ success: false, error: '提交失败,请重试' },
{ status: 500 }
);
}
}
Step 4: Run test to verify it passes
Run: npm test -- src/app/api/contact/route.test.ts
Expected: PASS
Step 5: Commit
git add src/app/api/contact/route.ts src/app/api/contact/route.test.ts
git commit -m "feat: integrate security middleware into contact API"
Task 8: Update Contact Form Client Component
Files:
- Modify:
src/app/(marketing)/contact/page.tsx - Modify:
src/app/(marketing)/contact/actions.ts
Step 1: Update contact page to use enhanced captcha
'use client';
import { useState, useEffect } from 'react';
import { submitContactForm } from '../actions';
export default function ContactPage() {
const [captcha, setCaptcha] = useState<any>(null);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
subject: '',
message: '',
captchaAnswer: '',
});
useEffect(() => {
fetchCaptcha();
}, []);
const fetchCaptcha = async () => {
try {
const response = await fetch('/api/contact');
const data = await response.json();
setCaptcha(data);
} catch (error) {
console.error('Failed to fetch captcha:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!captcha) {
alert('请等待验证码加载');
return;
}
const result = await submitContactForm({
...formData,
captchaHash: captcha.hash,
captchaAnswer: parseInt(formData.captchaAnswer),
captchaTimestamp: captcha.timestamp,
submitTime: Date.now().toString(),
});
if (result.success) {
alert('消息已发送');
setFormData({
name: '',
email: '',
phone: '',
subject: '',
message: '',
captchaAnswer: '',
});
fetchCaptcha();
} else {
alert(result.error || '发送失败');
if (result.error?.includes('captcha')) {
fetchCaptcha();
}
}
};
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">联系我们</h1>
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto space-y-6">
<div>
<label className="block mb-2">姓名 *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full p-3 border rounded"
required
/>
</div>
<div>
<label className="block mb-2">邮箱 *</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full p-3 border rounded"
required
/>
</div>
<div>
<label className="block mb-2">电话</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full p-3 border rounded"
/>
</div>
<div>
<label className="block mb-2">主题 *</label>
<input
type="text"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
className="w-full p-3 border rounded"
required
/>
</div>
<div>
<label className="block mb-2">留言 *</label>
<textarea
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full p-3 border rounded h-32"
required
/>
</div>
{captcha && (
<div>
<label className="block mb-2">验证码 *</label>
<div className="flex items-center gap-4">
<div className="bg-gray-100 p-4 rounded text-lg font-semibold">
{captcha.question}
</div>
<input
type="text"
value={formData.captchaAnswer}
onChange={(e) => setFormData({ ...formData, captchaAnswer: e.target.value })}
className="w-32 p-3 border rounded"
placeholder="答案"
required
/>
<button
type="button"
onClick={fetchCaptcha}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
刷新
</button>
</div>
</div>
)}
<button
type="submit"
className="w-full bg-red-700 text-white py-3 rounded hover:bg-red-800"
>
发送消息
</button>
</form>
</div>
);
}
Step 2: Update actions to work with new security
'use server';
import { Resend } from 'resend';
import { z } from 'zod';
import { SecurityMiddleware } from '@/lib/security/middleware';
const resend = new Resend(process.env.RESEND_API_KEY);
const companyEmail = process.env.COMPANY_EMAIL || 'contact@novalon.cn';
const securityMiddleware = new SecurityMiddleware();
const contactFormSchema = z.object({
name: z.string().min(2, '姓名至少需要2个字符'),
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号码').optional(),
email: z.string().email('请输入有效的邮箱地址'),
subject: z.string().min(2, '主题至少需要2个字符'),
message: z.string().min(10, '留言内容至少需要10个字符'),
captchaHash: z.string(),
captchaAnswer: z.number(),
captchaTimestamp: z.number(),
submitTime: z.string().optional(),
website: z.string().optional(),
});
export interface ContactFormState {
success: boolean;
message?: string;
error?: string;
errors?: Record<string, string>;
}
export async function submitContactForm(
_prevState: ContactFormState | null,
formData: any
): Promise<ContactFormState> {
const validationResult = contactFormSchema.safeParse(formData);
if (!validationResult.success) {
const errors: Record<string, string> = {};
validationResult.error.issues.forEach((issue) => {
const field = issue.path[0] as string;
errors[field] = issue.message;
});
return { success: false, error: '请检查表单字段', errors };
}
const data = validationResult.data;
const securityCheck = await securityMiddleware.checkRequest({
ip: 'unknown',
email: data.email,
name: data.name,
phone: data.phone,
subject: data.subject,
message: data.message,
captchaHash: data.captchaHash,
captchaAnswer: data.captchaAnswer,
captchaTimestamp: data.captchaTimestamp,
});
if (!securityCheck.allowed) {
return {
success: false,
error: securityCheck.errors[0] || '安全验证失败',
};
}
const sanitizedData = securityMiddleware.sanitizeRequest({
ip: 'unknown',
email: data.email,
name: data.name,
phone: data.phone,
subject: data.subject,
message: data.message,
captchaHash: data.captchaHash,
captchaAnswer: data.captchaAnswer,
captchaTimestamp: data.captchaTimestamp,
});
try {
const { data: emailData, error } = await resend.emails.send({
from: '睿新致远官网 <onboarding@resend.dev>',
to: [companyEmail],
subject: `📧 ${sanitizedData.subject} - ${sanitizedData.name}`,
html: generateEmailContent(sanitizedData),
replyTo: sanitizedData.email,
});
if (error) {
console.error('Resend API error:', error);
return { success: false, error: '邮件发送失败,请稍后重试' };
}
console.log('Email sent successfully:', emailData);
return { success: true, message: '消息已发送' };
} catch (error) {
console.error('Contact form submission error:', error);
return { success: false, error: '提交失败,请重试' };
}
}
function generateEmailContent(data: any): string {
return `
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: sans-serif; line-height: 1.6; color: #1C1C1C; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #C41E3A; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; }
.info-row { margin-bottom: 10px; }
.info-label { font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📬 新的客户咨询</h1>
</div>
<div class="content">
<div class="info-row"><span class="info-label">姓名:</span> ${data.name}</div>
<div class="info-row"><span class="info-label">邮箱:</span> ${data.email}</div>
${data.phone ? `<div class="info-row"><span class="info-label">电话:</span> ${data.phone}</div>` : ''}
<div class="info-row"><span class="info-label">主题:</span> ${data.subject}</div>
<div class="info-row"><span class="info-label">留言:</span> ${data.message}</div>
</div>
</div>
</body>
</html>
`;
}
Step 3: Run tests
Run: npm test -- src/app/(marketing)/contact/
Expected: PASS
Step 4: Commit
git add src/app/(marketing)/contact/page.tsx src/app/(marketing)/contact/actions.ts
git commit -m "feat: update contact form with enhanced security"
Task 9: Create Security Monitoring Dashboard
Files:
- Create:
src/app/admin/security/page.tsx - Create:
src/app/api/admin/security/route.ts
Step 1: Create security API endpoint
import { NextRequest, NextResponse } from 'next/server';
import { SecurityMiddleware } from '@/lib/security/middleware';
const securityMiddleware = new SecurityMiddleware();
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get('limit') || '100');
const logs = securityMiddleware.getSecurityLogs(limit);
const suspiciousIPs = securityMiddleware.getSuspiciousIPs();
return NextResponse.json({
success: true,
data: {
logs,
suspiciousIPs,
summary: {
totalEvents: logs.length,
suspiciousIPCount: suspiciousIPs.length,
},
},
});
} catch (error) {
console.error('Security API error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch security data' },
{ status: 500 }
);
}
}
Step 2: Create security monitoring page
'use client';
import { useState, useEffect } from 'react';
interface SecurityLog {
type: string;
timestamp: number;
ip?: string;
email?: string;
details?: any;
}
interface SecurityData {
logs: SecurityLog[];
suspiciousIPs: string[];
summary: {
totalEvents: number;
suspiciousIPCount: number;
};
}
export default function SecurityMonitoringPage() {
const [securityData, setSecurityData] = useState<SecurityData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchSecurityData();
}, []);
const fetchSecurityData = async () => {
try {
const response = await fetch('/api/admin/security');
const data = await response.json();
if (data.success) {
setSecurityData(data.data);
}
} catch (error) {
console.error('Failed to fetch security data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <div className="p-8">加载中...</div>;
}
if (!securityData) {
return <div className="p-8">无法加载安全数据</div>;
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">安全监控</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-2">总事件数</h3>
<p className="text-3xl font-bold text-blue-600">{securityData.summary.totalEvents}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-2">可疑IP数</h3>
<p className="text-3xl font-bold text-red-600">{securityData.summary.suspiciousIPCount}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-2">最近24小时</h3>
<p className="text-3xl font-bold text-green-600">
{securityData.logs.filter(log => Date.now() - log.timestamp < 24 * 60 * 60 * 1000).length}
</p>
</div>
</div>
<div className="bg-white rounded-lg shadow mb-8">
<h2 className="text-xl font-semibold p-6 border-b">可疑IP列表</h2>
<div className="p-6">
{securityData.suspiciousIPs.length === 0 ? (
<p className="text-gray-500">暂无可疑IP</p>
) : (
<ul className="space-y-2">
{securityData.suspiciousIPs.map((ip, index) => (
<li key={index} className="flex items-center justify-between p-3 bg-red-50 rounded">
<span className="font-mono">{ip}</span>
<span className="text-red-600 text-sm">可疑活动</span>
</li>
))}
</ul>
)}
</div>
</div>
<div className="bg-white rounded-lg shadow">
<h2 className="text-xl font-semibold p-6 border-b">安全事件日志</h2>
<div className="p-6">
{securityData.logs.length === 0 ? (
<p className="text-gray-500">暂无安全事件</p>
) : (
<div className="space-y-4">
{securityData.logs.slice(0, 20).map((log, index) => (
<div key={index} className="p-4 bg-gray-50 rounded">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold">{log.type}</span>
<span className="text-sm text-gray-500">
{new Date(log.timestamp).toLocaleString('zh-CN')}
</span>
</div>
{log.ip && <p className="text-sm text-gray-600">IP: {log.ip}</p>}
{log.email && <p className="text-sm text-gray-600">邮箱: {log.email}</p>}
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}
Step 3: Test the security monitoring
Run: npm run dev and navigate to /admin/security
Expected: Security dashboard loads and displays data
Step 4: Commit
git add src/app/admin/security/page.tsx src/app/api/admin/security/route.ts
git commit -m "feat: add security monitoring dashboard"
Task 10: Add Environment Variables Documentation
Files:
- Create:
.env.example
Step 1: Create environment variables example
# Email Service
RESEND_API_KEY=your_resend_api_key_here
COMPANY_EMAIL=contact@novalon.cn
# Security
CAPTCHA_SECRET=your_secret_key_for_captcha_hashing
# Rate Limiting (optional, defaults are used if not set)
RATE_LIMIT_IP_MAX_REQUESTS=10
RATE_LIMIT_IP_WINDOW_MINUTES=60
RATE_LIMIT_EMAIL_MAX_REQUESTS=3
RATE_LIMIT_EMAIL_WINDOW_HOURS=24
RATE_LIMIT_GLOBAL_MAX_REQUESTS=50
RATE_LIMIT_GLOBAL_WINDOW_MINUTES=1
# Security Logging
SECURITY_LOG_RETENTION_DAYS=30
SECURITY_ALERT_THRESHOLD=5
Step 2: Update README with security section
Add to README.md:
## Security Features
The contact form includes comprehensive security measures:
- **Enhanced Captcha**: Multi-level complexity (simple/medium/complex) with server-side validation
- **Rate Limiting**: Multi-tier protection (IP, email, global limits)
- **Input Sanitization**: XSS and SQL injection prevention
- **Security Logging**: Comprehensive event tracking and monitoring
- **Suspicious Activity Detection**: Automatic IP blocking for repeated violations
### Security Configuration
Configure security settings in `.env.local`:
```bash
CAPTCHA_SECRET=your-secret-key
RATE_LIMIT_IP_MAX_REQUESTS=10
Monitoring
Access security monitoring at /admin/security to view:
- Recent security events
- Suspicious IP addresses
- Rate limit violations
- Malicious content attempts
**Step 3: Commit**
```bash
git add .env.example README.md
git commit -m "docs: add security configuration documentation"
Task 11: Create Integration Tests
Files:
- Create:
src/lib/security/integration.test.ts
Step 1: Write comprehensive integration tests
import { SecurityMiddleware } from './middleware';
import { generateCaptcha } from './captcha';
describe('Security Integration Tests', () => {
let middleware: SecurityMiddleware;
beforeEach(() => {
middleware = new SecurityMiddleware();
});
test('should handle complete contact form submission flow', async () => {
const captcha = generateCaptcha('medium');
const result = await middleware.checkRequest({
ip: '192.168.1.100',
email: 'integration-test@example.com',
name: 'Integration Test User',
subject: 'Test Subject',
message: 'This is a test message for integration testing.',
captchaHash: captcha.hash,
captchaAnswer: captcha.answer,
captchaTimestamp: captcha.timestamp,
});
expect(result.allowed).toBe(true);
expect(result.errors).toHaveLength(0);
});
test('should block multiple rapid submissions from same IP', async () => {
const ip = '192.168.1.200';
for (let i = 0; i < 10; i++) {
const captcha = generateCaptcha('medium');
await middleware.checkRequest({
ip,
email: `test${i}@example.com`,
name: 'Test User',
subject: 'Test',
message: 'Test message',
captchaHash: captcha.hash,
captchaAnswer: captcha.answer,
captchaTimestamp: captcha.timestamp,
});
}
const captcha = generateCaptcha('medium');
const result = await middleware.checkRequest({
ip,
email: 'blocked@example.com',
name: 'Blocked User',
subject: 'Test',
message: 'This should be blocked',
captchaHash: captcha.hash,
captchaAnswer: captcha.answer,
captchaTimestamp: captcha.timestamp,
});
expect(result.allowed).toBe(false);
expect(result.errors).toContain('Rate limit exceeded for IP');
});
test('should sanitize malicious input while allowing legitimate content', async () => {
const captcha = generateCaptcha('medium');
const maliciousRequest = {
ip: '192.168.1.300',
email: 'sanitization-test@example.com',
name: '<script>alert("xss")</script>Test User',
subject: 'Test Subject',
message: 'Visit http://malicious.com for free money! But also, I need help with order #12345.',
captchaHash: captcha.hash,
captchaAnswer: captcha.answer,
captchaTimestamp: captcha.timestamp,
};
const result = await middleware.checkRequest(maliciousRequest);
expect(result.allowed).toBe(false);
expect(result.errors.some(e => e.includes('malicious'))).toBe(true);
});
test('should track and report suspicious activity', async () => {
const ip = '192.168.1.400';
for (let i = 0; i < 6; i++) {
const captcha = generateCaptcha('medium');
await middleware.checkRequest({
ip,
email: `suspicious${i}@example.com`,
name: 'Suspicious User',
subject: 'Test',
message: 'Test message',
captchaHash: 'invalid-hash',
captchaAnswer: 999,
captchaTimestamp: Date.now(),
});
}
const suspiciousIPs = middleware.getSuspiciousIPs();
expect(suspiciousIPs).toContain(ip);
const logs = middleware.getSecurityLogs(10);
const failedCaptchaLogs = logs.filter(log => log.type.includes('captcha_failed'));
expect(failedCaptchaLogs.length).toBeGreaterThan(0);
});
});
Step 2: Run integration tests
Run: npm test -- src/lib/security/integration.test.ts
Expected: PASS
Step 3: Commit
git add src/lib/security/integration.test.ts
git commit -m "test: add comprehensive security integration tests"
Task 12: Final Testing and Validation
Files:
- Test: All security modules
- Manual: Contact form functionality
Step 1: Run all security tests
Run: npm test -- src/lib/security/
Expected: All tests PASS
Step 2: Run contact form tests
Run: npm test -- src/app/api/contact/route.test.ts
Expected: All tests PASS
Step 3: Manual testing checklist
- Load contact page and verify captcha appears
- Submit valid form with correct captcha - should succeed
- Submit form with incorrect captcha - should fail
- Submit form with XSS content - should be blocked
- Submit 11 forms rapidly from same IP - should be rate limited
- Submit 4 forms from same email - should be rate limited
- Check security monitoring dashboard displays events
- Verify suspicious IPs are detected and logged
Step 4: Performance testing
Run: npm run dev and test form responsiveness
Expected: Form loads quickly, captcha generation is fast
Step 5: Commit final changes
git add .
git commit -m "test: complete security enhancement testing and validation"
Task 13: Create Deployment Documentation
Files:
- Create:
docs/SECURITY_DEPLOYMENT.md
Step 1: Create deployment guide
# Security Enhancement Deployment Guide
## Prerequisites
- Node.js 18+ installed
- Redis server (optional, for production rate limiting)
- Environment variables configured
## Environment Setup
1. Copy environment variables:
```bash
cp .env.example .env.local
- Configure required variables:
RESEND_API_KEY=your_actual_api_key
COMPANY_EMAIL=your@company.com
CAPTCHA_SECRET=generate-a-strong-random-secret
Installation
- Install dependencies:
npm install
- Run tests:
npm test
- Start development server:
npm run dev
Production Deployment
Security Considerations
- CAPTCHA_SECRET: Must be unique and unpredictable
- Rate Limits: Adjust based on your traffic patterns
- Monitoring: Regularly review security logs
- IP Blocking: Consider implementing persistent IP blocking
Monitoring Setup
- Access security dashboard:
/admin/security - Set up alerts for suspicious activity
- Review logs regularly
- Adjust thresholds based on traffic patterns
Performance Optimization
- Consider Redis for distributed rate limiting
- Implement log rotation for long-running deployments
- Cache captcha generation for high-traffic scenarios
- Monitor memory usage of security middleware
Troubleshooting
Captcha Issues
If users report captcha problems:
- Check CAPTCHA_SECRET is set correctly
- Verify captcha expiry time is appropriate
- Review security logs for validation failures
Rate Limit Issues
If legitimate users are blocked:
- Increase rate limits in configuration
- Check for shared IP addresses (office networks)
- Review security logs for false positives
Performance Issues
If form submission is slow:
- Check rate limiter implementation
- Review security logging overhead
- Consider caching strategies
Maintenance
Regular Tasks
- Weekly: Review security logs
- Monthly: Adjust rate limits based on traffic
- Quarterly: Update security patterns and rules
- Annually: Security audit and penetration testing
Updates
Stay updated with:
- New security vulnerabilities
- Improved captcha techniques
- Enhanced rate limiting algorithms
- Updated sanitization patterns
**Step 2: Commit documentation**
```bash
git add docs/SECURITY_DEPLOYMENT.md
git commit -m "docs: add security deployment guide"
Summary
This implementation plan provides a comprehensive security enhancement for the contact form with:
✅ Multi-layer rate limiting (IP, email, global) ✅ Enhanced captcha system (simple/medium/complex) ✅ Input sanitization (XSS, SQL injection prevention) ✅ Security logging (comprehensive event tracking) ✅ Suspicious activity detection (automatic IP blocking) ✅ Security monitoring dashboard (real-time visibility) ✅ Comprehensive testing (unit, integration, manual) ✅ Production-ready (documentation, monitoring, maintenance)
All tasks follow TDD principles, include proper error handling, and maintain backward compatibility where possible.