Politique de Sécurité
Ce fichier est la référence sécurité de la plateforme. Il répond à trois questions : comment nous nous protégeons, quoi faire si quelque chose est compromis, et comment nous gardons les dépendances saines. C'est pour nous d'abord, pour un auditeur ensuite.
Surface d'attaque — Ce qui est exposé publiquement
Cette section documente exactement ce qui est accessible sans authentification :
| Endpoint / Ressource | Public | Authentifié | Admin uniquement |
|---|---|---|---|
POST /api/v1/auth/login | ✅ | — | — |
POST /api/v1/auth/register | ✅ | — | — |
GET /api/v1/internships | — | ✅ | — |
POST /api/v1/applications | — | ✅ (L3/M) | — |
GET /api/v1/admin/users | — | — | ✅ |
| Frontends (HTML/JS/CSS) | ✅ | — | — |
Règle : Tout ce qui n'est pas dans la colonne "Public" doit être refusé par le backend si le token est absent ou invalide. Le frontend n'est pas une protection.
Protection contre les attaques communes
Cette section documente chaque mesure en place et comment la vérifier :
Injection SQL
- Mesure : Client Supabase natif avec requêtes paramétrées. Aucune requête SQL construite à la main avec des f-strings.
- Ce qui est interdit :
f"SELECT * FROM users WHERE email='{email}'"— jamais. - Comment vérifier : Le SDK Supabase gère automatiquement la protection contre les injections. Les requêtes brutes (raw) doivent être évitées.
XSS (Cross-Site Scripting)
- Mesure : React échappe automatiquement le contenu des variables dans le JSX.
- Ce qui est interdit :
dangerouslySetInnerHTMLsans sanitization explicite. - Tokens en JSON body : Les tokens sont retournés dans le corps de la réponse JSON. Le stockage côté client est géré par le frontend (localStorage ou mémoire JS).
CSRF (Cross-Site Request Forgery)
- Mesure :
SameSite=Laxsur les cookies + validation de l'origine dans les headers CORS côté backend. - Comment ça protège : Un site tiers ne peut pas déclencher des requêtes authentifiées à notre API depuis le navigateur d'un utilisateur connecté.
CORS
- Mesure : Liste blanche explicite des origines autorisées dans FastAPI (jamais
allow_origins=["*"]avec credentials). - Où c'est configuré :
app/main.py, constanteALLOWED_ORIGINS. - Comment vérifier : Tester depuis une origine non listée → doit recevoir une erreur CORS.
Rate Limiting
- Mesure : Nous limitons les endpoints sensibles (login, register, forgot-password) pour bloquer le brute force.
- Outil recommandé :
slowapi(FastAPI) ou rate limiting au niveau du reverse proxy (Nginx/Dokploy). - Seuils minimaux : Login → 10 tentatives par minute par IP. Register → 5 par minute par IP.
- Nous documentons ici les seuils exacts configurés.
Mots de passe
- Mesure : Hachage avec
bcrypt(coût minimum 12). Jamais MD5, jamais SHA1, jamais en clair. - Aucun mot de passe ne doit apparaître dans les logs — vérifier que le schéma Pydantic de login exclut le password des logs.
Logs d'audit — Ce qui est tracé
Cette section documente quelles actions sont loggées et où :
| Action | Loggée ? | Où | Ce qui est enregistré |
|---|---|---|---|
| Login réussi | ✅ | DB table audit_logs | user_id, IP, timestamp |
| Login échoué | ✅ | DB table audit_logs | email tenté, IP, timestamp |
| Logout | ✅ | DB table audit_logs | user_id, timestamp |
| Action admin (suppression, modification) | ✅ | DB table audit_logs | admin_id, action, target_id, timestamp |
| Erreurs 500 | ✅ | Sentry | Stack trace complète |
Règle : Les logs d'audit ne doivent jamais contenir de données sensibles (mot de passe, token complet, données personnelles non nécessaires).
Durée de rétention : Définir ici combien de temps les logs sont conservés (ex : 90 jours).
Gestion des dépendances vulnérables
Le vrai risque silencieux : une lib que nous utilisons a une faille connue et nous ne le savons pas.
Procédure de vérification (à faire chaque mois)
Backend (Python) :
pip install pip-audit
pip-audit
# Liste les CVE connues dans nos dépendances
Frontend (Node.js) :
npm audit
# ou
npm audit --fix # Correction automatique des failles mineures
Règle de réponse selon la sévérité
| Sévérité | Délai de correction | Responsable |
|---|---|---|
| Critique (CVSS ≥ 9) | 24h maximum | Dev senior en priorité absolue |
| Haute (CVSS 7-9) | 1 semaine | Dev senior |
| Moyenne (CVSS 4-7) | Prochain sprint | N'importe quel dev |
| Faible (CVSS < 4) | Backlog | Traité quand possible |
Configurer GitHub Dependabot pour recevoir automatiquement des PRs quand une dépendance a une faille connue. C'est gratuit et ça prend 5 minutes à configurer.
Procédure d'incident — Si quelque chose est compromis
C'est le document le plus important à lire avant qu'un incident arrive, pas pendant.
Scénario 1 : Secret/credential exposé (API key, DB password dans un commit)
- Immédiatement (< 5 min) : Révoquer et régénérer le credential compromis — avant même de comprendre comment c'est arrivé
- Dans l'heure : Vérifier les logs pour détecter une utilisation non autorisée du credential
- Dans la journée : Identifier comment c'est arrivé, corriger le processus (
.gitignore, pre-commit hook, etc.) - Nous documentons : Ajouter un post-mortem dans
security/incidents/YYYY-MM-DD-description.md
Scénario 2 : Compte admin compromis
- Immédiatement : Changer le mot de passe du compte (la révocation de token est partielle — voir lacunes connues — le token actif reste valide jusqu'à expiration naturelle, soit 30 minutes pour l'access token)
- Immédiatement : Examiner les logs d'audit pour toutes les actions effectuées par ce compte dans les 24-72h précédentes
Scénario 3 : Faille découverte dans notre code
- Ne pas patcher en prod directement — créer une branche
hotfix/ - Évaluer l'impact réel avant de paniquer : la faille est-elle exploitable ? A-t-elle été exploitée ?
- Patcher, tester sur staging, déployer
- Post-mortem obligatoire
Contacts d'urgence
Nous documentons ici (pas dans ce guide public, dans notre version interne) :
- Qui appeler en dehors des heures de bureau
- Accès Dokploy/Vercel en urgence (qui a les droits ?)
- Si obligation légale de notification : contact CNIL
RGPD — Données personnelles collectées
Documenter ce que nous stockons et pourquoi — utile si quelqu'un demande la suppression de ses données ou si la CNIL pose des questions.
| Donnée | Où stockée | Pourquoi | Durée de rétention |
|---|---|---|---|
DB users | Authentification | Jusqu'à suppression du compte | |
| Mot de passe (haché) | DB users | Authentification | Jusqu'à suppression du compte |
| CV (fichier) | Stockage fichiers | Candidature | Jusqu'à suppression du compte |
| Logs d'activité | DB audit_logs | Sécurité | 90 jours |
Procédure de suppression de compte : Nous documentons exactement quelles tables sont touchées quand un utilisateur demande la suppression de ses données (droit à l'effacement RGPD).
Ce qui n'est PAS encore en place (et c'est ok)
Être honnête sur ce qui manque évite de fausser un futur audit. Nous documentons les lacunes connues avec une date cible :
| Mesure | Statut | Priorité | Date cible |
|---|---|---|---|
| Pentest externe | ❌ Pas fait | Moyenne | Quand 500+ users actifs |
| 2FA pour les admins | ❌ Pas implémenté | Haute | Sprint 3 |
| Chiffrement des fichiers au repos | ❌ Pas fait | Moyenne | Phase 2 |
| Audit trail complet | 🟡 Partiel | Haute | Sprint 2 |
| Révocation immédiate des tokens | ✅ Implémenté | Haute | Sprint 2 |
| Tokens en mémoire JS (pas localStorage) | ❌ À corriger | Haute | Sprint 1 |
Solutions à implémenter
Révocation des tokens JWT ✅ Implémenté
État actuel (Sprint 2) :
- Redis est maintenant activé par défaut en production (
REDIS_ENABLED=true) - Chaque token JWT contient un
jti(JWT ID) unique - Les tokens révoqués sont stockés dans Redis avec clé
revoked:jti:{jti}et TTL automatique - Le logout révoque immédiatement le token (pas d'attente de 15 min)
- Endpoint
/api/v1/auth/revokedisponible pour révoquer manuellement un token
Fonctionnalités implémentées :
- ✅ JTI ajouté dans le payload JWT
- ✅ Stockage Redis des tokens révoqués avec TTL
- ✅ Vérification de la révocation à chaque requête
- ✅ Endpoint POST
/api/v1/auth/revoke - ✅ Logout révoque le token immédiatement
Limitations restantes :
- Pas de refresh token rotation (issue #14)
Stockage des tokens côté client
Problème actuel : L'access token est stocké dans localStorage côté frontend. Cette approche est vulnérable aux attaques XSS — tout script malveillant exécuté sur la page peut lire le localStorage et voler les tokens.
Solution à implémenter :
- Stocker l'access token en mémoire JavaScript (variable en mémoire, pas dans localStorage)
- Stocker le refresh token dans un cookie httpOnly avec les flags
SecureetSameSite=Strict - Le cookie httpOnly n'est pas accessible via JavaScript, ce qui protège contre le vol par XSS
- Implémenter un mécanisme de refresh automatique quand l'access token expire