202 lines
4.8 KiB
TypeScript
202 lines
4.8 KiB
TypeScript
import { type NextRequest, NextResponse } from 'next/server';
|
|
|
|
// In-memory stores for tracking IPs (use Redis in production)
|
|
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
|
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
|
|
const bannedIPs = new Set<string>();
|
|
|
|
// Ban Arctic Wolf Explicitly
|
|
bannedIPs.add('::ffff:10.0.1.49');
|
|
|
|
// Suspicious patterns that indicate malicious activity
|
|
const MALICIOUS_PATTERNS = [
|
|
// Your existing patterns
|
|
/web-inf/i,
|
|
/\.jsp/i,
|
|
/\.php/i,
|
|
/puttest/i,
|
|
/WEB-INF/i,
|
|
/\.xml$/i,
|
|
/perl/i,
|
|
/xampp/i,
|
|
/phpwebgallery/i,
|
|
/FileManager/i,
|
|
/standalonemanager/i,
|
|
/h2console/i,
|
|
/WebAdmin/i,
|
|
/login_form\.php/i,
|
|
/%2e/i,
|
|
/%u002e/i,
|
|
/\.%00/i,
|
|
/\.\./,
|
|
/lcgi/i,
|
|
|
|
// New patterns from your logs
|
|
/\/appliance\//i,
|
|
/bomgar/i,
|
|
/netburner-logo/i,
|
|
/\/ui\/images\//i,
|
|
/logon_merge/i,
|
|
/logon_t\.gif/i,
|
|
/login_top\.gif/i,
|
|
/theme1\/images/i,
|
|
/\.well-known\/acme-challenge\/.*\.jpg$/i,
|
|
/\.well-known\/pki-validation\/.*\.jpg$/i,
|
|
|
|
// Path traversal and system file access patterns
|
|
/\/etc\/passwd/i,
|
|
/\/etc%2fpasswd/i,
|
|
/\/etc%5cpasswd/i,
|
|
/\/\/+etc/i,
|
|
/\\\\+.*etc/i,
|
|
/%2f%2f/i,
|
|
/%5c%5c/i,
|
|
/\/\/+/,
|
|
/\\\\+/,
|
|
/%00/i,
|
|
/%23/i,
|
|
|
|
// Encoded path traversal attempts
|
|
/%2e%2e/i,
|
|
/%252e/i,
|
|
/%c0%ae/i,
|
|
/%c1%9c/i,
|
|
];
|
|
|
|
// Suspicious HTTP methods
|
|
const SUSPICIOUS_METHODS = ['TRACE', 'PUT', 'DELETE', 'PATCH'];
|
|
|
|
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
|
const MAX_ATTEMPTS = 10; // Max suspicious requests per window
|
|
const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
|
|
|
|
// 404 rate limiting settings
|
|
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
|
|
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
|
|
|
|
const getClientIP = (request: NextRequest): string => {
|
|
const forwarded = request.headers.get('x-forwarded-for');
|
|
const realIP = request.headers.get('x-real-ip');
|
|
const cfConnectingIP = request.headers.get('cf-connecting-ip');
|
|
|
|
if (forwarded) return (forwarded.split(',')[0] ?? '').trim();
|
|
if (realIP) return realIP;
|
|
if (cfConnectingIP) return cfConnectingIP;
|
|
return request.headers.get('host') ?? 'unknown';
|
|
};
|
|
|
|
const isPathSuspicious = (pathname: string): boolean => {
|
|
return MALICIOUS_PATTERNS.some((pattern) => pattern.test(pathname));
|
|
};
|
|
|
|
const isMethodSuspicious = (method: string): boolean => {
|
|
return SUSPICIOUS_METHODS.includes(method);
|
|
};
|
|
|
|
const updateIPAttempts = (ip: string): boolean => {
|
|
const now = Date.now();
|
|
const attempts = ipAttempts.get(ip);
|
|
|
|
if (!attempts || now - attempts.lastAttempt > RATE_LIMIT_WINDOW) {
|
|
ipAttempts.set(ip, { count: 1, lastAttempt: now });
|
|
return false;
|
|
}
|
|
|
|
attempts.count++;
|
|
attempts.lastAttempt = now;
|
|
|
|
if (attempts.count > MAX_ATTEMPTS) {
|
|
bannedIPs.add(ip);
|
|
ipAttempts.delete(ip);
|
|
|
|
setTimeout(() => {
|
|
bannedIPs.delete(ip);
|
|
}, BAN_DURATION);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const update404Attempts = (ip: string): boolean => {
|
|
const now = Date.now();
|
|
const attempts = ip404Attempts.get(ip);
|
|
|
|
if (!attempts || now - attempts.lastAttempt > RATE_404_WINDOW) {
|
|
ip404Attempts.set(ip, { count: 1, lastAttempt: now });
|
|
return false;
|
|
}
|
|
|
|
attempts.count++;
|
|
attempts.lastAttempt = now;
|
|
|
|
if (attempts.count > MAX_404_ATTEMPTS) {
|
|
bannedIPs.add(ip);
|
|
ip404Attempts.delete(ip);
|
|
|
|
console.log(
|
|
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
|
|
);
|
|
|
|
setTimeout(() => {
|
|
bannedIPs.delete(ip);
|
|
}, BAN_DURATION);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
|
|
const { pathname } = request.nextUrl;
|
|
const method = request.method;
|
|
const ip = getClientIP(request);
|
|
|
|
// Check if IP is already banned
|
|
if (bannedIPs.has(ip)) {
|
|
return new NextResponse('Access denied.', { status: 403 });
|
|
}
|
|
|
|
const isSuspiciousPath = isPathSuspicious(pathname);
|
|
const isSuspiciousMethod = isMethodSuspicious(method);
|
|
|
|
// Handle suspicious activity
|
|
if (isSuspiciousPath || isSuspiciousMethod) {
|
|
const shouldBan = updateIPAttempts(ip);
|
|
|
|
if (shouldBan) {
|
|
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
|
|
return new NextResponse('Access denied - IP banned. Please fuck off.', {
|
|
status: 403,
|
|
});
|
|
}
|
|
|
|
return new NextResponse('Not Found', { status: 404 });
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
// Call this function when you detect a 404 response
|
|
export const handle404Response = (
|
|
request: NextRequest,
|
|
): NextResponse | null => {
|
|
const ip = getClientIP(request);
|
|
|
|
if (bannedIPs.has(ip)) {
|
|
return new NextResponse('Access denied.', { status: 403 });
|
|
}
|
|
|
|
const shouldBan = update404Attempts(ip);
|
|
|
|
if (shouldBan) {
|
|
return new NextResponse('Access denied - IP banned for excessive 404s.', {
|
|
status: 403,
|
|
});
|
|
}
|
|
|
|
return null;
|
|
};
|