fix: Security vulnerability - block malicious file requests
All checks were successful
Production Deployment (Zero Downtime) / deploy-production (push) Successful in 14m30s
All checks were successful
Production Deployment (Zero Downtime) / deploy-production (push) Successful in 14m30s
This commit is contained in:
@@ -1,10 +1,19 @@
|
|||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { setRequestLocale } from "next-intl/server";
|
import { setRequestLocale } from "next-intl/server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
import ConsentBanner from "../components/ConsentBanner";
|
import ConsentBanner from "../components/ConsentBanner";
|
||||||
import { getLocalizedMessage } from "@/lib/i18n-loader";
|
import { getLocalizedMessage } from "@/lib/i18n-loader";
|
||||||
|
|
||||||
async function loadEnhancedMessages(locale: string) {
|
// Supported locales - must match middleware.ts
|
||||||
|
const SUPPORTED_LOCALES = ["en", "de"] as const;
|
||||||
|
type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||||
|
|
||||||
|
function isValidLocale(locale: string): locale is SupportedLocale {
|
||||||
|
return SUPPORTED_LOCALES.includes(locale as SupportedLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEnhancedMessages(locale: SupportedLocale) {
|
||||||
// Lade basis JSON Messages
|
// Lade basis JSON Messages
|
||||||
const baseMessages = (await import(`../../messages/${locale}.json`)).default;
|
const baseMessages = (await import(`../../messages/${locale}.json`)).default;
|
||||||
|
|
||||||
@@ -13,6 +22,11 @@ async function loadEnhancedMessages(locale: string) {
|
|||||||
return baseMessages;
|
return baseMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define valid static params to prevent malicious path traversal
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return SUPPORTED_LOCALES.map((locale) => ({ locale }));
|
||||||
|
}
|
||||||
|
|
||||||
export default async function LocaleLayout({
|
export default async function LocaleLayout({
|
||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
@@ -21,6 +35,12 @@ export default async function LocaleLayout({
|
|||||||
params: Promise<{ locale: string }>;
|
params: Promise<{ locale: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
|
||||||
|
// Security: Validate locale to prevent malicious imports
|
||||||
|
if (!isValidLocale(locale)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure next-intl actually uses the route segment locale for this request.
|
// Ensure next-intl actually uses the route segment locale for this request.
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
// Load messages explicitly by route locale to avoid falling back to the wrong
|
// Load messages explicitly by route locale to avoid falling back to the wrong
|
||||||
|
|||||||
@@ -4,6 +4,29 @@ import type { NextRequest } from "next/server";
|
|||||||
const SUPPORTED_LOCALES = ["en", "de"] as const;
|
const SUPPORTED_LOCALES = ["en", "de"] as const;
|
||||||
type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||||
|
|
||||||
|
// Security: Block common malicious file patterns
|
||||||
|
const BLOCKED_PATTERNS = [
|
||||||
|
/\.php$/i,
|
||||||
|
/\.asp$/i,
|
||||||
|
/\.aspx$/i,
|
||||||
|
/\.jsp$/i,
|
||||||
|
/\.cgi$/i,
|
||||||
|
/\.env$/i,
|
||||||
|
/\.sql$/i,
|
||||||
|
/\.gz$/i,
|
||||||
|
/\.tar$/i,
|
||||||
|
/\.zip$/i,
|
||||||
|
/\.rar$/i,
|
||||||
|
/\.bash_history$/i,
|
||||||
|
/ftpsync\.settings$/i,
|
||||||
|
/__MACOSX/i,
|
||||||
|
/\.well-known\.zip$/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
function isBlockedPath(pathname: string): boolean {
|
||||||
|
return BLOCKED_PATTERNS.some((pattern) => pattern.test(pathname));
|
||||||
|
}
|
||||||
|
|
||||||
function pickLocaleFromHeader(acceptLanguage: string | null): SupportedLocale {
|
function pickLocaleFromHeader(acceptLanguage: string | null): SupportedLocale {
|
||||||
if (!acceptLanguage) return "en";
|
if (!acceptLanguage) return "en";
|
||||||
const lower = acceptLanguage.toLowerCase();
|
const lower = acceptLanguage.toLowerCase();
|
||||||
@@ -20,6 +43,11 @@ function hasLocalePrefix(pathname: string): boolean {
|
|||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
const { pathname, search } = request.nextUrl;
|
const { pathname, search } = request.nextUrl;
|
||||||
|
|
||||||
|
// Security: Block malicious/suspicious requests immediately
|
||||||
|
if (isBlockedPath(pathname)) {
|
||||||
|
return new NextResponse(null, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
// If a locale-prefixed request hits a public asset path (e.g. /de/images/me.jpg),
|
// If a locale-prefixed request hits a public asset path (e.g. /de/images/me.jpg),
|
||||||
// redirect to the non-prefixed asset path.
|
// redirect to the non-prefixed asset path.
|
||||||
if (hasLocalePrefix(pathname)) {
|
if (hasLocalePrefix(pathname)) {
|
||||||
|
|||||||
@@ -82,6 +82,27 @@ http {
|
|||||||
# Avoid `unsafe-eval` in production CSP
|
# Avoid `unsafe-eval` in production CSP
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://analytics.dk0.dev; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://analytics.dk0.dev;";
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://analytics.dk0.dev; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://analytics.dk0.dev;";
|
||||||
|
|
||||||
|
# Block common malicious file extensions and paths
|
||||||
|
location ~* \.(php|asp|aspx|jsp|cgi|sh|bat|cmd|exe|dll)$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Block access to sensitive files
|
||||||
|
location ~* (\.env|\.sql|\.tar|\.gz|\.zip|\.rar|\.bash_history|ftpsync\.settings|__MACOSX) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Block access to .well-known if not explicitly needed
|
||||||
|
location ~ /\.well-known(?!\/acme-challenge) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Block access to hidden files and directories
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
# Cache static assets
|
# Cache static assets
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
|
|||||||
Reference in New Issue
Block a user