diff --git a/src/lib/ddos.ts b/src/lib/ddos.ts index 5dfc815c..1b1035d2 100644 --- a/src/lib/ddos.ts +++ b/src/lib/ddos.ts @@ -35,9 +35,29 @@ const HOSTILE_PATH_SEGMENTS = new Set([ "server-status", ".env", ".git", + "cmd.exe", // Windows command execution + "shell", // Generic web shell indicator + "jndi", // Log4j exploit attempts + "tomcat", // Tomcat exploitation attempts + "web-inf", // Java web app directory exposure + "passwd", // Linux password file attempts + "etc", // Linux configuration directory traversal ]); -const HOSTILE_PATH_EXTENSIONS = [".php", ".asp", ".aspx", ".jsp", ".cgi"]; +const HOSTILE_PATH_EXTENSIONS = [ + ".php", + ".asp", + ".aspx", + ".jsp", + ".cgi", + ".env", // Direct .env file access + ".bak", // Backup files + ".sql", // Database dumps + ".zip", // Archives that may contain sensitive data + ".rar", // Archives that may contain sensitive data + ".7z", // Archives that may contain sensitive data + ".txt", // Generic text files that might contain sensitive data +]; /** * Cheap scanner/exploit triage. These paths are never routes in this app, so a diff --git a/src/proxy.ts b/src/proxy.ts index 50055848..5570392f 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { getToken } from "next-auth/jwt"; import { env } from "@/env"; import { buildContentSecurityPolicy, createCspNonce } from "@/lib/csp"; +import { isSuspiciousPath } from "@/lib/ddos"; import { ddosReject, enforceDdosRateLimit } from "@/lib/ddos-guard"; import { isCacheableAssetPath, @@ -19,6 +20,13 @@ const SECURITY_HEADERS: Record = { }; export const proxy = async (req: import("next/server").NextRequest) => { + const pathname = req.nextUrl.pathname; + + // Drop scanner probes immediately before performing any rate-limit or DB operations. + if (isSuspiciousPath(pathname)) { + return ddosReject(403); + } + // Server actions carry an opaque action id in this header. A malformed or // trivially short id (e.g. "x" sent by bots) can never resolve to a real // action; rejecting it here avoids the expensive lookup/render path. @@ -28,15 +36,10 @@ export const proxy = async (req: import("next/server").NextRequest) => { } const decision = await enforceDdosRateLimit(req); - if (decision.outcome === "suspect") { - return ddosReject(403); - } if (decision.outcome === "block") { return ddosReject(429, decision.retryAfterSeconds); } - const pathname = req.nextUrl.pathname; - // Only /admin needs a real session token for the redirect guard; skipping // JWT decoding on every other request keeps the proxy cheap under load. const adminPath = pathname === "/admin" || pathname.startsWith("/admin/");