12. Sécurité des APIs#

Introduction#

Les APIs — REST, GraphQL, gRPC — sont devenues l’épine dorsale des architectures modernes. Elles exposent directement la logique métier et les données sans la couche de présentation HTML qui protégeait partiellement les applications web traditionnelles. L’OWASP API Security Top 10 2023 identifie les catégories de vulnérabilités spécifiques aux APIs, distinctes du Top 10 Web classique.


OWASP API Security Top 10 — 2023#

Rang

Identifiant

Nom

1

API1:2023

Broken Object Level Authorization (BOLA)

2

API2:2023

Broken Authentication

3

API3:2023

Broken Object Property Level Authorization

4

API4:2023

Unrestricted Resource Consumption

5

API5:2023

Broken Function Level Authorization

6

API6:2023

Unrestricted Access to Sensitive Business Flows

7

API7:2023

Server Side Request Forgery

8

API8:2023

Security Misconfiguration

9

API9:2023

Improper Inventory Management

10

API10:2023

Unsafe Consumption of APIs

API1 — BOLA (Broken Object Level Authorization)#

Le BOLA est la vulnérabilité la plus répandue dans les APIs. Chaque endpoint qui reçoit un identifiant d’objet doit vérifier que l’utilisateur authentifié est propriétaire de cet objet.

Différence BOLA vs IDOR :

  • IDOR est le terme historique (OWASP Web Top 10), souvent associé aux applications web.

  • BOLA est le terme spécifique aux APIs, plus précis : il désigne l’absence de contrôle d’autorisation au niveau de l’objet dans un endpoint API.

API3 — Broken Object Property Level Authorization#

Même si l’utilisateur est autorisé à accéder à l’objet, il peut ne pas être autorisé à lire ou modifier certaines propriétés. Un endpoint PATCH /users/123 qui accepte {"role": "admin"} sans vérification est vulnérable.

API4 — Unrestricted Resource Consumption#

Sans limitation, un client peut soumettre des milliers de requêtes (DoS), télécharger des payloads démesurés, déclencher des opérations computationnellement coûteuses (conversion vidéo, OCR).


BOLA — Exploitation sur IDs séquentiels et UUID#

IDs séquentiels#

Les IDs auto-incrémentés (1, 2, 3…) sont trivialement énumérables. Un attaquant peut extraire toutes les ressources en incrémentant l’ID.

IDs UUIDv4#

Les UUID v4 sont aléatoires sur 122 bits — l’énumération est infaisable. Mais ils ne remplacent pas le contrôle d’autorisation : si l’UUID est prévisible (mauvais PRNG) ou accessible via une autre fuite, l’attaque reste possible.

Sécurité par obscurité ≠ sécurité réelle

Remplacer les IDs séquentiels par des UUIDs réduit la surface d’attaque mais ne constitue pas un contrôle d’autorisation. La vérification de propriété côté serveur reste obligatoire, même avec des UUIDs.


Rate Limiting et Throttling#

Trois algorithmes principaux#

Fixed Window :

  • Compteur réinitialisé à intervalles fixes (ex. : 100 req/min).

  • Vulnérable au burst en fin/début de fenêtre : jusqu’à 200 requêtes en quelques secondes à la jonction.

  • Simple à implémenter.

Sliding Window :

  • La fenêtre glisse avec le temps. Le compteur tient compte des requêtes des N dernières secondes, pas d’une fenêtre fixe.

  • Élimine le burst de jonction.

  • Plus coûteux en mémoire (Redis sorted sets).

Token Bucket :

  • Un « seau » de jetons se remplit à un débit constant (rate). Chaque requête consomme un jeton.

  • Permet les bursts légitimes (seau plein) tout en limitant le débit moyen.

  • Algorithme le plus flexible, utilisé par AWS API Gateway, Kong.

Configuration Kong / AWS API Gateway#

# Kong — plugin rate-limiting
plugins:
  - name: rate-limiting
    config:
      minute: 100
      hour: 2000
      policy: sliding
      hide_client_headers: false
      error_message: "Trop de requêtes. Réessayez dans un instant."
// AWS API Gateway — Usage Plan
{
  "throttle": {
    "rateLimit": 100,
    "burstLimit": 200
  },
  "quota": {
    "limit": 10000,
    "period": "DAY"
  }
}

GraphQL Security#

Introspection — divulgation du schéma#

L’introspection GraphQL permet à tout client de découvrir l’intégralité du schéma (types, champs, mutations, queries). En production, elle expose la structure interne de l’API.

# Requête d'introspection (à désactiver en production)
{ __schema { types { name fields { name type { name } } } } }

Protection : désactiver l’introspection en production ou la restreindre aux IPs internes.

Query Depth et Complexity#

GraphQL permet des requêtes imbriquées arbitrairement profondes (batching attacks) :

# Requête malveillante : profondeur excessive
{
  user(id: "1") {
    friends {
      friends {
        friends {
          friends { id email }
        }
      }
    }
  }
}

Mitigations :

  • Limite de profondeur (max depth = 5-10 niveaux).

  • Limite de complexité : chaque champ a un coût, la requête est rejetée si le total dépasse un seuil.

  • Query timeout : interruption des requêtes dépassant une durée limite.

  • Persisted Queries : seules les requêtes préenregistrées sont acceptées.

Batching Attacks#

GraphQL autorise l’envoi de plusieurs requêtes dans un seul appel HTTP (array batching) ou via les alias. Cela permet de contourner les rate limiters qui comptent les requêtes HTTP.


gRPC Security#

TLS obligatoire#

gRPC utilise HTTP/2 comme transport. Sans TLS, les métadonnées d’authentification (headers de metadata) transitent en clair.

// Go — connexion gRPC avec TLS obligatoire
creds, err := credentials.NewClientTLSFromFile("cert.pem", "")
conn, err := grpc.Dial("api.alkimya.fr:443", grpc.WithTransportCredentials(creds))

Interceptors d’autorisation#

Les interceptors gRPC (équivalents des middlewares HTTP) permettent de centraliser l’authentification et l’autorisation :

func authInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    token := extractToken(ctx)
    if !validateJWT(token) {
        return nil, status.Error(codes.Unauthenticated, "token invalide")
    }
    return handler(ctx, req)
}

gRPC Reflection — risque en production#

Le service de réflexion gRPC permet aux clients de découvrir les services et méthodes disponibles (équivalent de l’introspection GraphQL). À désactiver en production.


JWT Security#

Structure d’un JWT#

Un JWT (JSON Web Token) est composé de trois parties encodées en Base64url, séparées par des points :

header.payload.signature
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0In0.signature_bytes

Vulnérabilités JWT courantes#

Algorithme none :

{ "alg": "none", "typ": "JWT" }

Certaines implémentations acceptent un JWT sans signature si alg est none. La signature est vide. Le payload peut être falsifié librement.

Secret faible : Un secret HMAC court ou prévisible (secret, password, jwt) est craquable par dictionnaire en secondes avec hashcat ou jwt_tool.

Confusion RS256 → HS256 : Si le serveur accepte les deux algorithmes, l’attaquant peut forger un token en signant avec HS256 en utilisant la clé publique RSA comme secret HMAC. La bibliothèque côté serveur vérifie avec la clé publique (connue) → valide.

Absence d’expiration (exp manquant) : Un token sans expiration est valide indéfiniment, même après révocation du compte.

JWT mal formés#

Exemples de JWT vulnérables (à des fins éducatives)

  • alg: none sans signature : eyJhbGciOiJub25lIn0.eyJyb2xlIjoiYWRtaW4ifQ.

  • Secret faible : signature HMAC-SHA256 avec le secret "secret"

  • Payload expiré ignoré : "exp": 1700000000 (2023) accepté sans vérification


API Gateway comme point d’application#

Un API Gateway centralise les contrôles de sécurité transversaux :

Fonction

Description

Authentification

Validation JWT, API Keys, OAuth 2.0

Rate limiting

Token bucket, sliding window par IP/utilisateur

WAF intégré

Détection d’injections, payloads malformés

Logging

Audit de toutes les requêtes et réponses

TLS termination

Gestion des certificats centralisée

Circuit breaker

Protection des backends contre les surcharges

Défense en profondeur — ne pas tout déléguer au gateway

L’API Gateway est une couche de défense supplémentaire, pas un substitut aux contrôles d’autorisation dans le code applicatif. Les vérifications BOLA et les contrôles de propriété des objets doivent rester dans la couche métier.


Cellules Python exécutables#

Hide code cell source

import hmac
import hashlib
import base64
import json
import time
import math
import random
import collections
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

Simulation BOLA — API mock en mémoire#

# ──────────────────────────────────────────────────────────
# API mock : base de données en mémoire
# ──────────────────────────────────────────────────────────

COMMANDES_DB = {
    "cmd-001": {"id": "cmd-001", "user_id": "alice", "montant": 149.99, "article": "Laptop"},
    "cmd-002": {"id": "cmd-002", "user_id": "alice", "montant":  29.99, "article": "Souris"},
    "cmd-003": {"id": "cmd-003", "user_id": "bob",   "montant": 899.00, "article": "Moniteur"},
    "cmd-004": {"id": "cmd-004", "user_id": "bob",   "montant":  49.99, "article": "Clavier"},
    "cmd-005": {"id": "cmd-005", "user_id": "carol", "montant": 299.00, "article": "Webcam"},
}

# ──────────────────────────────────────────────────────────
# Version VULNÉRABLE : aucune vérification de propriété
# ──────────────────────────────────────────────────────────
def get_commande_vulnerable(commande_id, user_authentifie):
    """Retourne la commande sans vérifier l'appartenance."""
    commande = COMMANDES_DB.get(commande_id)
    if commande is None:
        return None, "Commande introuvable"
    # BOLA : on ne vérifie pas que user_authentifie == commande["user_id"]
    return commande, "OK"

# ──────────────────────────────────────────────────────────
# Version SÉCURISÉE : vérification de propriété obligatoire
# ──────────────────────────────────────────────────────────
def get_commande_securise(commande_id, user_authentifie):
    """Retourne la commande uniquement si elle appartient à l'utilisateur."""
    commande = COMMANDES_DB.get(commande_id)
    if commande is None:
        return None, "Commande introuvable"
    if commande["user_id"] != user_authentifie:
        return None, "403 Forbidden — accès refusé (BOLA corrigé)"
    return commande, "OK"

# ──────────────────────────────────────────────────────────
# Scénarios de test
# ──────────────────────────────────────────────────────────
print("=" * 65)
print("API VULNÉRABLE — Sans contrôle d'autorisation objet")
print("=" * 65)

# Alice accède à sa propre commande
res, msg = get_commande_vulnerable("cmd-001", "alice")
print(f"[alice] GET /commandes/cmd-001 → {msg} | {res}")

# Alice accède à la commande de Bob (BOLA)
res, msg = get_commande_vulnerable("cmd-003", "alice")
print(f"[alice] GET /commandes/cmd-003 → {msg} | {res}")

# Énumération complète par Alice
print("\n[alice] Énumération de toutes les commandes (cmd-001 à cmd-005) :")
for cmd_id in COMMANDES_DB:
    res, msg = get_commande_vulnerable(cmd_id, "alice")
    owner = res["user_id"] if res else "—"
    print(f"  {cmd_id} → owner={owner} | {msg}")

print("\n" + "=" * 65)
print("API SÉCURISÉE — Avec vérification de propriété")
print("=" * 65)

res, msg = get_commande_securise("cmd-001", "alice")
print(f"[alice] GET /commandes/cmd-001 → {msg}")

res, msg = get_commande_securise("cmd-003", "alice")
print(f"[alice] GET /commandes/cmd-003 → {msg}")

print("\n[alice] Tentative d'énumération (version sécurisée) :")
for cmd_id in COMMANDES_DB:
    res, msg = get_commande_securise(cmd_id, "alice")
    if res:
        print(f"  {cmd_id} → AUTORISÉ  | {res}")
    else:
        print(f"  {cmd_id}{msg}")
=================================================================
API VULNÉRABLE — Sans contrôle d'autorisation objet
=================================================================
[alice] GET /commandes/cmd-001 → OK | {'id': 'cmd-001', 'user_id': 'alice', 'montant': 149.99, 'article': 'Laptop'}
[alice] GET /commandes/cmd-003 → OK | {'id': 'cmd-003', 'user_id': 'bob', 'montant': 899.0, 'article': 'Moniteur'}

[alice] Énumération de toutes les commandes (cmd-001 à cmd-005) :
  cmd-001 → owner=alice | OK
  cmd-002 → owner=alice | OK
  cmd-003 → owner=bob | OK
  cmd-004 → owner=bob | OK
  cmd-005 → owner=carol | OK

=================================================================
API SÉCURISÉE — Avec vérification de propriété
=================================================================
[alice] GET /commandes/cmd-001 → OK
[alice] GET /commandes/cmd-003 → 403 Forbidden — accès refusé (BOLA corrigé)

[alice] Tentative d'énumération (version sécurisée) :
  cmd-001 → AUTORISÉ  | {'id': 'cmd-001', 'user_id': 'alice', 'montant': 149.99, 'article': 'Laptop'}
  cmd-002 → AUTORISÉ  | {'id': 'cmd-002', 'user_id': 'alice', 'montant': 29.99, 'article': 'Souris'}
  cmd-003 → 403 Forbidden — accès refusé (BOLA corrigé)
  cmd-004 → 403 Forbidden — accès refusé (BOLA corrigé)
  cmd-005 → 403 Forbidden — accès refusé (BOLA corrigé)

Rate limiting — comparaison Token Bucket vs Sliding Window vs Fixed Window#

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

class FixedWindowLimiter:
    """Rate limiter à fenêtre fixe."""
    def __init__(self, limite, fenetre_s):
        self.limite = limite
        self.fenetre_s = fenetre_s
        self.compteur = 0
        self.debut_fenetre = 0.0

    def autoriser(self, now):
        if now - self.debut_fenetre >= self.fenetre_s:
            self.compteur = 0
            self.debut_fenetre = now
        if self.compteur < self.limite:
            self.compteur += 1
            return True
        return False

class SlidingWindowLimiter:
    """Rate limiter à fenêtre glissante (log exact)."""
    def __init__(self, limite, fenetre_s):
        self.limite = limite
        self.fenetre_s = fenetre_s
        self.timestamps = collections.deque()

    def autoriser(self, now):
        # Supprimer les timestamps hors fenêtre
        while self.timestamps and self.timestamps[0] < now - self.fenetre_s:
            self.timestamps.popleft()
        if len(self.timestamps) < self.limite:
            self.timestamps.append(now)
            return True
        return False

class TokenBucketLimiter:
    """Rate limiter token bucket."""
    def __init__(self, capacite, debit):
        self.capacite = capacite
        self.debit = debit  # jetons par seconde
        self.jetons = capacite
        self.dernier_remplissage = 0.0

    def autoriser(self, now):
        elapsed = now - self.dernier_remplissage
        self.jetons = min(self.capacite, self.jetons + elapsed * self.debit)
        self.dernier_remplissage = now
        if self.jetons >= 1:
            self.jetons -= 1
            return True
        return False

# Paramètres : 10 requêtes par seconde en régime normal
LIMITE = 10
FENETRE = 1.0

fw = FixedWindowLimiter(LIMITE, FENETRE)
sw = SlidingWindowLimiter(LIMITE, FENETRE)
tb = TokenBucketLimiter(capacite=20, debit=LIMITE)  # burst de 20

# Simulation : burst intense au niveau des jonctions de fenêtres
# t=0.9s : 10 req rapides (fin de fenêtre fixe)
# t=1.0s : 10 req rapides (début de nouvelle fenêtre)
# Puis trafic normal

random.seed(42)
duree = 5.0
dt = 0.01

timestamps_requetes = []
for i in range(int(duree / dt)):
    t = i * dt
    # Burst aux jonctions de fenêtres : t ∈ [0.88, 0.92] et [1.88, 1.92]
    if 0.88 <= t <= 0.92 or 1.88 <= t <= 1.92:
        for _ in range(5):
            timestamps_requetes.append(t)
    else:
        if random.random() < 0.12:  # trafic normal ~12 req/s en moyenne
            timestamps_requetes.append(t)

# Évaluation de chaque requête avec chaque algorithme
resultats = {"Fixed Window": [], "Sliding Window": [], "Token Bucket": []}
for t in timestamps_requetes:
    resultats["Fixed Window"].append((t, fw.autoriser(t)))
    resultats["Sliding Window"].append((t, sw.autoriser(t)))
    resultats["Token Bucket"].append((t, tb.autoriser(t)))

# Calcul du débit autorisé par fenêtre de 100ms
fenetres_t = np.arange(0, duree, 0.1)
fig, axes = plt.subplots(3, 1, figsize=(13, 9), sharex=True)
colors = sns.color_palette("muted", 3)

for ax, (algo, res), col in zip(axes, resultats.items(), colors):
    debit_autorise = []
    debit_bloque = []
    for ft in fenetres_t:
        dans_fenetre = [(t, ok) for t, ok in res if ft <= t < ft + 0.1]
        n_ok = sum(1 for _, ok in dans_fenetre if ok)
        n_ko = sum(1 for _, ok in dans_fenetre if not ok)
        debit_autorise.append(n_ok * 10)    # req/s
        debit_bloque.append(n_ko * 10)

    ax.bar(fenetres_t, debit_autorise, width=0.09, color=col, alpha=0.8, label="Autorisées", align="edge")
    ax.bar(fenetres_t, debit_bloque, width=0.09, bottom=debit_autorise,
           color=sns.color_palette("muted")[3], alpha=0.6, label="Bloquées", align="edge")
    ax.axhline(y=LIMITE * 10, color="red", linestyle="--", linewidth=1.5, label=f"Limite ({LIMITE * 10}/s)")
    ax.set_ylabel("Requêtes / s")
    ax.set_title(f"Algorithme : {algo}", fontsize=10, fontweight="bold")
    ax.legend(fontsize=8, loc="upper right")
    ax.set_ylim(0, max(max(debit_autorise), 1) * 1.6)

axes[-1].set_xlabel("Temps (secondes)")
fig.suptitle("Comparaison des algorithmes de rate limiting — réponse aux bursts", fontsize=12, fontweight="bold")
plt.show()

# Statistiques
for algo, res in resultats.items():
    n_ok = sum(1 for _, ok in res if ok)
    n_ko = sum(1 for _, ok in res if not ok)
    print(f"{algo:<20} : {n_ok:3} autorisées, {n_ko:3} bloquées ({100*n_ko/len(res):.1f}% de blocage)")
_images/a8ce0b590f4fd0b820499c21199df551dff6de36000040e39ecf33df99c24aa1.png
Fixed Window         :  49 autorisées,  59 bloquées (54.6% de blocage)
Sliding Window       :  46 autorisées,  62 bloquées (57.4% de blocage)
Token Bucket         :  67 autorisées,  41 bloquées (38.0% de blocage)

Exploitation JWT faible et heatmap OWASP API Top 10#

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

# ──────────────────────────────────────────────────────────
# Partie 1 : Exploitation JWT avec secret faible
# ──────────────────────────────────────────────────────────

def b64url_encode(data):
    """Encodage Base64url sans padding."""
    if isinstance(data, str):
        data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

def b64url_decode(s):
    """Décodage Base64url avec padding restauré."""
    s += "=" * (4 - len(s) % 4)
    return base64.urlsafe_b64decode(s)

def creer_jwt(payload, secret):
    """Crée un JWT signé HMAC-SHA256."""
    header = b64url_encode(json.dumps({"alg": "HS256", "typ": "JWT"}))
    body   = b64url_encode(json.dumps(payload))
    msg    = f"{header}.{body}".encode()
    sig    = hmac.new(secret.encode(), msg, hashlib.sha256).digest()
    return f"{header}.{body}.{b64url_encode(sig)}"

def verifier_jwt(token, secret):
    """Vérifie la signature d'un JWT HS256."""
    try:
        parts = token.split(".")
        if len(parts) != 3:
            return False, "Format invalide"
        header_b, payload_b, sig_b = parts
        msg = f"{header_b}.{payload_b}".encode()
        sig_attendue = hmac.new(secret.encode(), msg, hashlib.sha256).digest()
        sig_recue = b64url_decode(sig_b)
        if hmac.compare_digest(sig_attendue, sig_recue):
            payload = json.loads(b64url_decode(payload_b))
            return True, payload
        return False, "Signature invalide"
    except Exception as e:
        return False, str(e)

def decoder_jwt_sans_verification(token):
    """Décode un JWT sans vérifier la signature (risque)."""
    parts = token.split(".")
    header  = json.loads(b64url_decode(parts[0]))
    payload = json.loads(b64url_decode(parts[1]))
    return header, payload

# ──────────────────────────────────────────────────────────
# Scénario : serveur utilise le secret "secret" (faible)
# ──────────────────────────────────────────────────────────
SECRET_SERVEUR = "secret"  # secret faible, dans un dictionnaire de 10 000 mots

# Token légitime pour l'utilisateur Alice (rôle user)
payload_alice = {"sub": "alice", "role": "user", "exp": int(time.time()) + 3600}
token_alice = creer_jwt(payload_alice, SECRET_SERVEUR)

print("=" * 70)
print("SCÉNARIO JWT — Secret faible")
print("=" * 70)
print(f"\nToken JWT légitime d'Alice :\n{token_alice[:60]}...\n")

# Décodage sans vérification (header + payload visibles par n'importe qui)
h, p = decoder_jwt_sans_verification(token_alice)
print(f"Header (décodé sans clé) : {h}")
print(f"Payload (décodé sans clé) : {p}\n")

# Simulation d'une attaque dictionnaire sur le secret
dictionnaire_secrets = ["password", "123456", "admin", "secret", "jwt", "key", "token", "p@ssw0rd"]
print("Attaque dictionnaire sur le secret JWT :")
for candidat in dictionnaire_secrets:
    ok, result = verifier_jwt(token_alice, candidat)
    status = "TROUVÉ ✓" if ok else "raté"
    print(f"  Secret candidat '{candidat:<12}' → {status}")
    if ok:
        print(f"    Secret trouvé ! Payload : {result}")
        # Forger un token administrateur
        payload_forge = {"sub": "alice", "role": "admin", "exp": int(time.time()) + 3600}
        token_forge = creer_jwt(payload_forge, candidat)
        ok2, res2 = verifier_jwt(token_forge, SECRET_SERVEUR)
        print(f"    Token forgé avec rôle admin, vérification serveur : {ok2}{res2}")
        break

# ──────────────────────────────────────────────────────────
# Partie 2 : Heatmap OWASP API Security Top 10 2023
# ──────────────────────────────────────────────────────────
print("\n")
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)

categories_api = [
    "API1 BOLA",
    "API2 Broken Auth",
    "API3 Object Prop Auth",
    "API4 Resource Consumption",
    "API5 Func Level Auth",
    "API6 Business Flow",
    "API7 SSRF",
    "API8 Misconfiguration",
    "API9 Inventory Mgmt",
    "API10 Unsafe Consumption",
]

dimensions_api = ["Fréquence", "Impact", "Exploitabilité"]

# Scores estimés (0-10) basés sur le rapport OWASP API 2023
scores_api = np.array([
    [9.5, 8.5, 9.0],   # API1 BOLA
    [8.0, 9.0, 7.5],   # API2 Broken Auth
    [7.5, 7.5, 7.0],   # API3 Object Property Auth
    [7.0, 7.0, 8.0],   # API4 Resource Consumption
    [7.5, 8.0, 7.0],   # API5 Function Level Auth
    [6.5, 7.5, 7.5],   # API6 Business Flow
    [5.0, 8.5, 6.5],   # API7 SSRF
    [8.5, 6.5, 7.5],   # API8 Misconfiguration
    [6.0, 7.0, 6.0],   # API9 Inventory Management
    [5.5, 6.5, 5.5],   # API10 Unsafe Consumption
])

fig, ax = plt.subplots(figsize=(10, 7))
sns.heatmap(
    scores_api,
    annot=True,
    fmt=".1f",
    cmap="YlOrRd",
    xticklabels=dimensions_api,
    yticklabels=[c.replace(" ", "\n", 1) for c in categories_api],
    linewidths=0.5,
    linecolor="white",
    vmin=0, vmax=10,
    cbar_kws={"label": "Score (0–10)"},
    ax=ax
)
ax.set_title("OWASP API Security Top 10 — 2023\nFréquence × Impact × Exploitabilité",
             fontsize=12, fontweight="bold", pad=15)
ax.set_xlabel("Dimension d'évaluation", labelpad=10)
ax.set_ylabel("Catégorie OWASP API", labelpad=10)
ax.tick_params(axis="y", labelsize=8)
plt.show()
======================================================================
SCÉNARIO JWT — Secret faible
======================================================================

Token JWT légitime d'Alice :
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYWxpY2U...

Header (décodé sans clé) : {'alg': 'HS256', 'typ': 'JWT'}
Payload (décodé sans clé) : {'sub': 'alice', 'role': 'user', 'exp': 1774529496}

Attaque dictionnaire sur le secret JWT :
  Secret candidat 'password    ' → raté
  Secret candidat '123456      ' → raté
  Secret candidat 'admin       ' → raté
  Secret candidat 'secret      ' → TROUVÉ ✓
    Secret trouvé ! Payload : {'sub': 'alice', 'role': 'user', 'exp': 1774529496}
    Token forgé avec rôle admin, vérification serveur : True → {'sub': 'alice', 'role': 'admin', 'exp': 1774529496}
_images/edc5321d29333bb88ec21d5f3f8844e16262f2529d712211b80ddbebc8f21319.png

Résumé#

  1. BOLA (API1) est la vulnérabilité la plus fréquente et la plus impactante des APIs. Chaque endpoint recevant un identifiant d’objet doit vérifier côté serveur que l’utilisateur authentifié est propriétaire de cet objet. Les UUIDs réduisent l’énumération mais ne remplacent pas cette vérification.

  2. Le rate limiting doit adapter son algorithme au cas d’usage : le token bucket est optimal pour les APIs tolérantes aux bursts légitimes, le sliding window pour les APIs nécessitant un débit strict. La protection ne doit pas se limiter au niveau de l’IP — elle doit aussi s’appliquer par compte utilisateur.

  3. GraphQL expose des vecteurs spécifiques : introspection du schéma, requêtes de profondeur arbitraire, batching attacks. Les limites de profondeur et de complexité, combinées à la désactivation de l’introspection en production, atténuent ces risques.

  4. Les JWT avec secrets faibles sont exploitables par attaque dictionnaire en quelques millisecondes. La longueur minimale recommandée pour un secret HMAC-SHA256 est 256 bits aléatoires. Préférer RS256 (asymétrique) pour les architectures où la vérification est distribuée.

  5. L’algorithme none et la confusion RS256→HS256 sont des vulnérabilités d’implémentation JWT : utiliser des bibliothèques maintenues qui interdisent explicitement alg: none et valident l’algorithme attendu avant la vérification.

  6. L’API Gateway centralise les contrôles transversaux (authentification, rate limiting, WAF, logging) mais ne remplace pas les contrôles d’autorisation dans le code métier. La défense en profondeur exige les deux niveaux.

  7. La gestion des inventaires d’APIs (API9) est souvent négligée : des versions anciennes non documentées (/api/v1/, /api/v2/) restent exposées sans les correctifs de sécurité appliqués aux versions récentes. Un catalogue d’APIs à jour et des tests de régression sur toutes les versions sont essentiels.