140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
import { validateRequestIP, getClientIP } from './lib/security/ipWhitelist';
|
|
import {
|
|
PROTECTED_PATHS,
|
|
BLOCKED_USER_AGENTS,
|
|
BLOCKED_IPS,
|
|
SUSPICIOUS_PATTERNS,
|
|
} from './lib/security/config';
|
|
|
|
export function middleware(request: NextRequest) {
|
|
// Safely get pathname and search
|
|
let pathname = '/';
|
|
let search = '';
|
|
try {
|
|
if (request?.nextUrl) {
|
|
pathname = request.nextUrl.pathname || '/';
|
|
search = request.nextUrl.search || '';
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error getting URL from request:', e);
|
|
}
|
|
|
|
// Safely get headers
|
|
let headersObj: Record<string, string> = {};
|
|
try {
|
|
if (request?.headers && typeof request.headers.entries === 'function') {
|
|
try {
|
|
headersObj = Object.fromEntries(request.headers.entries());
|
|
} catch (entriesError) {
|
|
// If entries() fails, try to manually extract headers
|
|
console.warn('Error getting header entries, trying alternative method:', entriesError);
|
|
headersObj = {};
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// If headers can't be converted, use empty object
|
|
console.warn('Error converting headers:', e);
|
|
}
|
|
|
|
// Safely get user agent
|
|
let userAgent = '';
|
|
try {
|
|
if (request?.headers && typeof request.headers.get === 'function') {
|
|
const ua = request.headers.get('user-agent');
|
|
userAgent = ua || '';
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error getting user agent:', e);
|
|
}
|
|
|
|
const ip = getClientIP(headersObj);
|
|
|
|
// Security checks
|
|
const securityChecks: Array<{ check: () => boolean; action: () => NextResponse }> = [];
|
|
|
|
// 1. Block malicious user agents
|
|
securityChecks.push({
|
|
check: () => BLOCKED_USER_AGENTS.some(blocked => userAgent.toLowerCase().includes(blocked.toLowerCase())),
|
|
action: () => {
|
|
console.warn(`[SECURITY] Blocked malicious user agent: ${userAgent} from IP: ${ip}`);
|
|
return new NextResponse('Forbidden', { status: 403 });
|
|
},
|
|
});
|
|
|
|
// 2. Block known malicious IPs
|
|
securityChecks.push({
|
|
check: () => BLOCKED_IPS.includes(ip),
|
|
action: () => {
|
|
console.warn(`[SECURITY] Blocked known malicious IP: ${ip}`);
|
|
return new NextResponse('Forbidden', { status: 403 });
|
|
},
|
|
});
|
|
|
|
// 3. IP whitelist check for protected paths
|
|
if (PROTECTED_PATHS.some(path => pathname.startsWith(path))) {
|
|
const validation = validateRequestIP(headersObj);
|
|
if (!validation.allowed) {
|
|
console.warn(`[SECURITY] Blocked non-whitelisted IP: ${ip} from accessing: ${pathname}`);
|
|
return new NextResponse(
|
|
JSON.stringify({
|
|
error: 'Forbidden',
|
|
message: 'Access denied from this IP address',
|
|
}),
|
|
{
|
|
status: 403,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
// 4. Block suspicious query parameters (potential XSS/SQL injection attempts)
|
|
const fullUrl = pathname + search;
|
|
if (SUSPICIOUS_PATTERNS.some(pattern => pattern.test(fullUrl))) {
|
|
console.warn(`[SECURITY] Blocked suspicious request pattern from IP: ${ip} - URL: ${fullUrl}`);
|
|
return new NextResponse('Bad Request', { status: 400 });
|
|
}
|
|
|
|
// 5. Rate limiting headers (basic implementation)
|
|
// In production, use a proper rate limiting service
|
|
const response = NextResponse.next();
|
|
|
|
// Add security headers (safely check if headers exist)
|
|
try {
|
|
if (response?.headers && typeof response.headers.set === 'function') {
|
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
|
response.headers.set('X-XSS-Protection', '1; mode=block');
|
|
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error setting response headers:', e);
|
|
}
|
|
|
|
// Execute security checks
|
|
for (const { check, action } of securityChecks) {
|
|
if (check()) {
|
|
return action();
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
// Configure which routes the middleware runs on
|
|
export const config = {
|
|
matcher: [
|
|
/*
|
|
* Match all request paths except:
|
|
* - _next/static (static files)
|
|
* - _next/image (image optimization files)
|
|
* - favicon.ico (favicon file)
|
|
* - public files (images, etc.)
|
|
*/
|
|
'/((?!_next/static|_next/image|favicon.ico|images|icons|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff|woff2|ttf|eot)).*)',
|
|
],
|
|
};
|
|
|