| /** | |
| * Security Headers Middleware | |
| * | |
| * Implements security headers following OWASP best practices. | |
| * Provides defense-in-depth security controls. | |
| * | |
| * Security Headers: | |
| * - Content-Security-Policy (CSP): Prevents XSS attacks | |
| * - X-Frame-Options: Prevents clickjacking | |
| * - X-Content-Type-Options: Prevents MIME type sniffing | |
| * - Referrer-Policy: Controls referrer information leakage | |
| * - Permissions-Policy: Restricts browser features | |
| * - Strict-Transport-Security: Enforces HTTPS (production only) | |
| * | |
| * OWASP Compliance: | |
| * - Addresses A05:2021 (Security Misconfiguration) | |
| * - Implements "Defense in Depth" principle | |
| */ | |
| import { NextResponse } from 'next/server' | |
| /** | |
| * CSP: intentionally no `upgrade-insecure-requests`. | |
| * That directive breaks http://localhost when NODE_ENV=production (`next start` after build): | |
| * the browser rewrites /_next/static/... to https://localhost/... which has no TLS, so CSS/JS fail and the app is unstyled. | |
| * Real HTTPS deployments still get HSTS below when NODE_ENV=production. | |
| * | |
| * Google Maps JS API requires frames + broader connect/img/script hosts than `maps.googleapis.com` alone. | |
| * @see https://developers.google.com/maps/documentation/javascript/content-security-policy | |
| */ | |
| function contentSecurityPolicy(): string { | |
| return [ | |
| "default-src 'self'", | |
| // Next.js needs unsafe-inline / unsafe-eval (dev); Maps needs googleapis + gstatic + blob workers | |
| "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.googleapis.com https://*.gstatic.com *.google.com https://*.ggpht.com *.googleusercontent.com blob:", | |
| "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", | |
| "img-src 'self' data: blob: https://*.googleapis.com https://*.gstatic.com *.google.com *.googleusercontent.com", | |
| "connect-src 'self' https://*.googleapis.com *.google.com https://*.gstatic.com data: blob:", | |
| "font-src 'self' data: https://fonts.gstatic.com", | |
| // was frame-src 'none' — that blocks Maps’ iframes and leads to a blank map / broken UI | |
| "frame-src 'self' *.google.com https://*.googleapis.com https://*.gstatic.com", | |
| "worker-src 'self' blob:", | |
| "object-src 'none'", | |
| "base-uri 'self'", | |
| "form-action 'self'", | |
| "frame-ancestors 'none'", | |
| ].join('; ') | |
| } | |
| /** | |
| * Security headers configuration | |
| * | |
| * Note: CSP policy may need adjustment based on your specific needs | |
| * (e.g., if you use external CDNs, analytics, etc.) | |
| */ | |
| const SECURITY_HEADERS = { | |
| 'Content-Security-Policy': contentSecurityPolicy(), | |
| /** | |
| * X-Frame-Options | |
| * Prevents clickjacking attacks by preventing page embedding | |
| */ | |
| 'X-Frame-Options': 'DENY', | |
| /** | |
| * X-Content-Type-Options | |
| * Prevents MIME type sniffing attacks | |
| */ | |
| 'X-Content-Type-Options': 'nosniff', | |
| /** | |
| * Referrer-Policy | |
| * Controls referrer information to prevent data leakage | |
| * 'strict-origin-when-cross-origin' sends full URL for same-origin, origin only for cross-origin | |
| */ | |
| 'Referrer-Policy': 'strict-origin-when-cross-origin', | |
| /** | |
| * Permissions-Policy (formerly Feature-Policy) | |
| * Restricts browser features to prevent abuse | |
| * | |
| * Disabled features: | |
| * - camera, microphone: Prevent unauthorized access | |
| * - geolocation: Only allow when explicitly requested | |
| * - payment: Disable payment APIs | |
| * - usb: Disable USB access | |
| */ | |
| 'Permissions-Policy': [ | |
| // Allow same-origin camera for mobile "take photo" on leaf uploads | |
| 'camera=(self)', | |
| 'microphone=()', | |
| 'geolocation=(self)', | |
| 'fullscreen=(self)', | |
| 'payment=()', | |
| 'usb=()', | |
| ].join(', '), | |
| /** | |
| * Strict-Transport-Security (HSTS) | |
| * Enforces HTTPS connections (production only) | |
| * | |
| * Note: Only set in production to avoid issues in development | |
| */ | |
| ...(process.env.NODE_ENV === 'production' && { | |
| 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload', | |
| }), | |
| } | |
| /** | |
| * Add security headers to response | |
| * | |
| * @param response - Next.js response object | |
| * @returns Response with security headers added | |
| * | |
| * Usage: | |
| * ```typescript | |
| * const response = NextResponse.json(data) | |
| * return addSecurityHeaders(response) | |
| * ``` | |
| */ | |
| export function addSecurityHeaders(response: NextResponse): NextResponse { | |
| // Add all security headers to response | |
| Object.entries(SECURITY_HEADERS).forEach(([key, value]) => { | |
| response.headers.set(key, value) | |
| }) | |
| return response | |
| } | |
| /** | |
| * Create a secure response with security headers | |
| * | |
| * @param body - Response body (JSON object) | |
| * @param status - HTTP status code (default: 200) | |
| * @returns Secure NextResponse with headers | |
| */ | |
| export function createSecureResponse( | |
| body: any, | |
| status: number = 200 | |
| ): NextResponse { | |
| const response = NextResponse.json(body, { status }) | |
| return addSecurityHeaders(response) | |
| } | |