Sécurité
Headers HTTP de sécurité
Les headers suivants sont appliqués à toutes les réponses via la configuration Next.js (next.config.ts) :
| Header | Valeur | Protection |
|---|---|---|
X-Content-Type-Options |
nosniff |
Empêche le MIME sniffing |
X-Frame-Options |
DENY |
Empêche le clickjacking (iframes) |
Referrer-Policy |
strict-origin-when-cross-origin |
Contrôle les infos de référent |
Permissions-Policy |
camera=(), geolocation=() |
Désactive les APIs non utilisées |
Note sur Permissions-Policy : le microphone est autorisé (nécessaire pour l'interface locuteur). Seules la caméra et la géolocalisation sont explicitement interdites.
Content Security Policy (CSP)
La CSP est configurée dynamiquement dans src/middleware.ts et src/proxy.ts. Elle inclut le domaine CDN audio.
Directives clés
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline'; ← unsafe-inline requis pour Next.js
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
media-src 'self' [AUDIO_CDN_DOMAIN] blob:;
connect-src 'self' [AUDIO_CDN_DOMAIN];
worker-src 'self' blob:; ← pour le Service Worker
frame-ancestors 'none'; ← équivalent X-Frame-Options: DENY
AUDIO_CDN_DOMAIN est injecté au runtime depuis la variable d'environnement. Si ce domaine est absent de media-src, les audios sont bloqués par le navigateur sans erreur visible pour l'utilisateur.
Diagnostic CSP
Si les audios ne se lisent pas, cherchez dans la console du navigateur :
Refused to connect to 'https://racines-s3.id2real.net/audios/...'
because it violates the following Content Security Policy directive: "connect-src 'self'"
Solution : vérifiez que AUDIO_CDN_DOMAIN est correctement défini.
Authentification
Voir la documentation complète : 07-authentification.md
Résumé des protections : - Mots de passe hashés PBKDF2 (100k itérations) - JWT HS256 dans cookie HttpOnly (pas de XSS possible) - Comparaison en temps constant (pas de timing attack)
Validation des entrées
API Routes
Toutes les API routes valident leurs entrées avant traitement. Exemple typique :
// Validation du corps de requête
const { name, email, subject, message } = await request.json();
if (!name || !email || !subject || !message) {
return Response.json({ error: 'Champs manquants' }, { status: 400 });
}
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return Response.json({ error: 'Email invalide' }, { status: 400 });
}
Requêtes SQL paramétrées
Jamais de concaténation de chaînes dans les requêtes SQL. Toujours des paramètres :
// ✅ Correct
const result = await pool.query(
'SELECT * FROM languages WHERE access_code = $1 AND is_active = true',
[code] // paramètre
);
// ❌ Incorrect (injection SQL possible)
const result = await pool.query(
`SELECT * FROM languages WHERE access_code = '${code}'`
);
Protection du proxy audio — path traversal
L'endpoint /api/audio/serve/[[...path]] proxy les fichiers MinIO. Il valide le chemin pour prévenir les attaques de traversal de répertoire :
// src/app/api/audio/serve/[[...path]]/route.ts
const path = params.path?.join('/') || '';
// Validation : pas de '..' dans le chemin
if (path.includes('..') || path.startsWith('/')) {
return Response.json({ error: 'Chemin invalide' }, { status: 400 });
}
Formulaire de contact — anti-spam
Honeypot
Un champ caché (website) est présent dans le HTML du formulaire. Les robots le remplissent automatiquement ; les humains ne le voient pas.
// Si le champ honeypot est rempli → rejet silencieux
if (body.website) {
return Response.json({ success: true }); // Fausse réponse de succès
}
Rate limiting
Maximum 1 message par heure par adresse IP :
- IP extraite du header X-Forwarded-For (via Traefik)
- Compteur stocké en mémoire (ou Redis si configuré)
- Réponse 429 : "Trop de messages envoyés, réessayez dans une heure"
Secrets et variables d'environnement
Règles absolues :
- Jamais de secrets dans le code source
- Jamais de fichiers .env commités dans Git (.gitignore configuré)
- Les secrets de production sont dans les variables GitLab CI/CD et le fichier .app_env sur le serveur
Variables sensibles :
- JWT_SECRET : si compromis, changez-le — toutes les sessions actives sont invalidées
- MINIO_SECRET_KEY et MINIO_ACCESS_KEY : accès complet au stockage audio
- PG_PASSWORD : accès à la base de données
CORS
MinIO est configuré avec des règles CORS strictes via Traefik middleware :
- Seule(s) l'URL(s) de l'application sont autorisées en Access-Control-Allow-Origin
- Méthodes autorisées : GET, HEAD
Le serveur Next.js n'a pas de configuration CORS particulière (les API routes ne sont accédées que depuis le frontend ou depuis le serveur lui-même).
Checklist sécurité
| Point | Status |
|---|---|
| Mots de passe hashés (PBKDF2) | ✅ |
| JWT dans cookie HttpOnly | ✅ |
| CSP configurée | ✅ |
| X-Frame-Options: DENY | ✅ |
| Requêtes SQL paramétrées | ✅ |
| Validation côté serveur | ✅ |
| Protection path traversal (proxy audio) | ✅ |
| Honeypot anti-spam (contact) | ✅ |
| Rate limiting (contact) | ✅ |
| Secrets hors du code source | ✅ |
| Pas de logs de données sensibles | ✅ |