Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1022 minIntermédiaire

CORS et sécurité des API : OWASP API Top 10, limitation de débit

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • expliquer la politique de même origine et corriger une erreur CORS ;
  • configurer CORS sans ouvrir de faille ;
  • reconnaître les risques de l'OWASP API Security Top 10 ;
  • corriger une faille BOLA et poser une limitation de débit.

6 exercices corrigés1 schéma

CORS (Cross-Origin Resource Sharing) est le mécanisme par lequel une API indique au navigateur quelles pages d'autres origines ont le droit de lire ses réponses ; sans lui, le navigateur bloque ces lectures au nom de la politique de même origine. CORS n'est pas une protection de l'API : celle-ci repose sur le contrôle d'accès à chaque objet, la validation des entrées et la limitation de débit, que l'OWASP résume dans son API Security Top 10. Ce chapitre explique CORS et ses erreurs, puis les dix risques des API et la façon de limiter le débit.

La politique de même origine

Une origine, c'est le triplet schéma + hôte + port. Deux URL ont la même origine si les trois sont identiques ; le chemin ne compte pas.

URL comparée à https://frais.gsb.exampleMême origine ?Pourquoi
https://frais.gsb.example/fiches/12OuiSeul le chemin change
http://frais.gsb.exampleNonSchéma différent (http)
https://api.gsb.exampleNonHôte différent (un sous-domaine est une autre origine)
https://frais.gsb.example:8443NonPort différent (443 par défaut)
http://localhost:5173NonTout est différent

Par défaut, le navigateur applique la politique de même origine (Same-Origin Policy) : un script chargé depuis une origine peut envoyer une requête vers une autre origine, mais ne peut pas lire la réponse. Sans cette règle, n'importe quel site que tu visites pourrait, avec ton navigateur et tes cookies, lire ta messagerie ou ton espace bancaire ouverts dans un autre onglet.

Le front d'AppliFrais est servi par https://frais.gsb.example et appelle l'API à https://api.gsb.example : deux origines différentes. Sans autorisation explicite de l'API, le navigateur refuse au front l'accès aux réponses. C'est là qu'intervient CORS.

CORS : l'API autorise des origines

Le navigateur ajoute à chaque requête vers une autre origine l'en-tête Origin, qui indique d'où vient la page. L'API répond avec Access-Control-Allow-Origin : si la valeur correspond à l'origine de la page (ou vaut *), le navigateur laisse le script lire la réponse ; sinon, il la bloque et affiche une erreur CORS dans la console.

http
GET /v1/fiches HTTP/1.1
Host: api.gsb.example
Origin: https://frais.gsb.example
Authorization: Bearer eyJhbGci…
http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frais.gsb.example
Vary: Origin
Content-Type: application/json; charset=utf-8

{"donnees":[ … ]}

Vary: Origin indique aux caches que la réponse dépend de l'en-tête Origin : sans lui, un cache pourrait resservir à une autre origine une réponse qui ne lui est pas destinée.

Requête simple ou pré-vérification

Pour certaines requêtes, le navigateur demande d'abord la permission à l'API, avant d'envoyer la vraie requête : c'est la requête de pré-vérification (preflight), une requête OPTIONS.

Une requête est dite simple, sans pré-vérification, si elle remplit toutes ces conditions : méthode GET, HEAD ou POST ; seulement des en-têtes ordinaires (Accept, Accept-Language, Content-Language, Content-Type) ; et un Content-Type limité à application/x-www-form-urlencoded, multipart/form-data ou text/plain. Ce sont les requêtes qu'un formulaire HTML pouvait déjà envoyer avant l'existence de CORS.

Les appels d'une API REST ne sont presque jamais simples : un corps en application/json, un en-tête Authorization, ou une méthode PUT, PATCH ou DELETE déclenchent la pré-vérification. Voici celle qu'envoie le navigateur avant que le front clôture la fiche 12, et la réponse de l'API de démonstration :

http
OPTIONS /v1/fiches/12 HTTP/1.1
Host: api.gsb.example
Origin: https://frais.gsb.example
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: authorization, content-type
http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frais.gsb.example
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Vary: Origin

Le navigateur vérifie que l'origine, la méthode PATCH et les en-têtes demandés sont autorisés, puis envoie la vraie requête. Access-Control-Max-Age: 600 lui permet de garder cette autorisation en cache dix minutes, sans refaire de pré-vérification pour chaque appel.

Navigateurpage de frais.gsb.exampleAPIapi.gsb.example1. le script demande un PATCH2. envoyée par le navigateur lui-même :OPTIONS /v1/fiches/12Origin: https://frais.gsb.exampleAccess-Control-Request-Method: PATCH3. 204 No ContentAccess-Control-Allow-Origin: https://frais.gsb.exampleAccess-Control-Allow-Methods: GET, POST, PATCH…4. la vraie requête : PATCH /v1/fiches/12 + jeton + JSON5. 200 OK + Access-Control-Allow-Originle script reçoit la réponseSi l'origine n'est pas autoriséeÀ l'étape 3, l'API ne renvoie pas l'en-tête Access-Control-Allow-Origin attendu.Le navigateur n'envoie pas le PATCH : le script reçoit une erreur réseau,et la console affiche une erreur CORS.
La pré-vérification CORS : le navigateur demande la permission avant d'envoyer la vraie requête
En-tête de réponseRôle
Access-Control-Allow-OriginL'origine autorisée (une seule valeur), ou * pour toutes
Access-Control-Allow-MethodsLes méthodes permises (pré-vérification)
Access-Control-Allow-HeadersLes en-têtes de requête permis (pré-vérification)
Access-Control-Allow-Credentialstrue pour que le script puisse lire la réponse d'une requête envoyée avec les cookies (fetch(…, { credentials: 'include' })) ; interdit avec Access-Control-Allow-Origin: *
Access-Control-Max-AgeDurée de mise en cache de la pré-vérification, en secondes
Access-Control-Expose-HeadersLes en-têtes de réponse que le script peut lire en plus des en-têtes de base (Location, ETag, par exemple)

Configurer CORS correctement

Avec Express, le paquet cors gère tous ces en-têtes et répond aux pré-vérifications :

js
const cors = require('cors');

app.use(cors({
  origin: ['https://frais.gsb.example'],            // liste blanche des origines
  methods: ['GET', 'POST', 'PATCH', 'DELETE'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  exposedHeaders: ['Location', 'ETag'],
  maxAge: 600,
}));

En développement, ajoute l'origine du serveur de développement du front (http://localhost:5173 pour Vite) dans la configuration de développement seulement, ou fais passer les appels par le proxy de développement du front, qui rend l'API « de même origine ».

Les erreurs à ne pas commettre :

  • renvoyer l'origine reçue, quelle qu'elle soit, avec Access-Control-Allow-Credentials: true (cors({ origin: true, credentials: true })) : n'importe quel site peut alors lire les données d'un utilisateur connecté par cookie. C'est une mauvaise configuration de sécurité, classée API8 par l'OWASP (le cas détaillé) ;
  • comparer l'origine avec une expression trop large : un test « l'origine contient gsb.example » accepte https://gsb.example.site-pirate.example ;
  • autoriser l'origine null, envoyée par des fichiers locaux et des pages en bac à sable, que n'importe qui peut produire ;
  • croire que * suffit partout : Access-Control-Allow-Origin: * est acceptable pour une API publique sans authentification par cookie, mais le navigateur l'interdit avec Access-Control-Allow-Credentials: true.

L'OWASP API Security Top 10

L'OWASP (Open Worldwide Application Security Project) publie une liste des dix risques les plus critiques propres aux API. L'édition en vigueur est celle de 2023. Elle diffère du Top 10 des applications web (édition 2025, détaillée dans le cours Sécurité des applications) : les API exposent directement les objets et les fonctions, et leurs failles dominantes sont des failles de contrôle d'accès.

CodeRisqueEn une phraseParade principale
API1:2023Broken Object Level Authorization (BOLA)L'API ne vérifie pas que l'objet demandé appartient à l'appelantVérifier le propriétaire à chaque accès à un objet
API2:2023Broken AuthenticationAuthentification contournable, jetons mal vérifiés, pas de limite sur la connexionverify avec algorithme imposé, expiration courte, limitation des tentatives
API3:2023Broken Object Property Level Authorization (BOPLA)L'API renvoie ou accepte des propriétés qu'elle ne devrait pasChamps renvoyés choisis explicitement ; liste blanche des champs modifiables
API4:2023Unrestricted Resource ConsumptionPas de limite de débit, de taille ou de paginationLimitation de débit, taille maximale des corps, pagination plafonnée
API5:2023Broken Function Level Authorization (BFLA)Une fonction réservée (administration, validation) est accessible à tousContrôle de rôle sur chaque route, refus par défaut
API6:2023Unrestricted Access to Sensitive Business FlowsUn processus métier peut être automatisé à l'excèsLimites métier, détection d'automatisation, vérifications supplémentaires
API7:2023Server Side Request Forgery (SSRF)L'API va chercher une URL fournie par le client sans la contrôlerListe blanche des destinations, pas d'accès au réseau interne
API8:2023Security MisconfigurationRéglages dangereux : CORS trop ouvert, traces d'erreur, HTTPS absentConfiguration durcie et revue, erreurs génériques
API9:2023Improper Inventory ManagementVersions et routes oubliées, non documentées, non protégéesInventaire par OpenAPI, retrait des anciennes versions
API10:2023Unsafe Consumption of APIsConfiance aveugle dans les réponses d'une API tierceValider ce qui vient d'un tiers comme une saisie d'utilisateur

Le chapitre Sécuriser une API REST et une application mobile du cours Sécurité applicative reprend chaque risque avec des exemples de code vulnérable et corrigé. Les trois premiers méritent qu'on s'y arrête ici.

BOLA : la faille n° 1 des API

L'API d'AppliFrais vérifie le jeton de Lucas, puis renvoie la fiche demandée :

js
// VULNÉRABLE : n'importe quel utilisateur authentifié lit n'importe quelle fiche
router.get('/v1/fiches/:id', authentifier, async (req, res) => {
  const [lignes] = await pool.execute('SELECT id, mois, etat, montant_valide FROM fiche_frais WHERE id = ?', [req.params.id]);
  if (lignes.length === 0) return res.status(404).end();
  res.json(lignes[0]);
});

Il suffit à Lucas de remplacer 12 par 13, 14, 15… pour lire les fiches de tous ses collègues : c'est exactement l'énumération du journal d'accès analysé dans les exercices du chapitre 3. L'authentification est correcte ; c'est l'autorisation sur l'objet qui manque. La correction place la condition dans la requête elle-même :

js
router.get('/v1/fiches/:id', authentifier, async (req, res) => {
  const estComptable = req.utilisateur.role === 'comptable';
  const [lignes] = await pool.execute(
    `SELECT id, mois, etat, montant_valide FROM fiche_frais
     WHERE id = ? AND (id_visiteur = ? OR ?)`,
    [req.params.id, req.utilisateur.sub, estComptable]
  );
  if (lignes.length === 0) return res.status(404).end();   // inexistante OU fiche d'un autre
  res.json(lignes[0]);
});

Un identifiant non devinable (UUID) rend l'énumération plus difficile, mais ne remplace pas ce contrôle : un identifiant finit toujours par fuiter (dans une URL partagée, un journal, une capture d'écran).

BOPLA : trop de champs, dans les deux sens

En lecture, l'API renvoie plus de champs que nécessaire, en comptant sur le front pour ne pas les afficher. SELECT * sur la table des utilisateurs renvoie le hash du mot de passe ; l'onglet Réseau le montre à qui veut le voir. En écriture, l'API recopie le corps reçu dans la base : un visiteur qui ajoute "montantValide": 9999 ou "role": "comptable" à sa requête modifie un champ qu'il ne devrait pas toucher. C'est l'affectation de masse (mass assignment). Les parades, déjà vues : sélectionner explicitement les champs renvoyés (ou un DTO), et valider le corps par liste blanche avec additionalProperties: false (JSON Schema).

BFLA : la route oubliée

La route POST /v1/fiches/:id/validations est réservée aux comptables, mais le développeur a seulement caché le bouton dans l'interface des visiteurs. N'importe quel visiteur qui connaît la route (elle est dans le code JavaScript du front, et dans la documentation) valide sa propre fiche. La parade : un contrôle de rôle côté serveur sur chaque route, et une règle de refus par défaut (une route sans règle explicite est refusée).

js
router.post('/v1/fiches/:id/validations', authentifier, exigerRole('comptable'), validerFiche);

Limiter le débit

La limitation de débit (rate limiting) plafonne le nombre de requêtes qu'un client peut envoyer dans un intervalle de temps. Elle protège la disponibilité (un script ou une attaque ne sature pas le serveur), freine la force brute sur la connexion et l'énumération d'identifiants, et maîtrise les coûts quand l'API appelle des services payants. Au-delà de la limite, l'API répond 429 Too Many Requests, avec Retry-After :

http
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json

{
  "type": "https://api.gsb.example/problemes/limite-debit",
  "title": "Trop de requêtes",
  "status": 429,
  "detail": "Limite de 100 requêtes par minute atteinte. Réessaie dans 30 secondes."
}

Par quoi compter ? Par jeton ou par utilisateur pour les routes authentifiées (plusieurs utilisateurs peuvent partager une adresse IP, derrière la box d'une entreprise) ; par clé d'API pour les partenaires ; par adresse IP pour les routes publiques et la connexion. Les routes sensibles ont des limites plus strictes : 5 tentatives de connexion par minute et par compte, quelques envois de courriel de réinitialisation par heure.

Trois algorithmes courants :

AlgorithmePrincipeDéfaut ou qualité
Fenêtre fixeUn compteur par minute calendaire (12 h 00, 12 h 01…), remis à zéro à chaque minuteEffet de bord : 100 requêtes à 12 h 00 min 59 s, puis 100 à 12 h 01 min 00 s, soit 200 en deux secondes
Fenêtre glissanteOn compte les requêtes des 60 dernières secondes, quel que soit l'instantPas d'effet de bord, un peu plus coûteux à calculer
Seau à jetons (token bucket)Un seau de capacité N se remplit d'un jeton par intervalle ; chaque requête consomme un jeton ; seau vide, requête refuséeAutorise de courtes rafales (jusqu'à N) tout en imposant un débit moyen

Exemple de seau à jetons : capacité 10, un jeton ajouté chaque seconde. Un client qui n'a rien envoyé depuis longtemps a 10 jetons ; il envoie 15 requêtes d'un coup : 10 passent, 5 reçoivent 429. Trois secondes plus tard, il a 3 jetons, donc 3 requêtes possibles. Sur la durée, il ne dépasse pas une requête par seconde en moyenne.

Où la placer ? Le plus en amont possible : dans la passerelle d'API ou le reverse proxy (le module limit_req de nginx), ou dans un middleware de l'application (express-rate-limit en Node.js, le RateLimiter de Symfony ou de Laravel). Avec plusieurs serveurs d'application, les compteurs doivent être partagés (dans Redis, par exemple), sinon chaque serveur applique sa propre limite.

Beaucoup d'API informent le client de son quota restant avec des en-têtes. Les plus répandus, X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset, ne sont pas normalisés ; l'IETF prépare des en-têtes standard RateLimit et RateLimit-Policy, encore à l'état de projet.

Les autres protections de base

  • HTTPS partout, avec l'en-tête Strict-Transport-Security (HSTS) pour que le navigateur refuse ensuite toute connexion en HTTP.
  • Des réponses qui ne se laissent pas réinterpréter : un Content-Type exact et X-Content-Type-Options: nosniff, pour que le navigateur n'exécute pas une réponse JSON comme un script.
  • Pas de mise en cache des données personnelles : Cache-Control: no-store sur les réponses sensibles, en particulier celles qui contiennent des jetons.
  • Une taille maximale des corps (express.json({ limit: '100kb' }), client_max_body_size dans nginx) et des fichiers envoyés.
  • Des requêtes préparées pour toute requête SQL, y compris celles qu'alimentent les filtres de la chaîne de requête (injections).
  • Une journalisation utile : qui a fait quoi, sur quelle ressource, avec quel résultat, pour détecter une énumération ou une force brute, sans jamais journaliser les jetons, les mots de passe ni les clés d'API (journalisation).
  • Des erreurs génériques côté client, le détail dans le journal (chapitre 6).

Exercices

Exercice 1 sur 6 Même origine ? Réussi À revoir

La page est servie par https://frais.gsb.example. Pour chaque appel, dis si la politique de même origine s'applique (autre origine) et pourquoi.

  1. https://frais.gsb.example/api/fiches
  2. https://api.gsb.example/v1/fiches
  3. http://frais.gsb.example/api/fiches
  4. https://frais.gsb.example:3000/api/fiches
Voir la réponseMasquer la réponse de l'exercice 1
  1. Même origine : schéma, hôte et port identiques ; seul le chemin change. Pas besoin de CORS.
  2. Autre origine : l'hôte diffère (api au lieu de frais).
  3. Autre origine : le schéma diffère (http). En plus, une page HTTPS qui appelle du HTTP est bloquée comme contenu mixte.
  4. Autre origine : le port diffère (3000 au lieu de 443).

Tu avais trouvé ?

Exercice 2 sur 6 Pré-vérification ou pas ? Réussi À revoir

Le front de https://frais.gsb.example appelle https://api.gsb.example. Pour chaque requête, dis si le navigateur envoie une pré-vérification OPTIONS.

  1. GET /v1/communes?codePostal=69001, sans en-tête particulier.
  2. GET /v1/fiches avec Authorization: Bearer ….
  3. POST /v1/fiches avec Content-Type: application/json.
  4. DELETE /v1/frais-hors-forfait/58.
  5. POST /v1/contact avec Content-Type: application/x-www-form-urlencoded, sans autre en-tête.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Non : requête simple (GET, pas d'en-tête particulier).
  2. Oui : l'en-tête Authorization n'est pas un en-tête simple.
  3. Oui : application/json ne fait pas partie des trois types de contenu des requêtes simples.
  4. Oui : DELETE n'est pas une méthode simple.
  5. Non : POST avec un type de formulaire classique et sans en-tête particulier est une requête simple. La réponse ne sera lisible que si l'API renvoie le bon Access-Control-Allow-Origin, mais la requête, elle, part et s'exécute : CORS ne l'empêche pas. C'est pourquoi une route qui modifie des données ne doit pas compter sur CORS pour se protéger (voir le CSRF).

Tu avais trouvé ?

Exercice 3 sur 6 Corriger une configuration CORS Réussi À revoir

Un développeur a écrit, pour « régler les erreurs CORS » de son API qui authentifie par cookie de session :

js
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', req.headers.origin);
  res.set('Access-Control-Allow-Credentials', 'true');
  next();
});
  1. Quel est le danger ?
  2. Réécris la configuration pour n'autoriser que https://frais.gsb.example.
Voir la réponseMasquer la réponse de l'exercice 3
  1. L'API autorise toutes les origines, cookies compris : un site malveillant visité par un utilisateur connecté peut appeler l'API avec le cookie de session de la victime et lire les réponses (ses fiches, ses données personnelles). C'est une mauvaise configuration de sécurité (API8:2023). Il manque aussi Vary: Origin et la réponse aux pré-vérifications.
  2. js
    const cors = require('cors');
    app.use(cors({
      origin: ['https://frais.gsb.example'],
      credentials: true,
      methods: ['GET', 'POST', 'PATCH', 'DELETE'],
      allowedHeaders: ['Content-Type'],
    }));
    
    Le paquet cors compare l'origine reçue à la liste, ne renvoie Access-Control-Allow-Origin que si elle y figure, ajoute Vary: Origin et répond aux requêtes OPTIONS. Avec des cookies, la protection contre le CSRF reste nécessaire (SameSite, jeton anti-CSRF).

Tu avais trouvé ?

Exercice 4 sur 6 Classer dans l'OWASP API Top 10 Réussi À revoir

Donne le risque de l'OWASP API Security Top 10 (2023) qui correspond à chaque constat dans l'API d'AppliFrais.

  1. Un visiteur obtient 200 sur POST /v1/fiches/12/validations.
  2. GET /v1/moi renvoie le hash du mot de passe et l'IBAN.
  3. La route de connexion accepte des milliers d'essais de mot de passe par minute.
  4. L'import de justificatif par URL permet de demander http://169.254.169.254/ (le service de métadonnées du serveur cloud).
  5. Un script réserve en boucle tous les créneaux de rendez-vous avec les médecins pour les revendre.
  6. L'ancienne route /v0/fiches, sans authentification, répond toujours.
  7. Un visiteur modifie montantValide en ajoutant ce champ à sa requête PATCH.
Voir la réponseMasquer la réponse de l'exercice 4
  1. API5:2023 Broken Function Level Authorization : une fonction réservée aux comptables est accessible à un visiteur.
  2. API3:2023 Broken Object Property Level Authorization : des propriétés sensibles sont exposées en lecture.
  3. API2:2023 Broken Authentication (absence de protection contre la force brute) ; on peut aussi citer API4:2023.
  4. API7:2023 Server Side Request Forgery.
  5. API6:2023 Unrestricted Access to Sensitive Business Flows : un flux métier automatisé à l'excès.
  6. API9:2023 Improper Inventory Management.
  7. API3:2023, côté écriture : l'affectation de masse.

Tu avais trouvé ?

Exercice 5 sur 6 Seau à jetons Réussi À revoir

La route de connexion est protégée par un seau à jetons de capacité 5, qui reçoit un jeton toutes les 12 secondes, par compte.

  1. Quel est le débit moyen maximal, en tentatives par minute ?
  2. Un attaquant envoie 20 tentatives d'un coup sur un compte dont le seau est plein. Combien passent ? Combien reçoivent 429 ?
  3. Combien de temps doit-il attendre pour pouvoir faire 2 nouvelles tentatives ?
  4. Pourquoi compter par compte et par adresse IP ?
Voir la réponseMasquer la réponse de l'exercice 5
  1. Un jeton toutes les 12 secondes, soit 60 ÷ 12 = 5 tentatives par minute en moyenne.
  2. 5 passent (le seau est vidé), 15 reçoivent 429.
  3. Deux jetons, soit 2 × 12 = 24 secondes.
  4. Par compte, on empêche de tester beaucoup de mots de passe sur un compte, même depuis de nombreuses adresses IP. Par adresse IP, on empêche une seule machine de tester un mot de passe courant sur des milliers de comptes (la pulvérisation de mots de passe, password spraying), que la limite par compte ne voit pas.

Tu avais trouvé ?

Exercice 6 sur 6 Corriger une faille BOLA Réussi À revoir

Cette route supprime un frais hors forfait. Identifie la faille et corrige-la : seul le propriétaire de la fiche peut supprimer le frais, et seulement si la fiche est à l'état CR.

js
router.delete('/v1/frais-hors-forfait/:id', authentifier, async (req, res) => {
  await pool.execute('DELETE FROM frais_hors_forfait WHERE id = ?', [req.params.id]);
  res.status(204).end();
});
Voir la réponseMasquer la réponse de l'exercice 6

La route vérifie l'authentification mais pas que le frais appartient au demandeur : n'importe quel visiteur supprime les frais des autres (BOLA), et même ceux d'une fiche validée.

js
router.delete('/v1/frais-hors-forfait/:id', authentifier, async (req, res) => {
  const [resultat] = await pool.execute(
    `DELETE h FROM frais_hors_forfait h
     JOIN fiche_frais f ON f.id = h.id_fiche
     WHERE h.id = ? AND f.id_visiteur = ? AND f.etat = 'CR'`,
    [req.params.id, req.utilisateur.sub]
  );
  if (resultat.affectedRows === 0) return res.status(404).end();
  res.status(204).end();
});

Les conditions de propriété et d'état sont dans la requête elle-même : il n'y a pas de fenêtre entre un contrôle et la suppression. 404 couvre « inexistant » et « pas à toi ». Pour distinguer une fiche non modifiable (409), il faudrait d'abord lire le frais avec sa fiche, vérifier le propriétaire, puis l'état.

Tu avais trouvé ?

Chapitre terminé !

Fais les 6 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Tester une API : curl, Bruno, Postman et HTTPieRevoir le sommaire du cours