Comment tester la sécurité de votre API contre les entrées malveillantes avant les pirates

L'entrée est une surface d'attaque. Élaborez des tests négatifs pour l'injection, les charges utiles surdimensionnées et malformées, faites du schéma un contrôle de sécurité, et exécutez-les dans la CI.

Ashley Innocent

Ashley Innocent

23 July 2026

Comment tester la sécurité de votre API contre les entrées malveillantes avant les pirates

Apidog pour les entreprises

Déploiement sur site

SSO & RBAC

Conforme SOC 2

Découvrir Apidog Enterprise
TL;DR : L'entrée de votre API est une surface d'attaque, testez-la donc comme telle. Rédigez des cas négatifs qui envoient des champs surdimensionnés, des types erronés, des corps mal formés et des chaînes d'injection, puis assurez-vous que le point de terminaison répond par un 4xx et jamais par un 5xx. Transformez la validation de schéma en un contrôle de sécurité avec additionalProperties: false, des énumérations et des limites de longueur. Exécutez la suite complète en CI à chaque modification. Les agents IA rendent cela urgent : ils génèrent et transmettent des charges utiles à la vitesse de la machine, de sorte que "charger ces données" qui devient "exécuter ce code" prend désormais de l'ampleur.

La plupart des suites de tests prouvent que votre API fonctionne lorsque l'appelant est poli. Vous envoyez un corps valide, vous obtenez un 200, l'assertion réussit. Ce résultat ne vous dit presque rien sur ce qui se passe lorsque le corps est hostile. Une entrée non fiable est toute donnée que votre point de terminaison n'a pas générée lui-même : corps de requêtes, chaînes de requête, en-têtes, téléchargements de fichiers, charges utiles de webhook et le JSON qu'un agent IA assemble à la volée. Tout cela mérite la même hypothèse, à savoir que quelqu'un finira par en envoyer la pire version possible.

En juillet 2026, Hugging Face a décrit un incident de sécurité dont le vecteur d'entrée était des données, et non un mot de passe volé. Nous avons abordé les leçons de cette brèche séparément ; ce guide en est la moitié pratique. Vous allez créer des tests qui envoient le type d'entrée qu'un attaquant envoie, puis les exécuter automatiquement à chaque modification. Les catégories s'alignent sur le Top 10 de la sécurité des API d'OWASP, qui mérite d'être gardé ouvert dans un onglet. Apidog est un moyen de concevoir le contrat et de piloter ces tests, mais les idées sont valables dans n'importe quel cadre que vous utilisez déjà.

L'entrée est une surface d'attaque, pas un champ de formulaire

La validation est souvent traitée comme une politesse pour l'expérience utilisateur : intercepter l'e-mail vide, afficher une bordure rouge, passer à autre chose. Ce cadrage est le problème. Chaque champ que votre API accepte est une promesse que l'appelant peut rompre, et chaque promesse rompue est un chemin vers votre logique. Un paramètre limit que vous attendiez être un petit entier devient 999999999. Un filename que vous attendiez être un seul mot devient ../../etc/passwd. Un objet config que vous attendiez contenir des paramètres devient un ensemble d'instructions.

Les tests de sécurité ne sont pas une discipline distincte ajoutée à la fin. Ce sont les mêmes tests négatifs que vous connaissez déjà, visant les champs les plus susceptibles de vous nuire. Si vous prenez l'habitude de vous demander « quelle est la pire chose qui puisse tenir dans ce champ », vous êtes à mi-chemin des pratiques décrites dans notre guide sur les meilleures pratiques de sécurité des API. Le reste de cet article transforme cette question en tests concrets que vous pouvez exécuter.

Comment « charger ces données » est devenu « exécuter ce code »

L'incident Hugging Face est un exemple clair de la raison pour laquelle l'entrée mérite cette attention. Hugging Face a déclaré que le vecteur d'entrée était des ensembles de données malveillants : un ensemble de données conçu a déclenché un chargeur d'ensemble de données d'exécution de code à distance, et une injection de modèle était présente dans une configuration d'ensemble de données. Vous pouvez lire le propre compte rendu de l'entreprise dans son rapport d'incident de sécurité.

Analysez la forme de cet échec. Un point de terminaison a accepté quelque chose décrit comme des données. Le chargement de ces données a exécuté un chemin de code qui pouvait exécuter des instructions contrôlées par l'attaquant. « Charger ces données » est devenu « exécuter ce code ». L'injection de modèle est la même histoire à une échelle plus petite : une valeur de configuration qui était censée être du texte inerte a été évaluée, de sorte que le texte est devenu une exécution.

La leçon à tirer n'est pas que "Hugging Face a fait une erreur rare". C'est que tout point de terminaison acceptant un nom de chargeur, un format, un modèle, un objet sérialisé ou un blob de configuration accepte des instructions, que vous l'ayez voulu ou non. Si vous n'avez jamais écrit de test qui envoie une configuration hostile à ce point de terminaison, vous n'avez jamais réellement vérifié l'hypothèse qu'il reste inerte. Cette hypothèse non testée est la vulnérabilité entière.

La validation de schéma comme contrôle de sécurité

Le contrôle le moins cher que vous puissiez ajouter est un schéma strict en périphérie. Un schéma n'est pas seulement de la documentation. Lorsque vous rejetez tout ce qui ne correspond pas, le schéma devient un filtre qui s'exécute avant même que votre logique métier ne voie la requête. JSON Schema vous fournit les primitives pour rendre ce filtre rigoureux.

Voici un schéma pour la configuration de jeu de données de l'histoire, écrit de manière à ce que la plupart des entrées hostiles n'atteignent jamais le code de l'application :

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["loader", "name"],
  "properties": {
    "loader": { "enum": ["csv", "json", "parquet"] },
    "name": { "type": "string", "maxLength": 128, "pattern": "^[\\w .-]+$" },
    "rows": { "type": "integer", "minimum": 0, "maximum": 1000000 }
  }
}

Lisez-le comme quatre défenses distinctes. additionalProperties: false rejette d'emblée un champ template dissimulé, de sorte qu'un attaquant ne peut pas en ajouter. L'énumération loader signifie que pickle:// ou tout chargeur de code à distance n'est tout simplement pas une valeur valide. maxLength élimine la chaîne de plusieurs mégaoctets visant à épuiser la mémoire. Le pattern sur name refuse les caractères {{ et '; DROP TABLE avant qu'ils ne voyagent plus loin. Aucune de ces lignes ne connaît les attaquants. Elles n'acceptent que l'ensemble étroit d'entrées que vous supportez réellement, et cette étroitesse est la propriété de sécurité.

Une validation de contrat comme celle-ci n'intercepte pas toutes les exploitations, et aucun schéma ne le fera. Ce qu'elle ferme est une catégorie spécifique et courante : le bug « nous n'avons jamais vérifié ce que ce point de terminaison accepte ». Cette catégorie est l'endroit où un nombre surprenant de violations commencent.

Tests négatifs : prouvez que le point de terminaison dit non

Les tests de chemin heureux affirment qu'une bonne entrée produit une bonne sortie. Les tests négatifs affirment qu'une mauvaise entrée produit un refus contrôlé. La distinction est importante car un refus est une fonctionnalité : un 400 avec une erreur claire est votre API qui défend sa frontière. Un 500 est votre API qui en perd le contrôle.

Construisez des cas négatifs toujours de la même manière. Pour chaque champ, notez ce qu'il doit rejeter : mauvais type, manquant lorsqu'il est requis, présent lorsqu'il est interdit, trop long, hors de portée et les chaînes d'injection qui correspondent à son format. Ensuite, affirmez deux choses sur la réponse. Premièrement, le statut est un 4xx, généralement 400 ou 422. Deuxièmement, le statut n'est jamais un 5xx. Un 500 signifie que votre entrée hostile a atteint du code qui n'était pas prêt pour elle, ce qui est exactement la portée qu'un attaquant souhaite. Notre liste de contrôle des tests de sécurité des API contient une liste de départ champ par champ que vous pouvez adapter.

Une règle garantit l'honnêteté : affirmer le comportement, pas le texte d'erreur. Si vous affirmez que le message indique « chargeur invalide », une refactorisation inoffensive brise votre test et apprend à l'équipe à le relâcher. Affirmez le code de statut, et là où vous le pouvez, affirmez qu'aucun effet secondaire ne s'est produit.

Les classes d'injection qui méritent un test dédié

Quelques familles d'injection apparaissent assez souvent pour que chacune mérite des cas de test permanents, et non une vérification manuelle ponctuelle. Vous n'avez pas besoin d'être exhaustif ici. Il vous faut un cas de sondage par classe pour qu'une régression échoue bruyamment. Les outils qui exécutent la détection automatisée des vulnérabilités d'API peuvent étendre la couverture plus tard, mais une poignée de cas écrits à la main détectent d'abord les failles évidentes.

Surdimensionné, mal formé et confusion de type de contenu

Toute entrée hostile n'est pas une chaîne intelligente. Certaines sont simplement trop grandes ou mal formées, et celles-ci brisent souvent les analyseurs avant même que votre logique de validation ne s'exécute.

Envoyez une charge utile surdimensionnée : un seul champ contenant cinq mégaoctets d'un caractère, ou un tableau JSON avec un million d'éléments. Une API saine applique une limite de taille de corps et renvoie 413 plutôt que d'allouer de la mémoire jusqu'à s'effondrer. Envoyez également des corps mal formés : JSON tronqué, une virgule finale, ou JSON imbriqué mille niveaux de profondeur pour sonder l'épuisement de la pile. La bonne réponse est un 400 rapide, pas un travailleur bloqué.

La confusion de type de contenu est la plus discrète. Déclarez Content-Type: application/json mais envoyez du XML, ou déclarez application/xml et envoyez une charge utile avec une entité externe pour sonder les XXE. Inversez la situation et envoyez du JSON en tant que text/plain pour voir si un analyseur laxiste l'accepte quand même. Chaque non-concordance teste si votre serveur fait confiance à l'en-tête, fait confiance au corps, ou vérifie que les deux sont en accord. Il devrait exiger un accord avant d'analyser quoi que ce soit.

Pourquoi les agents IA augmentent les enjeux

Tout ce qui précède était vrai avant l'existence des agents. Les agents changent le volume et la vitesse. Un attaquant humain tape une requête hostile à la fois. Un agent IA génère et transmet des charges utiles à la vitesse de la machine, et il construira volontiers des entrées qu'une personne n'aurait jamais pris la peine d'essayer.

Trois propriétés aggravent la situation. Les agents synthétisent l'entrée, ils produisent donc des valeurs de champ qu'aucun humain n'a écrites et qu'aucun test n'avait anticipées. Les agents réessayent et enchaînent les appels, de sorte qu'un seul document en amont empoisonné peut se transformer en des milliers de requêtes hostiles contre votre point de terminaison en quelques secondes. Et les agents transmettent des données auxquelles on leur a dit de faire confiance, c'est ainsi qu'une charge utile cachée dans un ensemble de données ou un webhook devient une requête réelle à votre API. Le modèle Hugging Face, où « charger ces données » se transforme en « exécuter ce code », est précisément le type d'instruction qu'un agent transportera au-delà d'une limite de confiance sans le remarquer. Notre note sur l'injection de prompt pour les équipes API approfondit cette transmission. La défense ne change pas ; elle doit juste être automatique, car vous ne pouvez pas examiner le trafic des agents manuellement.

Construisez la suite négative et exécutez-la en CI à chaque modification

Transformez les cas ci-dessus en une suite qui s'exécute sur chaque demande de tirage. Voici une version compacte et paramétrée en pytest qui atteint un point de terminaison de staging et affirme un refus contrôlé :

import httpx
import pytest

BASE = "https://staging.internal/v1"

HOSTILE_CONFIGS = [
    {"loader": "pickle://s3/models/payload.pkl", "format": "auto"},  # chargeur de code à distance
    {"loader": "csv", "name": "{{ 7*7 }}"},                          # injection de modèle
    {"loader": "csv", "name": "{{ config.__class__ }}"},             # parcours d'objet
    {"loader": "csv", "filter": "1); DROP TABLE datasets;--"},       # injection SQL
    {"loader": "csv", "name": "A" * 5_000_000},                      # champ surdimensionné
]

@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
    r = httpx.post(f"{BASE}/datasets", json={"config": config}, timeout=10)
    assert r.status_code in (400, 413, 422), r.text  # une limite qui dit non
    assert r.status_code < 500, "5xx signifie que la charge utile a atteint une logique qu'elle n'aurait pas dû"
    assert "49" not in r.text, "modèle rendu : injection de modèle côté serveur"

Connectez-le à votre CI afin qu'il contrôle les fusions. Un travail GitHub Actions minimal fait l'affaire :

name: api-abuse-tests
on: [push, pull_request]
jobs:
  negative-input:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/negative_input.py -q

C'est là qu'un outil axé sur le schéma trouve sa place. Dans Apidog, vous concevez le point de terminaison en fonction d'un contrat OpenAPI, de sorte que chaque requête et réponse est vérifiée par rapport à ce contrat pendant que vous testez. Vous pouvez enregistrer des scénarios négatifs juste à côté des scénarios de chemin heureux : champs surdimensionnés, types incorrects et les chaînes d'injection ci-dessus, chacune avec une assertion que le statut est un 4xx. Ensuite, vous exécutez les mêmes scénarios en CI via l'interface de ligne de commande Apidog, de sorte qu'un changement qui assouplit discrètement la validation échoue à la compilation au lieu d'être déployé. Si vous voulez l'essayer, Téléchargez Apidog et ajoutez un scénario négatif à un point de terminaison que vous avez déjà.

Soyez clair sur la limite. Apidog est un outil de conception, de test, de simulation et de documentation. Il n'exécute pas de pare-feu d'application web, ne filtre pas le trafic en direct, ni ne remplace un SIEM, et la validation des contrats pendant les tests ne détectera pas toutes les exploitations. Ce qu'il fait bien, c'est rendre le contrat explicite et vous tenir honnête sur ce qu'un point de terminaison accepte, de sorte que la catégorie « nous n'avons jamais vérifié » cesse d'être la chose qui vous surprend en production.

Questions fréquentes

Quelle est la différence entre les tests négatifs et le fuzzing ? Les tests négatifs envoient un ensemble sélectionné d'entrées incorrectes que vous avez choisies délibérément, une par échec qui vous intéresse. Le fuzzing envoie de grands volumes d'entrées aléatoires ou mutées pour trouver des cas auxquels vous n'aviez pas pensé. Commencez par les tests négatifs car ils sont rapides, déterministes et faciles à exécuter en CI. Ajoutez le fuzzing lorsque vous souhaitez une couverture plus large que votre propre imagination.

Ces tests doivent-ils être exécutés en production ? Non. Exécutez-les sur un environnement de staging ou isolé. Certains cas, comme la charge utile surdimensionnée ou une sonde d'injection de commande, sont conçus pour stresser le système, et quelques-uns pourraient modifier les données si un bug existe. Un environnement de test dédié permet aux tests d'être agressifs sans aucun risque pour les utilisateurs réels.

Un pare-feu ou un WAF ne devrait-il pas de toute façon intercepter cela ? Un WAF est une défense en profondeur utile, mais il ne remplace pas le refus d'une entrée incorrecte par l'application. Les règles peuvent être contournées, et un WAF ne peut pas connaître votre logique métier. L'objectif de ces tests est de prouver que le point de terminaison lui-même dit non, de sorte que vous ne dépendez pas d'un filtre que vous ne contrôlez pas entièrement.

Combien de cas négatifs sont suffisants par point de terminaison ? Visez un cas par champ par classe d'échec qu'il peut subir : type incorrect, hors plage, trop long, champ interdit et toute chaîne d'injection qui correspond à son format. Il s'agit généralement d'une poignée de cas par point de terminaison, pas de centaines. La couverture des classes est plus importante que le nombre brut.

La validation de schéma arrête-t-elle complètement l'injection ? Non, et ce ne devrait pas être votre seule couche. Un schéma strict élimine une grande partie des entrées mal formées et surdimensionnées et bloque les champs inattendus, mais une valeur peut être valide selon le schéma et toujours être une injection SQL ou de modèle. Maintenez les requêtes paramétrées, la désérialisation sûre et l'encodage de sortie en place, et utilisez le schéma pour réduire la surface que ces couches doivent défendre.

Pratiquez le Design-first d'API dans Apidog

Découvrez une manière plus simple de créer et utiliser des API