Sécurité du navigateur
Le frontend web ne détient aucun secret : pas de clé privée, pas de clé d'API, seulement le jeton de session de l'utilisateur. Sa sécurité tient en trois points : des en-têtes HTTP qui limitent ce que le navigateur accepte de charger et d'afficher, une hygiène du code qui évite les injections et les redirections ouvertes, et un build qui ne publie ni secret ni source map. Le modèle de menaces global est dans Modèle de menaces et la comparaison avec le mobile dans Navigateur et mobile.
En-têtes HTTP
Tous les en-têtes sont définis dans la fonction headers() de frontend-web/next.config.mjs et appliqués à toutes les routes (source: '/:path*'). Next.js les envoie aussi bien avec next start qu'avec le serveur standalone de l'image Docker.
| En-tête | Valeur | Effet |
|---|---|---|
X-Content-Type-Options |
nosniff |
Le navigateur respecte le type MIME déclaré |
X-Frame-Options |
DENY |
Interdit l'affichage du site dans un cadre (anti clickjacking), doublé par frame-ancestors 'none' |
Referrer-Policy |
strict-origin-when-cross-origin |
Seule l'origine est transmise aux autres sites, rien en passant de HTTPS à HTTP |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=() |
Désactive ces API pour le site et les éventuels cadres |
Strict-Transport-Security |
max-age=63072000; includeSubDomains |
Impose HTTPS pendant deux ans sur le domaine et ses sous-domaines |
Cross-Origin-Opener-Policy |
same-origin |
Isole la fenêtre des pages ouvertes depuis d'autres origines |
Content-Security-Policy |
voir ci-dessous | Uniquement si NODE_ENV === 'production' |
Deux autres réglages complètent la liste :
| Réglage | Valeur | Effet |
|---|---|---|
poweredByHeader |
false |
Supprime l'en-tête X-Powered-By: Next.js |
productionBrowserSourceMaps |
false |
Aucune source map publique en production |
Cache-Control sur /_next/static/:path* |
public, max-age=31536000, immutable, en production seulement |
Les fichiers à empreinte sont mis en cache un an. En développement, l'en-tête n'est pas envoyé : il empêchait le navigateur de recharger le code modifié et provoquait des erreurs d'hydratation |
Strict-Transport-Security est envoyé même en développement sur http://localhost ; les navigateurs l'ignorent sur une connexion non chiffrée, il ne gêne donc pas le travail local. En production, il ne protège que si le site est réellement servi en HTTPS par le proxy (voir Déploiement sur etherbay.tech).
Politique de sécurité du contenu
La CSP est construite à partir des deux variables publiques. originOf extrait l'origine de NEXT_PUBLIC_API_URL et NEXT_PUBLIC_WS_URL (repli sur localhost:2053), puis toWebSocket en déduit les variantes ws: ou wss:.
Avec les valeurs par défaut, la politique produite est :
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https: http://localhost:2053;
font-src 'self' data:;
connect-src 'self' http://localhost:2053 ws://localhost:2053;
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
manifest-src 'self';
worker-src 'self' blob:| Directive | Raison |
|---|---|
script-src 'self' 'unsafe-inline' |
Next.js injecte des scripts inline d'hydratation, et le layout racine injecte themeInitScript ; aucun nonce n'est généré |
style-src 'self' 'unsafe-inline' |
Styles inline de React (style=) et de Next.js |
img-src ... https: <origine de l'API> |
Images servies par l'API (visuels POAP, QR codes en data:) et logos de partenaires sur n'importe quel hôte HTTPS |
connect-src |
Seuls l'API et le serveur Socket.IO sont joignables par fetch et WebSocket |
object-src 'none', base-uri 'none' |
Pas de plugin, pas de changement de base des URL |
form-action 'self' |
Les formulaires ne peuvent pas être soumis ailleurs |
media-src absent |
Repli sur default-src 'self' : seuls les sons servis par le site (/sounds/*.mp3) peuvent être lus |
upgrade-insecure-requests |
Ajouté seulement si l'API est en https: |
En production, avec NEXT_PUBLIC_API_URL=https://api.etherbay.tech/api par exemple, connect-src contient https://api.etherbay.tech et wss://api.etherbay.tech, et upgrade-insecure-requests est ajouté.
Attention
'unsafe-inline' dans script-src affaiblit nettement la CSP : une injection HTML qui insère un <script> serait exécutée. Comme le jeton de session est dans localStorage, une XSS permettrait de le lire. La protection repose donc surtout sur React, qui échappe tout texte par défaut. Passer à une CSP à nonce (middleware Next.js qui génère un nonce par requête et le transmet aux scripts) est l'amélioration prioritaire.
La CSP n'est pas envoyée en développement, pour ne pas gêner le rechargement à chaud de Next.js. Pour la tester, il faut un build de production (npm run build puis npm run start).
Important
Les variables NEXT_PUBLIC_* sont lues au moment du build. Une image Docker construite avec http://localhost:2053/api gardera cette origine dans sa CSP même si l'API est déployée ailleurs : le navigateur bloquera alors tous les appels. Il faut passer les bonnes valeurs en --build-arg.
Hygiène du code
| Risque | Mesure | Fichier |
|---|---|---|
| Injection de HTML | Aucun dangerouslySetInnerHTML dans les pages ; le seul usage est themeInitScript, une chaîne constante sans donnée utilisateur |
layout.tsx |
| Rapport imprimable | Chaque valeur passe par escapeHtml avant document.write |
download.ts |
| Export CSV de l'audit | csvCell encadre chaque cellule de guillemets et double les guillemets internes. Il ne neutralise pas les cellules qui commencent par =, +, - ou @ : une valeur de métadonnée piégée pourrait être interprétée comme formule par un tableur |
mairie/audit/page.tsx |
| Redirection ouverte | safeInternalPath n'accepte qu'un chemin interne pour next et refuse //, /\, /connexion, /invite |
navigation.ts |
| Liens externes | target="_blank" toujours accompagné de rel="noopener noreferrer" (explorateur, Etherscan, partage) |
composants de loterie, LinkButton |
| Paramètres d'URL | utm_source filtré par une expression régulière, code de parrainage limité à 4 à 64 caractères [a-z0-9-] |
analytics.ts, invite/[code]/page.tsx |
Accès à localStorage |
Toutes les lectures et écritures sont dans des try/catch (navigation privée, stockage plein) |
storage.ts |
| Données sensibles dans l'URL | Aucun email ni jeton dans les paramètres ; le code de parrainage est la seule donnée de chemin | |
| Double soumission | Boutons désactivés pendant la requête | Button.tsx |
Le contrôle d'accès du navigateur (RequireRole) n'est pas une barrière de sécurité : il masque des écrans, le backend vérifie chaque requête (voir Authentification et routes protégées).
Stockage dans le navigateur
| Donnée | Stockage | Sensibilité |
|---|---|---|
| Jeton JWT | localStorage bl.auth.token |
Élevée : donne accès au compte pendant sa durée de vie (12 h par défaut), révocable par la déconnexion |
| Profil | localStorage bl.auth.user |
Moyenne : email, adresse, rôle, solde de points |
| Dernier code obtenu | localStorage bl.marketplace.lastClaim |
Moyenne : code promo utilisable en caisse ; effacé à la déconnexion |
Préférences (dont bl.pref.sound), source UTM, parrainage, choix de mesure d'audience (bl.consent.choice), établissement choisi |
localStorage bl.* |
Faible |
Aucun cookie n'est posé par le frontend. L'absence de cookie supprime le risque CSRF sur l'API, au prix d'une exposition du jeton à une éventuelle XSS. Ce compromis est discuté dans Sessions.
Contrôles de la CI
Le job « Frontend web » de .github/workflows/ci.yml vérifie, après le build :
- qu'aucune dépendance de production n'a de vulnérabilité haute ou critique (
npm audit --omit=dev --audit-level=high) ; - que
.next/staticne contient aucun motif de secret (sk_live_, clé AWS, en-tête de clé privée, clé privée par défaut du premier compte Hardhat) ; - qu'aucun fichier
.mapn'est publié dans.next/static.
L'image Docker est ensuite analysée par Trivy dans le job des images (voir Intégration continue).
Signalement
frontend-web/public/.well-known/security.txt est servi à /.well-known/security.txt. Il indique le contact securite@etherbay.fr, une expiration au 10 novembre 2027, les langues fr et en et un lien vers SECURITY.md sur GitHub. La procédure est décrite dans Signaler une vulnérabilité.