veza/apps/web/src/utils/csp.ts
2025-12-17 08:07:35 -05:00

174 lines
4.5 KiB
TypeScript

/**
* 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
*/
export const CSP_POLICY = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'nonce-__CSP_NONCE__'", // Nonce pour scripts inline
'https://cdn.jsdelivr.net', // Pour les CDN si nécessaire
],
'style-src': [
"'self'",
"'unsafe-inline'", // Nécessaire pour Tailwind CSS
'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'],
'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<string, string[]> = {
...CSP_POLICY,
} as unknown as Record<string, string[]>;
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(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // Supprimer les scripts
.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, ''); // Supprimer les iframes
}
/**
* Configuration CSP pour le développement (plus permissive)
*/
export const CSP_POLICY_DEV = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'unsafe-inline'", // Nécessaire pour Vite HMR
"'unsafe-eval'", // Nécessaire pour Vite en dev
],
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", 'data:', 'https:', 'blob:'],
'connect-src': ["'self'", 'ws:', 'wss:', 'http:', 'https:'],
'font-src': ["'self'", 'data:', 'https:'],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'frame-ancestors': ["'none'"],
};
/**
* Construit la CSP pour le développement
*/
export function buildCSPHeaderDev(): string {
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
*/
export function createCSPMiddleware() {
return (_req: any, res: any, next: any) => {
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();
};
}