/** * Content Security Policy (CSP) utilities * Gère les nonces et la configuration CSP pour la sécurité */ // Nonce généré côté serveur pour les scripts inline let cspNonce: string | null = null; /** * Définit le nonce CSP pour la session courante */ export function setCSPNonce(nonce: string): void { cspNonce = nonce; } /** * Récupère le nonce CSP actuel */ export function getCSPNonce(): string | null { return cspNonce; } /** * Génère un nonce CSP sécurisé */ export function generateCSPNonce(): string { const array = new Uint8Array(16); crypto.getRandomValues(array); return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join( '', ); } /** * Configuration CSP pour la production * SECURITY: Cette configuration utilise des nonces stricts, pas d'unsafe-inline ni unsafe-eval * Pour Tailwind CSS, on peut utiliser des nonces ou 'unsafe-inline' uniquement pour style-src * (moins critique que script-src car les styles ne peuvent pas exécuter de code) */ export const CSP_POLICY = { 'default-src': ["'self'"], 'script-src': [ "'self'", "'nonce-__CSP_NONCE__'", // Nonce pour scripts inline - PAS d'unsafe-inline ni unsafe-eval 'https://cdn.jsdelivr.net', // Pour les CDN si nécessaire ], 'style-src': [ "'self'", "'nonce-__CSP_NONCE__'", // Nonce pour styles inline (préféré) "'unsafe-inline'", // Fallback pour Tailwind CSS - acceptable car style-src ne peut pas exécuter de code 'https://fonts.googleapis.com', ], 'img-src': ["'self'", 'data:', 'https:', 'blob:'], 'connect-src': ["'self'", 'ws:', 'wss:', 'http:', 'https:'], 'font-src': ["'self'", 'data:', 'https://fonts.gstatic.com'], 'frame-src': ["'self'", 'http://localhost:8080', 'https://localhost:8080'], 'object-src': ["'none'"], 'base-uri': ["'self'"], 'form-action': ["'self'"], 'frame-ancestors': ["'none'"], 'upgrade-insecure-requests': [], } as const; /** * Construit la chaîne CSP à partir de la configuration */ export function buildCSPHeader(nonce?: string): string { const policy: Record = { ...CSP_POLICY, } as unknown as Record; if (nonce) { policy['script-src'] = policy['script-src'].map((src) => src === "'nonce-__CSP_NONCE__'" ? `'nonce-${nonce}'` : src, ); } return Object.entries(policy) .map(([directive, sources]) => { if (sources.length === 0) { return directive; } return `${directive} ${sources.join(' ')}`; }) .join('; '); } /** * Valide qu'un script peut être exécuté selon la CSP */ export function validateScriptExecution(scriptContent: string): boolean { // Vérifications de base pour les scripts inline const dangerousPatterns = [ /eval\s*\(/, /Function\s*\(/, /setTimeout\s*\(\s*["']/, /setInterval\s*\(\s*["']/, /document\.write/, /innerHTML\s*=/, /outerHTML\s*=/, ]; return !dangerousPatterns.some((pattern) => pattern.test(scriptContent)); } /** * Sanitise le contenu HTML pour éviter les violations CSP */ export function sanitizeForCSP(content: string): string { return content .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, '') // Supprimer les event handlers inline .replace(/]*>[\s\S]*?<\/script>/gi, '') // Supprimer les scripts .replace(/]*>[\s\S]*?<\/iframe>/gi, ''); // Supprimer les iframes } /** * Configuration CSP pour le développement (plus permissive) * SECURITY NOTE: unsafe-eval est nécessaire pour Vite HMR en développement uniquement * Cette configuration NE DOIT JAMAIS être utilisée en production */ export const CSP_POLICY_DEV = { 'default-src': ["'self'"], 'script-src': [ "'self'", "'unsafe-inline'", // Nécessaire pour Vite HMR "'unsafe-eval'", // Nécessaire pour Vite HMR en dev uniquement - NE PAS utiliser en production ], 'style-src': ["'self'", "'unsafe-inline'"], // Nécessaire pour Tailwind CSS et Vite HMR 'img-src': ["'self'", 'data:', 'https:', 'blob:'], 'connect-src': ["'self'", 'ws:', 'wss:', 'http:', 'https:'], 'font-src': ["'self'", 'data:', 'https:'], 'frame-src': ["'self'", 'http://localhost:8080', 'https://localhost:8080'], 'object-src': ["'none'"], 'base-uri': ["'self'"], 'form-action': ["'self'"], 'frame-ancestors': ["'none'"], }; /** * Construit la CSP pour le développement * SECURITY: Cette fonction ne doit être utilisée qu'en mode développement * En production, utiliser buildCSPHeader() avec des nonces stricts */ export function buildCSPHeaderDev(): string { // Vérifier qu'on est bien en mode développement if (import.meta.env.MODE === 'production') { console.error('[CSP] SECURITY WARNING: buildCSPHeaderDev() called in production mode! Using strict CSP instead.'); return buildCSPHeader(); } return Object.entries(CSP_POLICY_DEV) .map(([directive, sources]) => { if (sources.length === 0) { return directive; } return `${directive} ${sources.join(' ')}`; }) .join('; '); } /** * Hook pour utiliser le nonce CSP dans les composants React */ export function useCSPNonce(): string | null { return getCSPNonce(); } /** * Middleware pour injecter le nonce CSP dans les réponses * NOTE: Ce middleware est destiné à être utilisé côté serveur (Node.js/Express) * Si utilisé côté client, il ne sera pas exécuté */ export function createCSPMiddleware() { // Type pour les paramètres du middleware Express // NOTE: Ces types ne sont pas disponibles côté client, donc on utilise des types génériques // Si ce middleware est utilisé côté serveur, installer @types/express pour les types corrects return ( _req: { headers?: Record }, res: { setHeader: (name: string, value: string) => void; }, next: () => void, ) => { const nonce = generateCSPNonce(); setCSPNonce(nonce); const cspHeader = import.meta.env.MODE === 'production' ? buildCSPHeader(nonce) : buildCSPHeaderDev(); res.setHeader('Content-Security-Policy', cspHeader); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); next(); }; }