feat: implement security middleware
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<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 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user