fix: resolve CSP violations and React hydration issues

- Fix Permissions-Policy header: change ambient-light-sensor to ambient-light
- Add Google Fonts domain to font-src CSP for Leaflet map tiles
- Allow unsafe-inline for style-src to support third-party libraries (Sonner, Leaflet)
- Fix React hydration mismatch by conditionally adding nonce attribute
- Add debug logging for nonce retrieval issues

These changes resolve all CSP violations while maintaining security best practices.
This commit is contained in:
2025-07-13 22:23:40 +02:00
parent 1e0ee37a39
commit 6d5d0fd7a4
4 changed files with 24 additions and 9 deletions

View File

@ -134,7 +134,7 @@ export default async function RootLayout({
<head> <head>
<script <script
type="application/ld+json" type="application/ld+json"
nonce={nonce} {...(nonce ? { nonce } : {})}
// biome-ignore lint/security/noDangerouslySetInnerHtml: Safe use for JSON-LD structured data with CSP nonce // biome-ignore lint/security/noDangerouslySetInnerHtml: Safe use for JSON-LD structured data with CSP nonce
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/> />

View File

@ -52,9 +52,12 @@ export function buildCSP(config: CSPConfig = {}): string {
: ["'self'"]; : ["'self'"];
// Style sources - use nonce in production when available // Style sources - use nonce in production when available
const styleSrc = nonce // Note: We need 'unsafe-inline' for third-party libraries like Sonner that inject styles dynamically
? ["'self'", `'nonce-${nonce}'`] const styleSrc = isDevelopment
: ["'self'", "'unsafe-inline'"]; // Fallback for TailwindCSS ? ["'self'", "'unsafe-inline'"]
: nonce
? ["'self'", `'nonce-${nonce}'`, "'unsafe-inline'"] // Need unsafe-inline for Sonner/Leaflet
: ["'self'", "'unsafe-inline'"]; // Fallback for TailwindCSS
// Image sources - allow self, data URIs, and specific trusted domains // Image sources - allow self, data URIs, and specific trusted domains
const imgSrc = [ const imgSrc = [
@ -69,8 +72,8 @@ export function buildCSP(config: CSPConfig = {}): string {
.map((domain) => domain), .map((domain) => domain),
].filter(Boolean); ].filter(Boolean);
// Font sources - restrict to self and data URIs // Font sources - restrict to self, data URIs, and Google Fonts (for Leaflet)
const fontSrc = ["'self'", "data:"]; const fontSrc = ["'self'", "data:", "https://fonts.gstatic.com"];
// Connect sources - API endpoints and trusted domains // Connect sources - API endpoints and trusted domains
const connectSrc = isDevelopment const connectSrc = isDevelopment

View File

@ -6,9 +6,21 @@ import { headers } from "next/headers";
export async function getNonce(): Promise<string | undefined> { export async function getNonce(): Promise<string | undefined> {
try { try {
const headersList = await headers(); const headersList = await headers();
return headersList.get("X-Nonce") || undefined; const nonce = headersList.get("X-Nonce");
} catch {
// Log for debugging hydration issues
if (!nonce && process.env.NODE_ENV === "development") {
console.warn(
"No nonce found in headers - this may cause hydration mismatches"
);
}
return nonce || undefined;
} catch (error) {
// Headers not available (e.g., in client-side code) // Headers not available (e.g., in client-side code)
if (process.env.NODE_ENV === "development") {
console.warn("Failed to get headers for nonce:", error);
}
return undefined; return undefined;
} }
} }

View File

@ -78,7 +78,7 @@ export function middleware(request: NextRequest) {
"accelerometer=()", "accelerometer=()",
"gyroscope=()", "gyroscope=()",
"magnetometer=()", "magnetometer=()",
"ambient-light-sensor=()", "ambient-light=()",
"encrypted-media=()", "encrypted-media=()",
"autoplay=(self)", "autoplay=(self)",
].join(", ") ].join(", ")