feat(security): drop scanner probes and hostile paths early in proxy
CI / check (push) Successful in 30s
CI / tests-unit (push) Successful in 1m55s
CI / tests-integration (push) Successful in 1m59s
CI / tests-imaging-browsers (push) Successful in 3m38s
CI / tests-ui (push) Successful in 5m6s
CI / preflight (push) Skipped
CI / tests-quality (push) Successful in 4m0s
CI / deploy (push) Failing after 4m7s
CI / check (push) Successful in 30s
CI / tests-unit (push) Successful in 1m55s
CI / tests-integration (push) Successful in 1m59s
CI / tests-imaging-browsers (push) Successful in 3m38s
CI / tests-ui (push) Successful in 5m6s
CI / preflight (push) Skipped
CI / tests-quality (push) Successful in 4m0s
CI / deploy (push) Failing after 4m7s
This commit is contained in:
1 parent
a878d831aa
commit
fb724a35cc
2 files changed
+29
-6
No files matched your search
+21
-1
@@ -35,9 +35,29 @@ const HOSTILE_PATH_SEGMENTS = new Set([
|
|||||||
"server-status",
|
"server-status",
|
||||||
".env",
|
".env",
|
||||||
".git",
|
".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
|
* Cheap scanner/exploit triage. These paths are never routes in this app, so a
|
||||||
|
|||||||
+8
-5
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { buildContentSecurityPolicy, createCspNonce } from "@/lib/csp";
|
import { buildContentSecurityPolicy, createCspNonce } from "@/lib/csp";
|
||||||
|
import { isSuspiciousPath } from "@/lib/ddos";
|
||||||
import { ddosReject, enforceDdosRateLimit } from "@/lib/ddos-guard";
|
import { ddosReject, enforceDdosRateLimit } from "@/lib/ddos-guard";
|
||||||
import {
|
import {
|
||||||
isCacheableAssetPath,
|
isCacheableAssetPath,
|
||||||
@@ -19,6 +20,13 @@ const SECURITY_HEADERS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const proxy = async (req: import("next/server").NextRequest) => {
|
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
|
// 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
|
// 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.
|
// 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);
|
const decision = await enforceDdosRateLimit(req);
|
||||||
if (decision.outcome === "suspect") {
|
|
||||||
return ddosReject(403);
|
|
||||||
}
|
|
||||||
if (decision.outcome === "block") {
|
if (decision.outcome === "block") {
|
||||||
return ddosReject(429, decision.retryAfterSeconds);
|
return ddosReject(429, decision.retryAfterSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathname = req.nextUrl.pathname;
|
|
||||||
|
|
||||||
// Only /admin needs a real session token for the redirect guard; skipping
|
// Only /admin needs a real session token for the redirect guard; skipping
|
||||||
// JWT decoding on every other request keeps the proxy cheap under load.
|
// JWT decoding on every other request keeps the proxy cheap under load.
|
||||||
const adminPath = pathname === "/admin" || pathname.startsWith("/admin/");
|
const adminPath = pathname === "/admin" || pathname.startsWith("/admin/");
|
||||||
|
|||||||
Reference in new issue
Block a user