2025-12-03 21:56:50 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
2025-12-13 02:34:34 +00:00
|
|
|
let cspNonce: string | null = null;
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Définit le nonce CSP pour la session courante
|
|
|
|
|
*/
|
|
|
|
|
export function setCSPNonce(nonce: string): void {
|
2025-12-13 02:34:34 +00:00
|
|
|
cspNonce = nonce;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Récupère le nonce CSP actuel
|
|
|
|
|
*/
|
|
|
|
|
export function getCSPNonce(): string | null {
|
2025-12-13 02:34:34 +00:00
|
|
|
return cspNonce;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Génère un nonce CSP sécurisé
|
|
|
|
|
*/
|
|
|
|
|
export function generateCSPNonce(): string {
|
2025-12-13 02:34:34 +00:00
|
|
|
const array = new Uint8Array(16);
|
|
|
|
|
crypto.getRandomValues(array);
|
|
|
|
|
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(
|
|
|
|
|
'',
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Configuration CSP pour la production
|
2026-01-18 12:55:28 +00:00
|
|
|
* 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)
|
2025-12-03 21:56:50 +00:00
|
|
|
*/
|
|
|
|
|
export const CSP_POLICY = {
|
|
|
|
|
'default-src': ["'self'"],
|
|
|
|
|
'script-src': [
|
|
|
|
|
"'self'",
|
2026-01-18 12:55:28 +00:00
|
|
|
"'nonce-__CSP_NONCE__'", // Nonce pour scripts inline - PAS d'unsafe-inline ni unsafe-eval
|
2025-12-03 21:56:50 +00:00
|
|
|
'https://cdn.jsdelivr.net', // Pour les CDN si nécessaire
|
|
|
|
|
],
|
|
|
|
|
'style-src': [
|
|
|
|
|
"'self'",
|
2026-01-18 12:55:28 +00:00
|
|
|
"'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
|
2025-12-03 21:56:50 +00:00
|
|
|
'https://fonts.googleapis.com',
|
|
|
|
|
],
|
2025-12-13 02:34:34 +00:00
|
|
|
'img-src': ["'self'", 'data:', 'https:', 'blob:'],
|
|
|
|
|
'connect-src': ["'self'", 'ws:', 'wss:', 'http:', 'https:'],
|
|
|
|
|
'font-src': ["'self'", 'data:', 'https://fonts.gstatic.com'],
|
2026-01-18 13:03:02 +00:00
|
|
|
'frame-src': ["'self'", 'http://localhost:8080', 'https://localhost:8080'],
|
2025-12-03 21:56:50 +00:00
|
|
|
'object-src': ["'none'"],
|
|
|
|
|
'base-uri': ["'self'"],
|
|
|
|
|
'form-action': ["'self'"],
|
|
|
|
|
'frame-ancestors': ["'none'"],
|
|
|
|
|
'upgrade-insecure-requests': [],
|
2025-12-13 02:34:34 +00:00
|
|
|
} as const;
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Construit la chaîne CSP à partir de la configuration
|
|
|
|
|
*/
|
|
|
|
|
export function buildCSPHeader(nonce?: string): string {
|
2025-12-13 02:34:34 +00:00
|
|
|
const policy: Record<string, string[]> = {
|
|
|
|
|
...CSP_POLICY,
|
|
|
|
|
} as unknown as Record<string, string[]>;
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
if (nonce) {
|
2025-12-13 02:34:34 +00:00
|
|
|
policy['script-src'] = policy['script-src'].map((src) =>
|
|
|
|
|
src === "'nonce-__CSP_NONCE__'" ? `'nonce-${nonce}'` : src,
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
2025-12-13 02:34:34 +00:00
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
return Object.entries(policy)
|
|
|
|
|
.map(([directive, sources]) => {
|
|
|
|
|
if (sources.length === 0) {
|
2025-12-13 02:34:34 +00:00
|
|
|
return directive;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
2025-12-13 02:34:34 +00:00
|
|
|
return `${directive} ${sources.join(' ')}`;
|
2025-12-03 21:56:50 +00:00
|
|
|
})
|
2025-12-13 02:34:34 +00:00
|
|
|
.join('; ');
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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*=/,
|
2025-12-13 02:34:34 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return !dangerousPatterns.some((pattern) => pattern.test(scriptContent));
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // Supprimer les scripts
|
2025-12-13 02:34:34 +00:00
|
|
|
.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, ''); // Supprimer les iframes
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Configuration CSP pour le développement (plus permissive)
|
2026-01-18 12:55:28 +00:00
|
|
|
* SECURITY NOTE: unsafe-eval est nécessaire pour Vite HMR en développement uniquement
|
|
|
|
|
* Cette configuration NE DOIT JAMAIS être utilisée en production
|
2025-12-03 21:56:50 +00:00
|
|
|
*/
|
|
|
|
|
export const CSP_POLICY_DEV = {
|
|
|
|
|
'default-src': ["'self'"],
|
|
|
|
|
'script-src': [
|
|
|
|
|
"'self'",
|
|
|
|
|
"'unsafe-inline'", // Nécessaire pour Vite HMR
|
2026-01-18 12:55:28 +00:00
|
|
|
"'unsafe-eval'", // Nécessaire pour Vite HMR en dev uniquement - NE PAS utiliser en production
|
2025-12-03 21:56:50 +00:00
|
|
|
],
|
2026-01-18 12:55:28 +00:00
|
|
|
'style-src': ["'self'", "'unsafe-inline'"], // Nécessaire pour Tailwind CSS et Vite HMR
|
2025-12-13 02:34:34 +00:00
|
|
|
'img-src': ["'self'", 'data:', 'https:', 'blob:'],
|
|
|
|
|
'connect-src': ["'self'", 'ws:', 'wss:', 'http:', 'https:'],
|
|
|
|
|
'font-src': ["'self'", 'data:', 'https:'],
|
2026-01-18 13:03:02 +00:00
|
|
|
'frame-src': ["'self'", 'http://localhost:8080', 'https://localhost:8080'],
|
2025-12-03 21:56:50 +00:00
|
|
|
'object-src': ["'none'"],
|
|
|
|
|
'base-uri': ["'self'"],
|
|
|
|
|
'form-action': ["'self'"],
|
|
|
|
|
'frame-ancestors': ["'none'"],
|
2025-12-13 02:34:34 +00:00
|
|
|
};
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Construit la CSP pour le développement
|
2026-01-18 12:55:28 +00:00
|
|
|
* SECURITY: Cette fonction ne doit être utilisée qu'en mode développement
|
|
|
|
|
* En production, utiliser buildCSPHeader() avec des nonces stricts
|
2025-12-03 21:56:50 +00:00
|
|
|
*/
|
|
|
|
|
export function buildCSPHeaderDev(): string {
|
2026-01-18 12:55:28 +00:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
return Object.entries(CSP_POLICY_DEV)
|
|
|
|
|
.map(([directive, sources]) => {
|
|
|
|
|
if (sources.length === 0) {
|
2025-12-13 02:34:34 +00:00
|
|
|
return directive;
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
2025-12-13 02:34:34 +00:00
|
|
|
return `${directive} ${sources.join(' ')}`;
|
2025-12-03 21:56:50 +00:00
|
|
|
})
|
2025-12-13 02:34:34 +00:00
|
|
|
.join('; ');
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hook pour utiliser le nonce CSP dans les composants React
|
|
|
|
|
*/
|
|
|
|
|
export function useCSPNonce(): string | null {
|
2025-12-13 02:34:34 +00:00
|
|
|
return getCSPNonce();
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Middleware pour injecter le nonce CSP dans les réponses
|
2026-01-18 12:55:28 +00:00
|
|
|
* NOTE: Ce middleware est destiné à être utilisé côté serveur (Node.js/Express)
|
|
|
|
|
* Si utilisé côté client, il ne sera pas exécuté
|
2025-12-03 21:56:50 +00:00
|
|
|
*/
|
|
|
|
|
export function createCSPMiddleware() {
|
2026-01-18 12:55:28 +00:00
|
|
|
// 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<string, string | string[] | undefined> },
|
|
|
|
|
res: {
|
|
|
|
|
setHeader: (name: string, value: string) => void;
|
|
|
|
|
},
|
|
|
|
|
next: () => void,
|
|
|
|
|
) => {
|
2025-12-13 02:34:34 +00:00
|
|
|
const nonce = generateCSPNonce();
|
|
|
|
|
setCSPNonce(nonce);
|
|
|
|
|
|
|
|
|
|
const cspHeader =
|
2025-12-17 13:07:35 +00:00
|
|
|
import.meta.env.MODE === 'production'
|
2025-12-13 02:34:34 +00:00
|
|
|
? 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();
|
|
|
|
|
};
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|