Le geste de l'attaquant sur une API, du repérage au contournement, avec des démonstrations qu'il faut relire avant de les croire.
Pourquoi ce cours
Vous avez écrit une API (une porte d'entrée pour qu'un programme parle à votre serveur) et vos tests passent. Chaque adresse de cette porte, comme /commandes, s'appelle une route (ou point d'accès). La question qui reste : que ferait quelqu'un qui cherche à la casser ? Ce cours répond par le geste. Un test d'intrusion (pentest : attaquer son propre système, avec autorisation, pour trouver les failles avant les autres) se déroule étape par étape sur une cible d'entraînement.
La cible s'appelle crAPI, une API volontairement vulnérable. Les outils sont ceux du métier : Burp Suite (un proxy qui intercepte et rejoue les requêtes), Hydra (essaie des mots de passe en rafale), Radamsa (fabrique des entrées déformées). Le fil tient en une idée : l'attaque marche quand le serveur se contente de ce que le client affirme. Le cours le dit en toutes lettres : c'est la citation de l'idée 1.
Quelques précisions avant d'entrer. C'est un cours vidéo transcrit, pas un livre. Je n'en ai lu en entier que les leçons où l'attaque est neuve pour cette bibliothèque : 21 sur 69. Les cibles sont des laboratoires, donc les attaques réussissent plus facilement qu'ailleurs. Plusieurs démonstrations sont fausses ou inutilisables, et je dis lesquelles. Enfin cette bibliothèque couvre déjà la défense, dans la fiche Defending APIs et l'article BOLA : le test A/B en Go et PHP.
Les six idées forment trois familles. Ce que l'API croit sur parole : idées 1 et 5. Ce qu'elle laisse échapper : idées 2 et 3. Ce qu'elle croit pouvoir arrêter ou copier : idées 4 et 6.
Les idées qui restent
1Un rôle lu dans l'en-tête est un rôle offert
Pour répondre à une requête, le serveur doit savoir deux choses : qui demande, et ce qu'il a le droit de faire. L'en-tête d'une requête (une ligne de métadonnées, comme Authorization) est écrit par le client. Tout ce qui s'y trouve peut donc être inventé.
Le cours montre des serveurs Flask (un petit framework web Python) qui oublient cette règle. Je retiens deux de ses trois scénarios. Le premier suit le RBAC (contrôle d'accès par rôle : chaque utilisateur a un rôle, et le rôle ouvre des portes). Le code lit le rôle dans l'en-tête de la requête, sans plus : « Le code récupère simplement le rôle fourni par le client dans les en-têtes, sans vérifier si cette affirmation est légitime » (leçon 29). Traduction : l'attaquant écrit admin et la porte s'ouvre.
Le deuxième suit l'ABAC (contrôle d'accès par attributs : le droit dépend du type de compte, du montant, de l'heure). Un compte « standard » est limité à 1 000 par transaction, mais le type de compte vient de la requête : l'attaquant se déclare d'un autre type et dépasse la limite. Même verdict : « le code de validation se contente de ce que le client affirme » (leçon 29).
Le cours traite à part la BOLA (Broken Object Level Authorization : accéder à l'objet d'un autre) : le code se fie à un identifiant fourni par l'utilisateur. Il ne vérifie pas que cet utilisateur a le droit de le demander. Sur une route GET /user?id=, changer le numéro suffit (leçon 29). Son cousin, la BFLA (Broken Function Level Authorization), consiste à appeler une fonction réservée aux administrateurs : le cours montre une route /admin/create_user que n'importe quel utilisateur peut appeler.
La conclusion de l'auteur tient en une phrase : « Tous les mécanismes d'autorisation cités sont solides, mais la façon dont ils ont été implémentés sur un point d'accès peut les rendre inutiles » (leçon 29).
Un JWT (jeton signé qui prouve l'identité : il porte son rôle et sa date d'expiration) est le contrepoint. Il voyage lui aussi dans un en-tête, Authorization, mais le client ne peut pas réécrire son rôle sans casser la signature. Le cours le teste de deux façons. Première façon, un scanner de jetons (jwt_tool, un outil qui tente des attaques connues contre un jeton) ne trouve rien : « Aucune vulnérabilité trouvée » (leçon 24).
Deuxième façon, la confusion de clés. Le serveur signe ses jetons avec une clé privée, que lui seul possède, et publie la clé publique correspondante, que tout le monde peut lire. L'attaque signe un faux jeton avec cette clé publique, en espérant que le serveur la prenne pour un simple mot de passe de signature. Sur la route testée, le serveur répond « Invalid JWT Token ». Sur une autre route, le jeton forgé est accepté, mais le serveur renvoie le rôle d'origine, user, pas admin. Le cours conclut : « Cela signifie probablement que l'implémentation JWT de crAPI n'est pas vulnérable à la confusion de clés » (leçon 24). Un rôle signé ne se réécrit pas, alors qu'un rôle lu dans l'en-tête s'offre.
Cas concret, de mon cru. En PHP, la différence tient en deux lignes.
// ✗ le client choisit son rôle
$role = $_SERVER['HTTP_X_ROLE'] ?? 'user';
// ✓ le serveur le lit dans sa propre session
$role = $session->user()->role;
Même règle contre la BOLA : on filtre l'objet par son propriétaire, WHERE id = :id AND user_id = :moi (:moi est l'utilisateur connecté), jamais WHERE id = :id seul.
2Avant de toucher l'API, l'attaquant lit vos dépôts
L'attaque ne commence pas par l'API. Elle commence par la recherche de ce que vous avez laissé traîner. Le cours appelle cela l'OSINT (renseignement de sources ouvertes : collecter des informations publiques) et en donne la définition : « une série de techniques pour collecter et analyser des informations disponibles publiquement » (leçon 18).
Les outils sont simples. Shodan est un moteur de recherche des machines connectées à Internet : la requête content-type:application/json ramène les serveurs qui répondent en JSON, le format de la plupart des API. Un dork est une requête de recherche détournée pour trouver ce qui n'aurait pas dû être public, comme intitle:"Index of /api/", qui liste des répertoires d'API ouverts. Une cible typique est le fichier de secrets poussé par erreur : « Un développeur distrait a pu les pousser dans un dépôt public » (leçon 18).
Vient ensuite le compte par défaut. Hydra, lancé contre la page de connexion de crAPI avec une liste d'identifiants et de mots de passe courants, trouve le couple admin@example.com et Admin!123. L'origine est banale : « le développeur oublie de supprimer une paire d'identifiants utilisée uniquement pour les tests » (leçon 25). La variante s'appelle la pulvérisation de mots de passe : essayer un seul mot de passe, ou très peu, contre beaucoup de comptes (leçon 26).
Le cours montre aussi une impasse. Avec le seul identifiant admin, Hydra annonce « 0 valid password found ». L'auteur passe alors à un fichier d'identifiants, sans expliquer l'échec : « Nous n'avons rien trouvé pour l'instant » (leçon 25).
Pour le défenseur, le journal est un indice. Chaque requête porte un en-tête User-Agent qui annonce le logiciel client, et Hydra s'y signe Mozilla/4.0 (Hydra) (leçon 25). Autre indice : le volume. Des milliers de requêtes se mesurent, et une protection qui limite le nombre de requêtes par client peut les bloquer (l'idée 4 la détaille). Pour y échapper, le cours conseille de lancer plusieurs copies d'Hydra depuis des adresses différentes (leçon 25).
Pratique, hors cours. Hydra peut annoncer un autre nom dans son User-Agent, par un simple réglage. Ce nom reste donc un indice, pas une défense. Les deux gestes qui comptent sont gratuits : chercher soi-même .env, .yml et clés dans ses dépôts avant qu'un autre le fasse, et supprimer les comptes de test avant la mise en ligne.
3Une erreur bavarde est un plan du bâtiment
Quand une requête fait planter le serveur, que renvoie-t-il ? Beaucoup d'API renvoient l'exception telle quelle. Pour l'attaquant, c'est une carte : le langage, le framework, les noms de fonctions.
Le fuzzing consiste à envoyer en masse des entrées déformées pour provoquer des erreurs. Le cours utilise Radamsa pour fabriquer 1 000 fichiers CSV abîmés. Un petit script Bash les envoie à /api/upload/csv et rejoue tout ce qui ne répond pas 200. Résultat honnête : 2 erreurs sur 1 000 fichiers, puis 41 au total avec 5 000. Les erreurs montrées sont des erreurs 500 qui crachent le texte de l'exception Python ('utf-8' codec can't decode byte). Le cours écrit « 2 % » pour 2 sur 1 000 : c'est 0,2 % (leçon 41). Le rendement est maigre, et l'auteur l'assume : « vous devez être patient avec le fuzzing » (leçon 41).
La leçon suivante liste ce que les erreurs révèlent. Une page d'erreur donne la version de .NET (la plateforme web de Microsoft). Un message nomme Flask et Werkzeug (la bibliothèque web sous Flask). Une trace de pile (la liste des appels au moment du plantage) donne des noms de fonctions et des numéros de ligne. Chaque détail réduit le champ de recherche d'une attaque ciblée.
Le conseil de défense est court et bon : « Des messages d'erreur génériques, avec un minimum de détails techniques, sont un bon premier pas » (leçon 42).
Le même conseil traduit en PHP par moi. L'erreur bavarde se corrige à un seul endroit, le gestionnaire d'exceptions, que PHP appelle quand une exception n'est attrapée nulle part. Le détail va dans le journal du serveur, le client reçoit un message neutre.
set_exception_handler(function (Throwable $e) {
error_log((string) $e); // le détail reste chez nous
http_response_code(500);
echo json_encode(['error' => 'internal error']); // le client n'apprend rien
});
4Le contournement de la limite de débit qui ne contournait rien
Une limitation de débit (rate limit) est un portier qui compte les requêtes de chaque client et refuse celles qui dépassent. Pour reconnaître un client, il lit son adresse IP (son numéro sur le réseau, que le serveur voit à chaque connexion). Le cours en donne l'image du seau percé : « un seau avec un petit trou au fond, qui laisse s'écouler un débit contrôlé » (leçon 49). Les requêtes remplissent le seau, le trou fixe la vitesse de sortie, et ce qui déborde est refusé.
Le laboratoire place NGINX (un serveur web qui sert ici de portier) devant Mockoon (un faux serveur d'API). La règle : une requête par seconde, avec un burst de cinq (le nombre de requêtes d'avance qu'on tolère). L'outil ab (Apache Bench, un générateur de charge) envoie 100 requêtes, 10 à la fois : 94 échouent avec un code 503. Le portier travaille, et l'auteur le constate : « Eh bien, nous pouvons attester que notre mécanisme de limitation fait son travail » (leçon 49).
La leçon suivante annonce le contournement : « Pour contourner une telle restriction, on applique couramment une rotation de l'adresse IP source » (leçon 50). Le script simule cinq utilisateurs, chacun avec sa propre adresse, et une seconde d'attente entre deux requêtes. Résultat : les 50 requêtes passent.
Or le script n'envoie aucune des adresses simulées. La variable simulated_ip = f"10.0.0.{user_id+1}" n'apparaît que dans un print. La requête part avec requests.get(url), sans en-tête, sans proxy, sans adresse source. Les cinq « utilisateurs » ont la même adresse : le serveur lit celle de la connexion, et rien dans la requête ne la change. Le script envoie une requête à la fois, au rythme de la limite : une par seconde. Il respecte la limite, il ne la contourne pas. L'auteur attribue pourtant le succès à la pause entre les requêtes : « Le point clé est le délai que nous mettons entre chaque requête » (leçon 50). Son résumé conclut pourtant que les délais et le changement d'adresse ont « contourné avec succès » la limitation (leçon 50).
Précision : j'ai lu ce code, je ne l'ai pas rejoué.
J'ai testé la règle du cours, pas son script : avec NGINX 1.29 et la même règle (rate=1r/s, burst=5), huit requêtes envoyées l'une après l'autre, sans aucune pause, passent toutes. Sans nodelay, NGINX met en file au lieu de refuser. Les refus du cours viennent d'envois simultanés (ab -c 10, Burp Intruder). Ce que j'en déduis, le cours s'arrête avant. Le cours dit que NGINX compte par adresse source ($binary_remote_addr). Faire varier la vraie adresse aurait donc changé le compte des requêtes. Un en-tête X-Forwarded-For, que le client écrit lui-même, n'aurait rien changé, sauf réglage particulier. Pour se défendre, limiter par compte ou par clé d'API en plus de l'adresse : la rotation d'adresses est le contournement évident.
5Les failles de logique se trouvent à la main
Un scanner trouve ce qui ressemble à une faille connue. Il ignore la logique métier, c'est-à-dire les règles propres à l'application : un code de remise ne devrait pas valoir 100 %. Le cours consacre six leçons à ces abus, avec cette conclusion : « Dévoiler les failles de logique métier repose sur des tests manuels » (leçon 64).
Premier geste, le bourrage d'identifiants (credential stuffing) : rejouer des couples identifiant et mot de passe réels, volés ailleurs, en pariant que les gens réutilisent leurs mots de passe. À la différence de la pulvérisation de l'idée 2, chaque essai est un vrai couple. L'outil s'appelle OpenBullet2 : il rejoue un fichier de cinq lignes utilisateur:mot_de_passe contre la page de connexion d'une petite API Flask. Cette API limite à cinq échecs par minute et par nom d'utilisateur. Le but de la démonstration : cette limite ne gêne pas une attaque qui change de nom à chaque essai, puisque chaque nom n'essuie qu'un échec au plus (leçon 59). L'auteur avoue avoir « perdu un temps raisonnable à comprendre pourquoi (ses) attaques ne marchaient pas » (leçon 60). Le correctif passe par un fichier de réglage d'OpenBullet2, et l'auteur ne sait pas dire si c'était un bogue de sa version (leçon 60).
Deuxième geste, la falsification de paramètres : l'attaque vise « la confiance que le point d'accès accorde aux paramètres fournis dans les requêtes » (leçon 63). Sur une petite API de démonstration, le cours fait passer Alice, un compte fictif, de user à admin. Une transaction passe de pending (en attente) à completed (terminée). Chaque requête porte la nouvelle valeur, et aussi le mot de passe d'administration (admin_secret) que le serveur exige.
Troisième geste, la logique métier proprement dite, sur une petite boutique de démonstration. Un code de remise inventé n'est pas appliqué. L'attaquant appelle alors la route d'administration, avec le mot de passe d'administration écrit en dur dans le code. Il y crée un code à 100 %. Il repasse ensuite commande avec ce code, et cette fois la commande passe.
Ces deux démonstrations prouvent peu, car elles supposent déjà de connaître le mot de passe d'administration. L'auteur le concède : il faudrait l'obtenir « par ingénierie sociale (convaincre quelqu'un de le donner), épuisement de ressources ou fuite de données » (leçon 63). Le geste reste utile, la preuve ne l'est pas.
6Le code « sécurisé » du cours ne protège pas toujours
La fin du cours donne des paires « mauvais / bon » en Python, Java, JavaScript et Go : hachage de mots de passe, requêtes paramétrées, gestion d'erreurs, chiffrement. Sa conclusion : « Ne réinventez jamais la roue » (leçon 68) et « Aucune technique ou principe ne protégera à lui seul toute l'API » (leçon 68).
Seulement, plusieurs de ces extraits sont faux. J'en ai vérifié deux en les exécutant.
Le premier est en Go. L'exemple « fort » écrit la clé directement dans le code (« en dur ») : []byte("strongkey12345678901234567890"). Elle mesure 29 octets. Or AES (l'algorithme de chiffrement standard) n'accepte que 16, 24 ou 32 octets. Lancée dans Go 1.26.10, la fonction aes.NewCipher répond crypto/aes: invalid key size 29, et le panic du même extrait (l'arrêt brutal du programme Go) se déclenche. L'exemple « faible » du même cours, une clé de 15 octets, échoue pareil.
Le second est en Python, avec bcrypt (un algorithme de hachage de mots de passe). Le code calcule hashpw(mot_de_passe, gensalt()), puis passe ce haché à une fonction authenticate que le cours ne montre pas. Or bcrypt fabrique un sel neuf à chaque appel : une chaîne aléatoire mêlée au mot de passe pour que deux mêmes mots de passe donnent deux hachés différents. Mesuré : deux appels sur le même mot de passe donnent deux hachés différents, donc ce haché ne peut jamais être égal à un haché déjà stocké. Pour vérifier un mot de passe, il fallait checkpw, qui relit le sel dans le haché stocké.
J'ai lu trois autres défauts dans le code du cours, sans les exécuter. Le « chiffrement » GraphQL est un HMAC (une empreinte à sens unique, impossible à déchiffrer). La clé Fernet (une clé de chiffrement) est régénérée à chaque exécution. En Java, le vecteur d'initialisation iv (une valeur aléatoire que le chiffrement exige) n'est jamais défini. La leçon contredit donc sa propre morale : elle recommande les bibliothèques éprouvées, puis les utilise de travers.
Mesuré chez moi. Les deux résultats ci-dessus viennent de petits programmes que j'ai lancés, avec Go 1.26.10 et la bibliothèque bcrypt de Python 3.12. Le cours ne signale aucun de ces défauts : checkpw, les longueurs de clé valides d'AES et le vecteur d'initialisation ont 0 occurrence sur les 69 leçons extraites.
Mon avis, honnêtement
Un cours qui montre bien l'attaque et mal ses propres preuves. Sa force : il montre aussi les échecs sans maquillage. Le scanner de jetons ne trouve rien. Hydra, avec le seul identifiant admin, ne trouve rien. Un code de remise inventé n'est pas appliqué. On apprend le vrai rythme d'un test, plein d'impasses.
Deux réserves. D'abord, plusieurs démonstrations ne prouvent pas ce qu'elles annoncent, dont la rotation d'adresses de l'idée 4. Une partie du code sécurisé de l'idée 6 est faux. Ne rien copier sans le rejouer. Ensuite, les cibles sont des laboratoires, et deux démonstrations de l'idée 5 partent du mot de passe d'administration déjà connu. On en retient des gestes, pas une méthode pour une vraie API.
Mon conseil : si vous défendez une API, lisez les idées 1, 3 et 4 et sautez les leçons où l'on installe les outils. Si vous n'avez le temps que pour une fiche, prenez Defending APIs. Je suis dev web, pas pentesteur : ce qui précède dit ce que le cours m'a appris, pas ce qu'un professionnel de l'attaque en penserait.
Odilon
Toujours valable en 2026 ?
Oui pour les gestes. Le cours cite l'édition 2023 de l'OWASP API Top 10, la liste de référence des risques d'API (leçon 66). Il range la BOLA parmi les deux menaces les plus dangereuses (leçon 8).
Ce qui vieillit : les syntaxes précises des filtres de recherche Shodan et GitHub, que je n'ai pas revérifiées. Et crypto.createCipher en JavaScript n'est plus recommandé.
À qui s'adresse-t-il ?
Lisez-le si
- vous développez une API en PHP ou en Go et vous n'avez jamais vu un test d'intrusion se dérouler
- vous voulez comprendre pourquoi un en-tête ou un identifiant venu du client ne se croit jamais
- vous cherchez des gestes de test à rejouer sur votre propre API, avec accord, en laboratoire
- vous avez une demi-journée et vous aimez voir les impasses autant que les réussites
Passez si
- vous voulez un guide de défense complet : prenez Defending APIs
- vous visez un métier de pentesteur : le niveau est celui d'une introduction, sur des cibles de laboratoire
- vous comptez copier les extraits de code du cours dans votre projet
Ce cours vidéo fait partie d'ExpertEdge, l'abonnement de Packt aux livres et cours vidéo techniques. Le code ODILON50 donne 50 % de remise sur l'abonnement annuel : le cours sur ExpertEdge. Lien affilié : je touche une commission si vous vous abonnez avec ce code, et mes notes n'en dépendent pas.
Pour aller plus loin
- La fiche Defending APIs : le même sujet vu du côté de la défense.
- L'article BOLA : le test A/B en Go et PHP : prouver qu'un correctif d'accès à l'objet tient.
- Les cours Contrôle d'accès défaillant et Penser comme un attaquant.
Fiche fondée sur : 21 leçons lues en entier sur les 69 extraites (77 annoncées, quiz exclus), plus deux passages repérés par recherche (leçons 8 et 66). Les passages « Ma lecture » sont de moi, pas du cours.
Commentaires (0)