Sécurité - rocambille/start-express-react GitHub Wiki
Cette page décrit les mécanismes de sécurité mis en place dans StartER : protection CSRF, stratégie contre le XSS, gestion des cookies, politique d'authentification, ainsi que les bonnes pratiques recommandées. L'objectif est de fournir un socle cohérent, minimaliste mais robuste, adapté à une architecture front React + API Express totalement découplée et stateless.
Dans l'architecture de StartER :
- l'API est stateless,
- le serveur ne maintient aucune session côté back,
- StartER ne configure pas explicitement CORS. Par défaut, aucune requête cross-site n'est autorisée, car le serveur ne renvoie aucun
Access-Control-Allow-Origin. - le front est servi sur le même domaine que le back : c'est le seul domaine autorisé à communiquer avec l'API,
- les cookies sont en
SameSite=strict.
Cela bloque déjà la majorité des attaques CSRF classiques, puisqu'un site tiers ne peut ni envoyer les cookies stricts, ni interagir avec l'API.
StartER utilise une protection additionnelle, notamment pour les requêtes mutatives (POST, PUT, PATCH, DELETE) : le pattern Client-Side Double-Submit.
Tip
Le pattern est particulièrement bien expliqué dans la FAQ du package csrf-csrf, avec d'autres éléments de réponse sur les attaques CSRF, la nécessité ou non de mettre en place des protections...
Le pattern est également mentionné entre autres dans la documentation de Symfony.
Pour compléter sur le sujet, nous vous recommandons de lire la documentation de l'OWASP.
Le front génère un jeton CSRF par l'utilitaire csrfToken() dans src/react/components/utils :
- le jeton est stocké dans un cookie
__Host-x-csrf-token(voir la documentation de l'OWASP sur le préfixe__Host-), - il expirera au bout de 30 secondes,
- mais chaque utilisation prolonge son expiration, reproduisant le comportement d'un cookie de session côté client,
- aucun stockage serveur n'est nécessaire.
Ce mécanisme permet de dire "la session CSRF est active tant qu'il y a de l'activité", tout en ayant un timeout explicite pour éviter les jetons périmés conservés trop longtemps.
Chaque requête mutative inclut un en-tête contenant la même valeur que celle stockée dans le cookie : c'est le "double-submit".
X-CSRF-Token: <token>
Le serveur :
- intercepte les requêtes POST/PUT/PATCH/DELETE,
- lit le cookie
__Host-x-csrf-token, - compare avec l'en-tête
x-csrf-token, - répond avec le statut
401en cas d'absence du header ou d'incohérence entre le header et le cookie.
Le serveur reste entièrement stateless : il compare simplement deux valeurs transmises par le client, sans maintenir de session ni stocker de token côté back.
Cette approche est acceptable tant que :
- L'API n'est pas exposée à des tiers (pas de cross-site autorisé).
-
SameSite=strictélimine la possibilité qu'un site tiers envoie les cookies nécessaires.
L'essentiel des protections XSS repose sur le front :
- aucune interpolation non sécurisée n'est faite dans le DOM,
- React empêche par défaut les injections HTML,
- StartER n'utilise
dangerouslySetInnerHTMLnulle part.
Du côté backend :
- les réponses JSON sont servies avec un
Content-Type: application/json, ce qui empêche leur interprétation comme du HTML ou du JavaScript exécutable, - toutes les données stockées en base sont traitées comme du contenu opaque côté front.
Pour les projets qui souhaitent aller plus loin :
- mettre en place une Content-Security-Policy (CSP) stricte,
- bloquer toute inline-script,
- n'autoriser que des sources explicites pour les scripts et styles.
StartER n'impose pas de CSP par défaut, mais la structure en front + API s'y prête bien.
StartER utilise deux cookies principaux :
-
__Host-auth: jeton JWT signé, portant l'identité utilisateur -
__Host-x-csrf-token: jeton CSRF éphémère renouvelé côté client
Les deux sont :
- en
SameSite=strict - en
Path=/ - préfixés avec
__Host-(voir la documentation de l'OWASP sur les préfixes de cookies)
Le cookie __Host-auth est en HttpOnly, ce qui le rend innaccessible par du code JS côté client. Ce n'est pas le cas pour le token CSRF qui est écrit côté client.
Le JWT stocké en cookie ne représente pas une session mais une attestation signée. Sa durée de vie doit rester courte, afin de limiter la fenêtre d'usage d'un jeton compromis.
Le JWT n'est pas chiffré, uniquement signé. Il contient des données qui sont lisibles par le client, mais protégées contre la modification.
Points importants :
- le serveur ne stocke pas d'état de session,
- la révocation se fait par expiration ou demande explicite au serveur de supprimer le cookie (logout).
StartER ne sert aucun contenu depuis un domaine tiers, ni depuis un sous-domaine différent. Cela réduit drastiquement les risques CSRF ainsi que les attaques liées aux requêtes provenant d'autres sites.
L'absence de session côté serveur limite les risques de :
- session fixation,
- cleanup incomplet,
- dépassement de mémoire pour suivi de sessions longues.
Par défaut, StartER ne traite pas l'upload de fichiers. Si ajouté, il doit être filtré, stocké hors du document root et validé (type, taille...).
Les déploiements devraient systématiquement inclure :
Strict-Transport-SecurityX-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-origin
Le serveur de production (Caddy, Nginx ou autre) est l'endroit adapté pour ces en-têtes.
Les erreurs renvoyées par le back sont volontairement sobres :
-
400pour les requêtes mal formées, -
401pour les problèmes d'authentification, -
403pour les problèmes d'autorisation, -
500pour les erreurs non gérées côté serveur, - pas de message détaillé exposant la nature de la défaillance (token manquant, signature invalide, cookie expiré...).
Le front affiche ensuite des messages clairs pour l'utilisateur sans révéler d'informations sensibles.
Il est recommandé de logger côté serveur les erreurs 401/403, ce qui inclut :
- les échecs d'authentification,
- les tentatives d'accès non authentifiées,
- les accès interdits.
Les requêtes mutatives (PUT, POST, PATCH, DELETE) devraient être systématiquement loggées avec :
- timestamp,
- utilisateur authentifié,
- endpoint appelé.
Un mécanisme périodique (mensuel / trimestriel) est recommandé pour prévenir :
- la fuite éventuelle d'une clé,
- la persistance de tokens anciens signés avec une clé obsolète.