Une API de gestion de tickets d’assistance accepte la requête {"title": 7, "description": "<texte très long>", "email": "alice@example.com"} sans en vérifier la structure. Une étape ultérieure de formatage provoque un plantage à cause du titre numérique, tandis qu’un gestionnaire d’erreurs enregistre la description et l’e-mail soumis. L’attaquant n’a eu besoin ni de compte ni de chaîne d’exploitation ; il lui a suffi d’envoyer une requête franchissant la frontière de l’API avec des données que le service considérait comme fiables. La question qui se pose est de savoir à quel stade le service doit rejeter les entrées mal formées, trop volumineuses ou inattendues, avant qu’elles n’atteignent la logique métier ou les journaux.
Nous utiliserons un service FastAPI unique tout au long de ce cours. L’endpoint POST /tickets accepte un nouveau ticket, GET /tickets/{id} permet d’en lire un, POST /login authentifie un utilisateur et GET /me renvoie les informations de l’utilisateur authentifié. Chaque endpoint reçoit des données contrôlées par un client — même lorsqu’il s’agit d’un navigateur de confiance — ; il est donc nécessaire d’établir une frontière explicite entre les entrées reçues et l’état interne du service.
La validation des entrées est la règle de frontière qui n’accepte que les données conformes au contrat du service. Pour POST /tickets, une requête valide doit comporter un titre textuel court, une description de taille limitée et une adresse e-mail au format requis. Un JSON mal formé est rejeté encore plus tôt, car le framework est incapable de construire le corps de la requête à partir d’une syntaxe invalide. L’avantage ne se limite pas à une réduction du nombre d’exceptions : les entrées rejetées n’atteignent jamais le système de stockage des tickets, l’étape de génération de la réponse ou les journaux de diagnostic susceptibles d’exposer des données personnelles.
Le service comporte trois frontières qu’il convient de garder à l’esprit lors du développement : l’analyse de la requête, l’authentification via POST /login et la génération de la réponse. La première frontière constitue notre point de départ, car toutes les requêtes — y compris celles non authentifiées — y parviennent. Suivons le parcours des champs contrôlés à travers le service :
Ce diagramme montre que title, description et email proviennent de l’extérieur du service, tandis que le dépôt en mémoire ne reçoit que les données ayant franchi la frontière de la requête.
Puisque le diagramme distingue les données du client de celles du dépôt, l’opération POST /tickets nécessite des règles sur lesquelles le gestionnaire peut s’appuyer, plutôt que des hypothèses éparpillées dans le code ultérieur. Le contrat définit les champs autorisés, leurs types et leurs limites. Un titre peut accepter entre 1 et 120 caractères, tandis qu’une description peut en accepter jusqu’à 4 000. Ces limites constituent des mesures de sécurité, car elles encadrent le stockage, le traitement, le volume des journaux et la taille de la réponse, empêchant ainsi un attaquant de les amplifier.
La normalisation s’inscrit dans ce même périmètre lorsque le service requiert une forme canonique unique. On peut supprimer les espaces superflus d’un titre avant de le stocker, mais il ne faut pas corriger silencieusement une adresse e-mail invalide ni tronquer une description trop longue. La correction altère l’intention du client, alors que le rejet rend le contrat explicite. Les validateurs de champs Pydantic peuvent transformer les valeurs acceptées, mais ils doivent renvoyer le résultat validé ; par ailleurs, les règles nécessitant une entrée brute doivent être capables de gérer des données de tout type.
Que doit renvoyer POST /tickets si l’e-mail fourni est not-an-email ? Le gestionnaire ne doit pas créer le ticket 42, et le client a besoin d’une structure d’erreur stable plutôt que d’une trace d’erreur interne. Le modèle de requête suivant applique des limites aux champs et enregistre le champ rejeté sans pour autant conserver l’adresse e-mail rejetée elle-même :