Files
obc-terrassement/middleware.ts
Claude 3843595e18 security: corriger les vraies vulnérabilités détectées par l'audit
1. MIME spoofing (upload) — app/api/admin/upload/route.ts
   - Ajout de la validation par magic bytes : lit les premiers octets du
     fichier et vérifie la signature binaire réelle (JPEG FF D8 FF,
     PNG 89 50 4E 47, GIF 47 49 46 38, WebP RIFF+WEBP, AVIF ftyp box)
   - Extension dérivée exclusivement du MIME validé côté serveur
     (MIME_TO_EXT), jamais du nom de fichier fourni par le client
   - Un fichier .exe renommé en .jpg est désormais rejeté

2. Générateur de mot de passe non-cryptographique — stripe/webhook/route.ts
   - Remplace Math.random() (non-déterministe mais prévisible) par
     crypto.getRandomValues() (CSPRNG, conforme Web Crypto API)

3. Headers HTTP de sécurité manquants — middleware.ts (nouveau)
   - X-Content-Type-Options: nosniff (anti MIME-sniffing navigateur)
   - X-Frame-Options: SAMEORIGIN (anti clickjacking)
   - Referrer-Policy: strict-origin-when-cross-origin
   - Permissions-Policy: désactive camera, micro, geolocation
   - Content-Security-Policy: whitelist stricte par type de ressource
     (scripts, styles, images Unsplash/Supabase/Sanity, connect Supabase/Stripe,
     frames Stripe uniquement, object-src none, form-action self)

https://claude.ai/code/session_01PzA98VhLMmsHpzs7gnLHGs
2026-02-21 09:01:21 +00:00

80 lines
3.7 KiB
TypeScript

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// ── X-Content-Type-Options ─────────────────────────────────────────────────
// Empêche le navigateur de "deviner" le Content-Type (MIME-sniffing).
// Sans ce header, un fichier .jpg contenant du HTML pourrait être exécuté.
response.headers.set("X-Content-Type-Options", "nosniff");
// ── X-Frame-Options ────────────────────────────────────────────────────────
// Bloque l'intégration de la page dans une iframe externe (clickjacking).
response.headers.set("X-Frame-Options", "SAMEORIGIN");
// ── Referrer-Policy ────────────────────────────────────────────────────────
// Limite les infos envoyées dans le header Referer aux requêtes cross-origin.
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
// ── Permissions-Policy ─────────────────────────────────────────────────────
// Désactive les APIs navigateur sensibles non utilisées par le site.
response.headers.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=(), payment=(self)"
);
// ── Content-Security-Policy ────────────────────────────────────────────────
// Whitelist explicite des origines autorisées pour chaque type de ressource.
const supabaseHost = process.env.NEXT_PUBLIC_SUPABASE_URL
? new URL(process.env.NEXT_PUBLIC_SUPABASE_URL).host
: "*.supabase.co";
const csp = [
"default-src 'self'",
// Next.js 15 (App Router + RSC) nécessite 'unsafe-inline' et 'unsafe-eval'
// pour le bundle client et l'hydration côté navigateur.
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://m.stripe.com",
// Tailwind CSS + styles inline de composants React
"style-src 'self' 'unsafe-inline'",
// Images : self, data URIs (placeholders SVG), blob (prévisualisations upload),
// Unsplash (images par défaut), Supabase Storage (signed URLs), Sanity CDN
`img-src 'self' data: blob: https://images.unsplash.com https://${supabaseHost} https://cdn.sanity.io`,
// API calls : Supabase (REST + WebSocket realtime), Stripe
`connect-src 'self' https://${supabaseHost} wss://${supabaseHost} https://api.stripe.com https://r.stripe.com`,
// Polices web : uniquement self (pas de Google Fonts)
"font-src 'self'",
// Iframes : uniquement Stripe (paiement sécurisé)
"frame-src https://js.stripe.com",
// Aucun plugin (Flash, Java, etc.)
"object-src 'none'",
// Empêche l'injection de <base> qui pourrait détourner les URLs relatives
"base-uri 'self'",
// Les formulaires ne peuvent soumettre qu'à l'origine actuelle
"form-action 'self'",
// Force HTTPS pour toutes les sous-ressources (utile si déployé en HTTP accidentellement)
"upgrade-insecure-requests",
].join("; ");
response.headers.set("Content-Security-Policy", csp);
return response;
}
export const config = {
// Appliquer sur toutes les routes sauf les assets statiques Next.js
matcher: [
"/((?!_next/static|_next/image|favicon\\.ico|.*\\.svg|.*\\.ico|.*\\.png|.*\\.jpg|.*\\.jpeg|.*\\.webp|.*\\.gif).*)",
],
};