Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

12.2 — Sécurité frontend

🎯 Objectif : protéger les utilisateurs contre les attaques côté navigateur : XSS qui vole les sessions, CSRF qui exécute des actions à leur insu, clickjacking qui les manipule.

À l'issue de cet axe, tu sauras :

  • Distinguer XSS stocké, réfléchi, DOM
  • Configurer une Content Security Policy (CSP) stricte
  • Comprendre CSRF et savoir s'en protéger (SameSite + tokens)
  • Activer HSTS, X-Frame-Options, et autres en-têtes de sécu
  • Valider et échapper les inputs dans toutes les contextes (HTML, attributs, JS, URLs)

Confirmé 10 min prérequis : axes 8 et 11 lus

L’attaque n°1 côté frontend. Un attaquant injecte du JS qui s’exécute dans le navigateur d’autres utilisateurs.

Un commentaire utilisateur <script>steal(document.cookie)</script> est sauvegardé en DB et affiché à tous les visiteurs sans échappement.

// ❌
const html = `<div>${userComment}</div>`;
res.send(html);

L’input arrive dans l’URL et est renvoyé tel quel.

https://example.com/search?q=<script>steal()</script>
// ❌
res.send(`<h1>Résultats pour ${req.query.q}</h1>`);

Côté client, du JS prend une valeur de l’URL et l’injecte dans le DOM sans échapper.

// ❌
document.getElementById('greeting').innerHTML = location.hash.slice(1);
// URL : example.com/#<img src=x onerror=alert(1)>
  • Vol de cookies / tokens (si pas HttpOnly).
  • Actions au nom de la victime (changer son mot de passe).
  • Phishing dans la page.
  • Crypto-mining dans le navigateur.

Les frameworks modernes (React, Vue, Svelte) échappent par défaut :

// ✅ React — échappe automatiquement
<div>{userComment}</div>

Le danger : dangerouslySetInnerHTML (React), v-html (Vue), innerHTML (vanilla). Si tu DOIS afficher du HTML utilisateur, assainis avec DOMPurify :

import DOMPurify from 'isomorphic-dompurify';
const clean = DOMPurify.sanitize(userHtml);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;

Le même input s’échappe différemment selon où il finit :

ContexteÉchappement
Texte HTML< → &lt;, > → &gt;, etc.
Attribut HTMLéchapper guillemets aussi
URLencodeURIComponent
JavaScriptJSON.stringify
CSSéviter, c’est complexe

Les frameworks gèrent ça correctement par défaut.

Défense en profondeur : même si une XSS passe, la CSP bloque l’exécution du JS injecté.

Content-Security-Policy:
default-src 'self';
script-src 'self' https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.stripe.com;
frame-ancestors 'none';
base-uri 'self';

Le navigateur refuse d’exécuter tout script qui n’est pas du domaine autorisé.

middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
const nonce = crypto.randomUUID();
const csp = `script-src 'self' 'nonce-${nonce}'; ...`;
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}
// Layout
const nonce = (await headers()).get('x-nonce');
<script nonce={nonce}>console.log('OK')</script>

Le nonce change à chaque requête → le JS injecté ne peut pas le deviner.

Un site malveillant fait faire une requête à ton site au nom de la victime connectée.

L’utilisateur est connecté à banque.com. Il visite mauvais-site.com qui contient :

<form action="https://banque.com/transfer" method="POST">
<input type="hidden" name="to" value="hacker" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>document.forms[0].submit();</script>

Le navigateur envoie automatiquement le cookie de banque.com → la banque exécute le transfert.

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax

SameSite=Lax = le cookie n’est PAS envoyé sur des POST cross-site. Protection automatique dans tous les navigateurs récents.

Le serveur génère un token unique par session, embedded dans les formulaires. Le serveur le vérifie sur chaque POST.

Avec Next.js (Server Actions) : protection CSRF native via le mécanisme de signature.

Avec Express / Hono : libs comme csurf, ou utiliser le pattern Double-Submit Cookie.

Pour les API : ajouter un header X-Requested-With: XMLHttpRequest. Les requêtes cross-origin ne peuvent pas ajouter de headers custom (sauf preflight CORS).

Si tu utilises :

  • Cookies HttpOnly Secure SameSite=Lax + frameworks modernes (Next.js, Laravel, Symfony) → CSRF géré.
  • JWT en Authorization Bearer (jamais cookie) → pas de CSRF (pas envoyé auto par le navigateur).

L’attaquant met ton site dans une iframe invisible et fait cliquer la victime à des endroits piégés.

X-Frame-Options: DENY

Ou (plus moderne, plus précis) :

Content-Security-Policy: frame-ancestors 'none';

Le navigateur refuse de charger ton site dans une iframe.

# Forcer HTTPS pour 1 an, incluant sous-domaines, soumis pour preload
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Empêche l'iframe (clickjacking)
X-Frame-Options: DENY
# Désactive le sniffing de type MIME
X-Content-Type-Options: nosniff
# CSP — voir plus haut
Content-Security-Policy: ...
# Référer leak control
Referrer-Policy: strict-origin-when-cross-origin
# Permissions modernes (caméra, micro, géoloc…)
Permissions-Policy: camera=(), microphone=(), geolocation=()
# CORS (côté API)
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

securityheaders.com → tape ton URL, obtiens un score A+ à F. Cible A minimum en prod.

Si tu charges un script depuis un CDN tiers, vérifie le hash :

<script
src="https://cdn.example.com/lib.js"
integrity="sha384-Xxx..."
crossorigin="anonymous">
</script>

Si le CDN est compromis et le fichier modifié, le navigateur refuse de l’exécuter.

integrity est calculé via openssl dgst -sha384 -binary lib.js | openssl base64 -A.

Une page HTTPS qui charge des ressources HTTP est bloquée par défaut depuis 2020+ pour les scripts/iframes (active mixed content).

Détection : DevTools console → message en rouge.

Migration en bloc :

Content-Security-Policy: upgrade-insecure-requests
StorageAccessible JS ?Envoyé serveur ?Conseil
Cookie HttpOnly Secure SameSite=LaxStandard pour sessions/JWT
localStorageOK pour préférences UI, PAS pour tokens
sessionStorageMême règle que localStorage
IndexedDBPour gros volumes data offline

Règle absolue : un token de session/auth dans localStorage → vol par XSS = accès complet au compte.

Tu affiches `<div>{userBio}</div>` dans React. userBio vient de la DB. Risque XSS ?
Tu stockes un JWT dans localStorage côté client. Risque ?
Tu mets `Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'`. Avis ?

Suite : 12.3 — Sécurité backend — injections, IDOR, secrets, rate-limit.