498bb3a3c8
- 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
2249 lines
59 KiB
Markdown
2249 lines
59 KiB
Markdown
# 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<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**
|
||
|
||
```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 = '<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**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```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<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**
|
||
|
||
```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: '<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**
|
||
|
||
```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 = `
|
||
<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**
|
||
|
||
```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<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**
|
||
|
||
```typescript
|
||
'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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```typescript
|
||
'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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```markdown
|
||
## 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**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```markdown
|
||
# 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
|
||
```
|
||
|
||
2. Configure required variables:
|
||
```bash
|
||
RESEND_API_KEY=your_actual_api_key
|
||
COMPANY_EMAIL=your@company.com
|
||
CAPTCHA_SECRET=generate-a-strong-random-secret
|
||
```
|
||
|
||
## Installation
|
||
|
||
1. Install dependencies:
|
||
```bash
|
||
npm install
|
||
```
|
||
|
||
2. Run tests:
|
||
```bash
|
||
npm test
|
||
```
|
||
|
||
3. Start development server:
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
## Production Deployment
|
||
|
||
### Security Considerations
|
||
|
||
1. **CAPTCHA_SECRET**: Must be unique and unpredictable
|
||
2. **Rate Limits**: Adjust based on your traffic patterns
|
||
3. **Monitoring**: Regularly review security logs
|
||
4. **IP Blocking**: Consider implementing persistent IP blocking
|
||
|
||
### Monitoring Setup
|
||
|
||
1. Access security dashboard: `/admin/security`
|
||
2. Set up alerts for suspicious activity
|
||
3. Review logs regularly
|
||
4. Adjust thresholds based on traffic patterns
|
||
|
||
### Performance Optimization
|
||
|
||
1. Consider Redis for distributed rate limiting
|
||
2. Implement log rotation for long-running deployments
|
||
3. Cache captcha generation for high-traffic scenarios
|
||
4. 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.
|