Exemples de menaces
- Leçon 3
- 50 min
- Module 1 : Bases de l'identité et des autorisations
Leçon 3 sur 19 · Authentification, autorisations et applications multilocatairesExemples de menaces
1. Bases de l'identité et des autorisations
2. Comprendre la connexion et les jetons
3. Sessions et intégration à l'application
4. Contrôler l'accès aux ressources
5. Travail en arrière-plan et administration
6. Valider et documenter la conception
Ce que vous saurez faire
Voyez ce qui arrive quand l’API vérifie seulement une clé partagée. Une personne lit le billet de soutien d’une autre organisation en devinant son identifiant. Une personne en lecture seule modifie un billet. Vous essayez les deux sur votre propre API d’exercice, avec des données fictives.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les trois questions de Trois questions distinctes, et la matrice d’accès avec ses réponses 403 et 404, de Utilisateurs et organisations.
curlet les routes de l’API/v1/tickets/{id},…/messageset…/similar, des cours sur les API et les bases de données.
Votre projet
~/projects/ticket-apià l’instantanéend-of-m01: les deux commerces sont chargés,154 passed, 1 deselected.
Sans danger et en local. Chaque tentative de cette leçon vise l’API d’exercice sur votre ordinateur, avec des données fictives. N’essayez jamais ces requêtes sur un système qui ne vous appartient pas, ou sans autorisation écrite.
Un vestiaire et une porte réservée au personnel
Section intitulée « Un vestiaire et une porte réservée au personnel »Imaginez le vestiaire d’un théâtre. Vous donnez votre manteau et recevez un reçu portant le numéro 41. Après le spectacle, vous montrez le reçu, et le préposé vous apporte le manteau 41.
Imaginez maintenant un préposé paresseux. Il ne regarde pas votre reçu. Il demande seulement : « Quel numéro? » Vous dites « 42 », et il vous apporte le manteau d’une autre personne. Personne n’a rien brisé. Le préposé n’a pas vérifié que le manteau 42 vous appartient.
Le même théâtre a une porte avec une affiche : « Personnel seulement ». La porte n’est pas verrouillée. La plupart des visiteurs respectent l’affiche. Un visiteur curieux ouvre quand même la porte, et rien ne l’arrête.
L’API de billets a ces deux problèmes aujourd’hui. Dans cette leçon, vous observez chacun avec de vraies requêtes.
Tentative 1 : un identifiant de billet deviné
Section intitulée « Tentative 1 : un identifiant de billet deviné »Sam travaille chez Larkfield. Le nouveau commerce l’intrigue. Larkfield numérote ses billets de T-30001 à T-30200. Les billets de Bramble ont donc probablement des numéros semblables. Sam essaie T-40001.
-
Démarrez l’API avec la clé, dans un deuxième terminal où l’environnement virtuel est actif :
Fenêtre de terminal API_KEY=local-dev-key fastapi devSur Windows, définissez d’abord
$env:API_KEY = "local-dev-key", puis exécutezfastapi dev. -
Dans le premier terminal, envoyez la requête de Sam. Sur Windows, tapez
curl.exe:Fenêtre de terminal curl -H "X-API-Key: local-dev-key" http://127.0.0.1:8000/v1/tickets/T-40001{"ticket_id":"T-40001","customer_id":"B-20101","customer_name":"Eleanor Price","subject":"Charged twice for order BB-310729","body":"My card was charged two times for order BB-310729, 47.50 dollars each time. Please refund one of the payments.","team":"billing","priority":1,"status":"open","attachments":[{"attachment_id":"e7e54964-6167-47fa-8c59-b0b14b2d7e4a","file_name":"receipt-BB-310729.pdf","size_bytes":724, …}], …}La réponse est raccourcie ici. Sam a maintenant le nom d’un client de Bramble, un numéro de commande, un montant et l’identifiant de son reçu.
-
Essayez un numéro qui n’existe pas :
Fenêtre de terminal curl -H "X-API-Key: local-dev-key" http://127.0.0.1:8000/v1/tickets/T-49999{"error":{"code":"not_found","message":"Ticket T-49999 does not exist.","request_id":"67f184db04b6","fields":[]}}
Les deux réponses diffèrent. Une personne curieuse peut donc essayer T-40001, T-40002, et ainsi de suite, pour lister tous les billets existants. Les identifiants sont faciles à deviner, car ils suivent une séquence.
Description textuelle du diagramme
Sam envoie GET /v1/tickets/T-40001 avec la clé partagée. L’API vérifie seulement que la clé est correcte. Elle exécute ensuite SELECT … FROM tickets WHERE ticket_id = 'T-40001', trouve le billet de Bramble Books et le renvoie avec 200 OK (succès). Il manque la vérification : « T-40001 appartient-il à une organisation dont Sam est membre? » Personne ne pose cette question.
Ce défaut porte un nom. L’API permet à un appelant d’accéder à un objet, ici un billet, auquel il n’a pas droit. C’est un défaut d’accès au niveau des objets. Les listes de sécurité l’appellent BOLA, Broken Object Level Authorization (autorisation défaillante au niveau des objets). Il arrive premier dans le classement OWASP API Security Top 10 (2023), une liste des risques d’API les plus courants. Comme le préposé paresseux, l’API fait confiance au numéro donné par l’appelant.
Tomás propose une solution : « Utilisez des identifiants aléatoires, comme pour les pièces jointes. Personne ne peut les deviner. » Regardez de nouveau l’étape 2. L’identifiant de la pièce jointe est aléatoire, e7e54964-…, et la réponse du billet le révèle. Un identifiant aléatoire rend les tentatives plus difficiles, mais ce n’est pas une vérification. Un identifiant peut être révélé par des liens, des journaux (logs) et des captures d’écran. L’appelant suivant obtient alors l’enregistrement. La seule solution est de vérifier, pour chaque enregistrement, qu’il appartient à une organisation dont l’appelant est membre.
Tentative 2 : une personne en lecture seule écrit
Section intitulée « Tentative 2 : une personne en lecture seule écrit »Omar a un accès en lecture seule chez Larkfield. Imaginez que la page Web du service de soutien cache le bouton « Reply » (répondre) aux personnes en lecture seule. Omar n’a pas besoin de la page. Il envoie lui-même la requête :
curl -X POST -H "X-API-Key: local-dev-key" -H "Content-Type: application/json" -d '{"author": "agent", "body": "Refund approved."}' http://127.0.0.1:8000/v1/tickets/T-30002/messages{"message_id":390,"author":"agent","body":"Refund approved.","created_at":"2026-10-10T06:10:54.369461Z"}Dans PowerShell, placez le corps dans un fichier, msg.json, et utilisez -d "@msg.json". L’API a enregistré le message : 201 Created (créé). Un client lit maintenant « Refund approved. » (remboursement approuvé), venant d’une personne qui ne peut rien approuver.
Description textuelle du diagramme
Par la page, Omar voit le billet T-30002 sans bouton Reply, donc il n’envoie aucune requête. Sans passer par la page, Omar envoie la même requête POST depuis son terminal. L’API vérifie seulement la clé. Elle enregistre donc le message et répond 201 Created. Le bouton fait partie de la page dans le navigateur. La règle doit être dans l’API, car chaque requête atteint l’API.
C’est une élévation de privilèges : une personne fait quelque chose que son rôle ne permet pas. Ici, une personne en lecture seule agit comme un membre du personnel. La liste OWASP appelle le défaut de l’API Broken Function Level Authorization (autorisation défaillante au niveau des fonctions). L’API ne vérifie pas si cet appelant peut utiliser cette fonction.
Tomás a déjà dit : « On a caché le bouton de suppression, donc les personnes en lecture seule ne peuvent pas supprimer. » Vous pouvez maintenant lui répondre. Un bouton caché facilite la vie de l’utilisateur : il cache ce que celui-ci ne peut pas utiliser. Ce n’est jamais la protection. L’API doit vérifier le rôle sur le serveur, pour chaque requête.
Des fuites sans rien deviner
Section intitulée « Des fuites sans rien deviner »Certains défauts ne demandent même pas de deviner. Ines est propriétaire de Bramble Books. Elle liste les billets « de son commerce » :
curl -H "X-API-Key: local-dev-key" "http://127.0.0.1:8000/v1/tickets?limit=6"Les six billets les plus récents sont renvoyés, provenant des deux commerces et mélangés selon l’heure :
T-40040 B-20102 Change the delivery address of BB-310461T-30200 C-0025 wooden garden bench stopped workingT-30199 C-0027 Change delivery addressT-30198 C-0038 Merge two accountsT-40039 B-20110 Where is my order BB-310304?T-30197 C-0029 Payment failed but money takenChaque ligne montre seulement trois champs ici. La liste ne précise aucune organisation, donc elle renvoie tout. La recherche de billets semblables a le même problème. Ines demande des billets semblables à son T-40001. Deux des cinq résultats viennent de Larkfield : T-30050 et T-30076, des billets « Charged twice » (facturé deux fois) du commerce de jardinage. Les vecteurs sont proches parce que les problèmes sont les mêmes, et rien ne sépare les commerces.
Le cours sur les bases de données présentait une injection qui brisait une requête construite avec des chaînes, dans Requêtes sûres. Exécutez de nouveau sa démonstration :
python -m scripts.injection_demo "x' OR '1'='1"unsafe (string building): SQL sent: SELECT customer_id, name, email FROM customers WHERE email = 'x' OR '1'='1' 52 row(s), first: ('C-0001', 'Olga Olsen', 'olga.olsen@example.com')safe (parameter): SQL sent: SELECT customer_id, name, email FROM customers WHERE email = %s value sent separately: "x' OR '1'='1" 0 row(s)Dans le cours sur les bases de données, elle renvoyait les 40 clients de Larkfield. Maintenant, elle en renvoie 52 : les clients des deux commerces. Un bogue qui révélait les données d’un commerce en révèle maintenant deux. La version sûre avec un paramètre renvoie toujours 0 ligne. Arrêtez l’API avec Ctrl+C.
Exécuter les cinq tentatives
Section intitulée « Exécuter les cinq tentatives »Le projet contient un script avec cinq tentatives fictives. Il envoie chaque requête au code de l’API, avec la clé, et évalue la réponse. Il n’a pas besoin d’un serveur en marche.
python -m scripts.attemptsMade-up attempts against your own practice API. Every request is sent the same way(with the API key, if one is set): the API cannot tell who is calling.
1. Sam (staff at Larkfield) tries to read Bramble Books' ticket T-40001 by guessing its ID. GET /v1/tickets/T-40001 -> 200 (should get: 404) LEAK: "Charged twice for order BB-310729"2. Ines (owner at Bramble Books) tries to list the tickets of her own shop. GET /v1/tickets?limit=100 -> 200 (should get: only bramble) LEAK: 77 of 100 tickets are Larkfield's, e.g. T-302003. Omar (read-only at Larkfield) tries to add a message to T-30002. POST /v1/tickets/T-30002/messages -> 201 (should get: 403) ESCALATION: "(attempt: a read-only member writes)"4. Tomás (no membership) tries to read Larkfield's ticket T-30002. GET /v1/tickets/T-30002 -> 200 (should get: 404) LEAK: "Charged twice for order LK-182074"5. Ines (owner at Bramble Books) tries to find tickets similar to her ticket T-40001. GET /v1/tickets/T-40001/similar?k=5 -> 200 (should get: only bramble) LEAK: 2 of 5 tickets are Larkfield's, e.g. T-30050
5 of 5 attempts got what they should not get.L’API affiche aussi ses propres lignes de journal entre les résultats. Pour voir seulement le rapport, ajoutez 2>/dev/null sur macOS et Linux, ou 2>$null dans PowerShell.
Les cinq tentatives obtiennent ce qu’elles ne devraient pas obtenir. Chacune échoue à l’une des trois questions :
| Tentative | Résultat attendu | Résultat obtenu | La question à laquelle l’API ne peut pas répondre |
|---|---|---|---|
| 1. Sam lit T-40001 | 404 | 200 | Dans quelle organisation? Sam n’est pas membre de Bramble. |
| 2. Ines liste les billets | Ceux de Bramble seulement | 77 sur 100 sont ceux de Larkfield | Dans quelle organisation? La liste n’en précise aucune. |
| 3. Omar écrit | 403 | 201 | Que peut-il faire? read_only ne peut pas écrire. |
| 4. Tomás lit T-30002 | 404 | 200 | Qui est l’appelant? Tomás n’a aucune adhésion. |
| 5. Billets semblables d’Ines | Ceux de Bramble seulement | 2 sur 5 sont ceux de Larkfield | Dans quelle organisation? La recherche n’a aucun filtre. |
Les réponses attendues viennent directement de la matrice d’accès. Utilisez l’explorateur pour les vérifier. Il commence là où la tentative 3 a échoué.
Explorer
Ce que l'API aurait dû répondre
Prédisez d’abord : Omar est read_only (lecture seule) chez Larkfield. Que devrait répondre l'API quand il ajoute un message? Et quand Sam, membre du personnel de Larkfield, lit un billet de Bramble Books?
- Rôle dans cette organisation
- read_only
- L’API répond
- 403 role_cannot
- La règle
- read_only may not ticket.write
Omar a le rôle read_only chez Larkfield, et la règle dit « read_only may not ticket.write ». L’API répond 403. Un membre peut déjà voir les billets de l’organisation, donc un 403 ne révèle rien de nouveau.
- 200 : permis
- 403 : un membre, mais le rôle ne le permet pas
- 404 : pas un membre, rien n’est montré
| Action | Larkfield read_only | Bramble Books aucune adhésion |
|---|---|---|
ticket.readlister et lire les billets, trouver des billets semblables | 200 oui | 404 |
ticket.writeajouter un message à un billet | 403 (choisi) | 404 |
file.readobtenir un lien de téléchargement pour une pièce jointe | 200 oui | 404 |
file.uploadtéléverser une pièce jointe | 403 | 404 |
document.readchercher dans les documents d’aide publics | 200 oui | 404 |
document.read_staffaussi les documents réservés au personnel | 403 | 404 |
member.readlister les membres de l’organisation | 403 | 404 |
member.manageinviter, changer un rôle, retirer un membre | 403 | 404 |
export.createlancer une exportation des billets en arrière-plan | 403 | 404 |
audit.readlire les événements d’audit de l’organisation | 403 | 404 |
Chaque réponse vient de la fonction decide() du projet, dans ticket_api/access.py, affichée par python -m scripts.access_matrix --json. « 200 » veut dire permis : une requête qui crée un enregistrement, comme un nouveau message, répond 201. Les personnes sont fictives.
Description textuelle de l’explorateur
L’explorateur a trois groupes de boutons : une personne, une organisation et une action. Il montre le rôle de la personne dans cette organisation, la réponse de la matrice d’accès (200, 403 ou 404) et la règle. Un tableau montre la ligne complète de cette personne dans les deux organisations.
Il commence avec Omar, Larkfield, ticket.write (écrire dans un billet). Omar est read_only, la règle dit « read_only may not ticket.write » (lecture seule ne permet pas d’écrire dans un billet), et la réponse est 403. L’API actuelle a répondu 201. Sam n’a aucune adhésion chez Bramble Books, donc ticket.read (lire un billet) répond 404; l’API actuelle a répondu 200. Tomás obtient 404 partout. Camille obtient 200 pour ticket.read chez Bramble Books, mais 403 pour ticket.write dans cette organisation.
Ce qui corrige chaque défaut
Section intitulée « Ce qui corrige chaque défaut »Kwame relie chaque défaut au travail à venir :
| Élément manquant | Tentatives corrigées | Où dans le cours |
|---|---|---|
| Connaître la personne : un utilisateur connecté avec un jeton vérifié, pas une clé partagée | 4, et toutes les autres en ont besoin | Comprendre la connexion et les jetons et Sessions et intégration à l’application |
Vérifier l’adhésion et le rôle sur le serveur pour chaque route, sous /v1/tenants/{tenant}/… |
1, 3, 4 | L’autorisation dans l’API |
| Rendre l’organisation obligatoire dans chaque requête, recherche et lien de fichier | 1, 2, 5 | Requêtes sûres pour chaque locataire et Accès aux fichiers et à la récupération |
| Tester chaque personne sur chaque route dans les deux commerces | toutes, durablement | Tests négatifs |
Tomás propose un autre raccourci : « Que le navigateur envoie un en-tête, X-Tenant-ID: bramble, et qu’on filtre avec ça. » Réfléchissez-y, sans code. Sam change un en-tête, et l’API lui montre les billets de Bramble. L’en-tête répond à « quelle organisation l’appelant veut-il », pas à « quelle organisation peut-il voir ». L’API doit répondre à la deuxième question à partir de sa propre table d’adhésions. Les tentatives de fin de cours envoient exactement cet en-tête falsifié, et l’API le refuse.
Ces tentatives valent la peine dès le premier jour de toute application partagée, avant l’arrivée de vrais clients. Elles coûtent peu : un script et quelques secondes. Le vrai coût vient des corrections, qui changent chaque route et chaque requête. C’est pourquoi le cours y consacre trois modules.
Erreurs courantes
Section intitulée « Erreurs courantes »Erreur courante
Chaque exécution ajoute un autre message
Symptôme : chaque exécution de python -m scripts.attempts ajoute un message au billet T-30002. Après python -m scripts.load --reset et deux exécutions, il a trois messages au lieu d’un. Vérifiez dans psql :
SELECT message_id, author, body FROM messages WHERE ticket_id = 'T-30002' ORDER BY message_id; message_id | author | body------------+--------+--------------------------------------------------- 2 | agent | Could you send the receipt or a screenshot of … 390 | agent | (attempt: a read-only member writes) 391 | agent | (attempt: a read-only member writes)(3 rows)Le premier message est raccourci ici.
Cause : la tentative 3 réussit actuellement, donc chaque exécution écrit vraiment un message. Vos identifiants de messages peuvent différer.
Correction : exécutez python -m scripts.load --reset. T-30002 a de nouveau un seul message. Après le module 4, l’API refuse la tentative et n’écrit rien.
Réinitialisation. python -m scripts.load --reset supprime tous les messages que vous avez ajoutés dans cette leçon. Si votre projet ne fonctionne plus, copiez de nouveau l’instantané end-of-m01, comme dans Utilisateurs et organisations.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Expliquez la différence entre un défaut d'accès au niveau des objets (BOLA) et une élévation de privilèges. Donnez un exemple de chacun tiré de cette leçon.
Afficher la réponse
Un défaut d’accès au niveau des objets permet à un appelant d’accéder à un enregistrement auquel il n’a pas droit. Sam lit le billet T-40001 de Bramble en devinant son identifiant. Une élévation de privilèges permet à un appelant de faire une action que son rôle ne permet pas. Omar, en lecture seule, ajoute un message à T-30002. Le premier échoue à « dans quelle organisation? » pour un enregistrement. Le deuxième échoue à « que peut-il faire? ».
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Prédisez : Larkfield remplace chaque identifiant de billet par un UUID aléatoire, sans rien changer d'autre. La tentative 1 réussit-elle encore? Pourquoi?
Afficher la réponse
Elle réussit encore pour tout identifiant que Sam découvre, car l’API vérifie toujours seulement la clé. Les identifiants aléatoires sont plus difficiles à deviner. Mais ils peuvent être révélés par des liens, des journaux, des captures d’écran et d’autres réponses. L’identifiant de pièce jointe dans la réponse de T-40001 le montre. La correction consiste à vérifier que l’enregistrement appartient à une organisation dont l’appelant est membre.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Vérification du module : Camille est membre du personnel chez Larkfield et en lecture seule chez Bramble Books. Pour chaque requête, donnez la réponse de la matrice d'accès (200, 403 ou 404) et la raison : (a) lire le billet T-40001 de Bramble par l'organisation Bramble; (b) y ajouter un message à T-40001; (c) ajouter un message au billet T-30002 de Larkfield; (d) inviter un nouveau membre chez Larkfield; (e) lire T-40001 par l'organisation Larkfield.
Afficher la réponse
(a) 200 : chez Bramble Books, Camille est read_only, et read_only permet ticket.read.
(b) 403 : read_only ne permet pas ticket.write. Camille est membre et voit déjà le billet, donc un 403 ne révèle rien de nouveau.
(c) 200, et l’API répond 201 pour le nouveau message : chez Larkfield, Camille est staff (personnel), et staff permet ticket.write.
(d) 403 : staff ne permet pas member.manage (gérer les membres). Seule la propriétaire de Larkfield, Grace, peut le faire.
(e) 404 : T-40001 n’est pas un billet de Larkfield. La recherche se fait dans Larkfield, donc le billet « n’existe pas » dans cette organisation, même si Camille peut le lire par l’organisation Bramble.
Une personne obtient cinq réponses différentes, car chaque décision utilise le rôle d’une adhésion et l’organisation de l’enregistrement.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Vérification pratique : exécutez les tentatives. Quelle est la dernière ligne, et quelle tentative modifie la base de données?
Afficher la réponse
python -m scripts.attempts se termine par 5 of 5 attempts got what they should not get. (les cinq tentatives ont obtenu ce qu’elles ne devraient pas obtenir). La tentative 3 (le message d’Omar) écrit une ligne dans messages. python -m scripts.load --reset la supprime.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Un défaut d’accès au niveau des objets (BOLA) : l’API renvoie un enregistrement parce que l’appelant connaît son identifiant. Sam a lu T-40001 de Bramble. Les identifiants aléatoires cachent les enregistrements; seule une vérification les protège.
- Élévation de privilèges : une personne fait plus que son rôle ne permet. Omar, en lecture seule, a ajouté un message. Un bouton caché n’est pas une protection : la règle doit être dans l’API.
- Sans organisation, les listes, les recherches et les anciens bogues mélangent les commerces. Sur 100 billets listés, 77 appartenaient à Larkfield, et la démonstration d’injection renvoie maintenant 52 clients.
python -m scripts.attempts: 5 sur 5 tentatives ont obtenu ce qu’elles ne devraient pas obtenir. À la fin du cours, les mêmes tentatives et cinq autres échouent toutes.
Récapitulatif du module. Vous pouvez maintenant poser les trois questions pour n’importe quelle requête. Vous pouvez stocker les utilisateurs, les organisations, les adhésions et les rôles, et lire la matrice d’accès. Vous pouvez aussi expliquer pourquoi Camille obtient des réponses différentes dans deux organisations. Votre projet correspond à end-of-m01.
Ensuite, l’API apprend qui est l’appelant. Vous découvrez OAuth et OpenID Connect, ainsi que le fournisseur d’identité qui connecte les personnes.
Leçon suivante : OAuth et OIDC