# 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** ```typescript 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** ```typescript 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** ```bash 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** ```typescript 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** ```typescript 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** ```bash 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** ```typescript 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** ```typescript 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(); private emailRequests = new Map(); private globalRequests: RequestRecord = { count: 0, windowStart: Date.now() }; async checkIP(ip: string): Promise { 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 { 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 { 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 { return this.limiter.checkIP(ip); } async checkEmail(email: string): Promise { return this.limiter.checkEmail(email); } async checkGlobal(): Promise { 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** ```bash 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** ```typescript import { sanitizeInput, sanitizeFormData, detectMaliciousContent } from '../sanitizer'; describe('Input Sanitization', () => { test('should remove XSS attempts', () => { const malicious = 'Hello'; const sanitized = sanitizeInput(malicious); expect(sanitized).not.toContain('John', email: 'test@example.com', message: 'Click http://evil.com', }; const sanitized = sanitizeFormData(formData); expect(sanitized.name).not.toContain('', }); 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** ```typescript 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 { 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** ```bash 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** ```typescript 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: '', 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** ```typescript 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 = `

新的客户咨询

姓名: ${sanitizedData.name}
邮箱: ${sanitizedData.email}
${sanitizedData.phone ? `
电话: ${sanitizedData.phone}
` : ''}
主题: ${sanitizedData.subject}
留言: ${sanitizedData.message}
`; const result = await resend.emails.send({ from: '睿新致远官网 ', 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** ```bash 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** ```typescript 'use client'; import { useState, useEffect } from 'react'; import { submitContactForm } from '../actions'; export default function ContactPage() { const [captcha, setCaptcha] = useState(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 (

联系我们

setFormData({ ...formData, name: e.target.value })} className="w-full p-3 border rounded" required />
setFormData({ ...formData, email: e.target.value })} className="w-full p-3 border rounded" required />
setFormData({ ...formData, phone: e.target.value })} className="w-full p-3 border rounded" />
setFormData({ ...formData, subject: e.target.value })} className="w-full p-3 border rounded" required />