From 60f3f371bbb51c79dc2a3cb7dbc3a69a9d67302c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Tue, 24 Mar 2026 10:53:37 +0800 Subject: [PATCH] feat: implement security middleware --- src/lib/security/middleware.test.ts | 79 +++++++++++++++++ src/lib/security/middleware.ts | 129 ++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/lib/security/middleware.test.ts create mode 100644 src/lib/security/middleware.ts diff --git a/src/lib/security/middleware.test.ts b/src/lib/security/middleware.test.ts new file mode 100644 index 0000000..b94674c --- /dev/null +++ b/src/lib/security/middleware.test.ts @@ -0,0 +1,79 @@ +import { SecurityMiddleware, SecurityCheckResult } from './middleware'; +import { generateCaptcha } from './captcha'; + +describe('Security Middleware', () => { + let middleware: SecurityMiddleware; + + beforeEach(() => { + middleware = new SecurityMiddleware(); + }); + + test('should pass valid request', async () => { + const captcha = generateCaptcha('simple'); + const result = await middleware.checkRequest({ + ip: '192.168.1.1', + email: 'valid@example.com', + captchaHash: captcha.hash, + captchaAnswer: captcha.answer, + captchaTimestamp: captcha.timestamp, + }); + + expect(result.allowed).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + test('should block request with invalid captcha', async () => { + const captcha = generateCaptcha('simple'); + const result = await middleware.checkRequest({ + ip: '192.168.1.2', + email: 'test@example.com', + captchaHash: captcha.hash, + captchaAnswer: 999, + captchaTimestamp: captcha.timestamp, + }); + + 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++) { + const captcha = generateCaptcha('simple'); + await middleware.checkRequest({ + ip, + email: `test${i}@example.com`, + captchaHash: captcha.hash, + captchaAnswer: captcha.answer, + captchaTimestamp: captcha.timestamp, + }); + } + + const captcha = generateCaptcha('simple'); + const result = await middleware.checkRequest({ + ip, + email: 'test@example.com', + 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 block request with malicious content', async () => { + const captcha = generateCaptcha('simple'); + const result = await middleware.checkRequest({ + ip: '192.168.1.4', + email: 'test@example.com', + captchaHash: captcha.hash, + captchaAnswer: captcha.answer, + captchaTimestamp: captcha.timestamp, + message: 'You won a free bitcoin lottery! Act now to claim your prize.', + }); + + expect(result.allowed).toBe(false); + expect(result.errors.some(e => e.includes('malicious'))).toBe(true); + }); +}); diff --git a/src/lib/security/middleware.ts b/src/lib/security/middleware.ts new file mode 100644 index 0000000..8e8c1b4 --- /dev/null +++ b/src/lib/security/middleware.ts @@ -0,0 +1,129 @@ +import { RateLimiter } from './rate-limiter'; +import { validateCaptcha } from './captcha'; +import { sanitizeFormData, detectMaliciousContent } 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 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); + } +}