diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index ec21198..c11214f 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -1,10 +1,19 @@ import { NextIntlClientProvider } from "next-intl"; import { setRequestLocale } from "next-intl/server"; import React from "react"; +import { notFound } from "next/navigation"; import ConsentBanner from "../components/ConsentBanner"; 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 const baseMessages = (await import(`../../messages/${locale}.json`)).default; @@ -13,6 +22,11 @@ async function loadEnhancedMessages(locale: string) { 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({ children, params, @@ -21,6 +35,12 @@ export default async function LocaleLayout({ params: Promise<{ locale: string }>; }) { 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. setRequestLocale(locale); // Load messages explicitly by route locale to avoid falling back to the wrong diff --git a/middleware.ts b/middleware.ts index e7aed7d..7fe0a11 100644 --- a/middleware.ts +++ b/middleware.ts @@ -4,6 +4,29 @@ import type { NextRequest } from "next/server"; const SUPPORTED_LOCALES = ["en", "de"] as const; 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 { if (!acceptLanguage) return "en"; const lower = acceptLanguage.toLowerCase(); @@ -20,6 +43,11 @@ function hasLocalePrefix(pathname: string): boolean { export function middleware(request: NextRequest) { 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), // redirect to the non-prefixed asset path. if (hasLocalePrefix(pathname)) { diff --git a/nginx.production.conf b/nginx.production.conf index 0dbd616..a8df9a5 100644 --- a/nginx.production.conf +++ b/nginx.production.conf @@ -82,6 +82,27 @@ http { # 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;"; + # 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 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y;