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 |
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: nonesans 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#
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)")
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}
Résumé#
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.
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.
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.
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.
L’algorithme
noneet la confusionRS256→HS256sont des vulnérabilités d’implémentation JWT : utiliser des bibliothèques maintenues qui interdisent explicitementalg: noneet valident l’algorithme attendu avant la vérification.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.
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.