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

작성자

카테고리:

← 피드로
DEV Community · Antoine Laurent · 2026-07-23 개발(SW)

TL;DR : L’entrée de votre API est une surface d’attaque : testez-la 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 vérifiez que le point de terminaison répond par un 4xx, jamais par un 5xx. Transformez la validation de schéma en 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 dit presque rien sur ce qui se passe lorsqu’un 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, fichiers téléversés, charges utiles de webhook et JSON assemblé à la volée par un agent IA. Traitez toutes ces entrées comme si quelqu’un finira par en envoyer la pire version possible.

Essayez Apidog dès aujourd’hui

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 couvre la partie pratique.

Vous allez :

  1. créer des tests qui envoient le type d’entrée qu’un attaquant envoie ;
  2. vérifier que l’API refuse ces entrées de manière contrôlée ;
  3. exécuter ces tests automatiquement à chaque modification.

Les catégories s’alignent sur le Top 10 de la sécurité des API d’OWASP, à garder ouvert dans un onglet. Apidog peut servir à concevoir le contrat et à piloter ces tests, mais les principes s’appliquent à n’importe quel framework.

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

La validation est souvent traitée comme une amélioration de l’expérience utilisateur : intercepter un e-mail vide, afficher une bordure rouge, puis passer à autre chose. Ce cadrage est insuffisant.

Chaque champ accepté par votre API est une promesse que l’appelant peut rompre. Chaque promesse rompue est un chemin vers votre logique :

  • un paramètre limit attendu comme petit entier devient 999999999 ;
  • un filename attendu comme un mot devient ../../etc/passwd ;
  • un objet config attendu comme ensemble de 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à, appliqués aux champs les plus susceptibles de vous nuire.

Pour chaque champ, posez-vous systématiquement cette question :

Quelle est la pire chose qui puisse tenir dans ce champ ?

C’est déjà la moitié des pratiques décrites dans notre guide sur les meilleures pratiques de sécurité des API. Le reste consiste à transformer cette question en tests automatisés.

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

L’incident Hugging Face illustre pourquoi l’entrée mérite cette attention. Hugging Face a déclaré que le vecteur d’entrée était constitué d’ensembles de données malveillants : un ensemble de données conçu a déclenché un chargeur d’ensemble de données permettant l’exécution de code à distance, et une injection de modèle était présente dans une configuration d’ensemble de données. Consultez le rapport d’incident de sécurité de l’entreprise.

La forme de l’échec est importante :

  1. un point de terminaison accepte quelque chose décrit comme des données ;
  2. le chargement de ces données suit un chemin de code ;
  3. ce chemin peut exécuter des instructions contrôlées par l’attaquant.

Autrement dit, « charger ces données » devient « exécuter ce code ».

L’injection de modèle suit le même mécanisme à plus petite échelle : une valeur de configuration censée être du texte inerte est évaluée, donc ce texte devient une exécution.

La leçon n’est pas que Hugging Face a commis une erreur rare. Tout point de terminaison qui accepte un nom de chargeur, un format, un modèle, un objet sérialisé ou un blob de configuration peut accepter des instructions, intentionnellement ou non.

Si vous n’avez jamais testé une configuration hostile sur ce point de terminaison, vous n’avez jamais vérifié qu’elle reste inerte.

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

Le contrôle le moins coûteux à ajouter est un schéma strict à la périphérie de l’API.

Un schéma n’est pas seulement de la documentation. Lorsqu’il rejette tout ce qui ne correspond pas, il devient un filtre exécuté avant que votre logique métier ne voie la requête. JSON Schema fournit les primitives nécessaires.

Voici un schéma de configuration de jeu de données qui bloque la plupart des entrées hostiles avant qu’elles n’atteignent le code applicatif :

{
  "$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
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Ce schéma applique quatre défenses distinctes :

Règle Effet de sécurité additionalProperties: false Rejette un champ dissimulé tel que template. enum sur loader Bloque pickle:// et les chargeurs non explicitement autorisés. maxLength Empêche les chaînes de plusieurs mégaoctets visant à épuiser la mémoire. pattern sur name Refuse notamment {{ et '; DROP TABLE avant qu’ils n’atteignent les couches suivantes.

Ces règles ne cherchent pas à reconnaître tous les attaquants. Elles n’acceptent que l’ensemble étroit des entrées réellement prises en charge. Cette étroitesse est une propriété de sécurité.

La validation de contrat ne bloque pas toutes les exploitations. En revanche, elle ferme une catégorie très courante de bugs : « nous n’avons jamais vérifié ce que ce point de terminaison accepte ».

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

Les tests de chemin heureux vérifient qu’une bonne entrée produit une bonne sortie.

Les tests négatifs vérifient qu’une mauvaise entrée produit un refus contrôlé.

Cette distinction compte : un refus est une fonctionnalité. Un 400 avec une erreur claire signifie que votre API défend sa frontière. Un 500 signifie qu’elle a perdu le contrôle de l’entrée.

Méthode de construction d’un test négatif

Pour chaque champ :

  1. listez les entrées qu’il doit rejeter ;
  2. envoyez une entrée invalide à la fois ;
  3. vérifiez que la réponse est un 4xx ;
  4. vérifiez qu’elle n’est jamais un 5xx ;
  5. lorsque c’est possible, vérifiez qu’aucun effet secondaire n’a eu lieu.

Utilisez cette grille de départ :

Classe d’échec Exemple Mauvais type Envoyer une chaîne au lieu d’un entier Champ requis absent Omettre name Champ interdit Ajouter template lorsque additionalProperties est désactivé Hors plage Envoyer rows: -1 ou rows: 999999999 Trop long Envoyer une chaîne de plusieurs mégaoctets Injection adaptée au champ Envoyer une sonde SQL, shell ou modèle

Notre liste de contrôle des tests de sécurité des API contient une liste de départ champ par champ à adapter.

Une règle garantit l’honnêteté de la suite : affirmez le comportement, pas le texte exact de l’erreur.

Évitez ceci :

assert response.json()["message"] == "chargeur invalide"

Enter fullscreen mode Exit fullscreen mode

Une refactorisation inoffensive casserait le test et encouragerait l’équipe à le relâcher.

Préférez ceci :

assert response.status_code in (400, 413, 422)
assert response.status_code < 500

Enter fullscreen mode Exit fullscreen mode

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

Certaines familles d’injection sont assez fréquentes pour mériter des tests permanents, plutôt qu’une vérification manuelle ponctuelle.

Vous n’avez pas besoin d’être exhaustif. Un cas de sondage par classe suffit souvent à faire échouer bruyamment une régression. Les outils de détection automatisée des vulnérabilités d’API peuvent compléter la couverture plus tard.

Injection SQL

Envoyez :

1); DROP TABLE datasets;--

Enter fullscreen mode Exit fullscreen mode

dans tout champ susceptible d’atteindre une requête SQL.

Le point de terminaison doit traiter cette valeur comme une donnée littérale et répondre par un 400, ou renvoyer un résultat vide. Il ne doit jamais exposer une erreur de base de données.

Injection de modèle

Envoyez ces charges utiles dans les champs de nom et d’étiquette :

{{ 7*7 }}

Enter fullscreen mode Exit fullscreen mode

{{ config.__class__ }}

Enter fullscreen mode Exit fullscreen mode

Si une réponse contient un jour 49, un moteur de modèle a évalué votre entrée. C’est un signal critique.

Désérialisation non sûre et chargeurs de code à distance

Envoyez :

{
  "loader": "pickle://s3/models/payload.pkl"
}

Enter fullscreen mode Exit fullscreen mode

Ou envoyez un objet sérialisé là où une valeur simple est attendue.

C’est la forme même de l’incident Hugging Face. Rejetez les chargeurs inconnus via une liste blanche ; n’essayez pas d’être serviable avec des formats non prévus.

Injection de commande

Envoyez :

; id

Enter fullscreen mode Exit fullscreen mode

et :

$(id)

Enter fullscreen mode Exit fullscreen mode

dans tout champ susceptible de devenir un argument shell, par exemple un nom de fichier ou une option de conversion.

Un 200 qui divulgue un identifiant utilisateur est une découverte critique, pas une curiosité.

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

Toutes les entrées hostiles ne sont pas des chaînes sophistiquées. Certaines sont simplement trop grandes ou mal formées. Elles peuvent casser les analyseurs avant même l’exécution de votre logique de validation.

Tester les limites de taille

Envoyez :

  • un champ contenant cinq mégaoctets d’un même caractère ;
  • un tableau JSON contenant un million d’éléments.

Une API saine applique une limite de taille de corps et renvoie un 413, plutôt que d’allouer de la mémoire jusqu’à s’effondrer.

Tester les corps mal formés

Envoyez notamment :

  • du JSON tronqué ;
  • une virgule finale ;
  • du JSON imbriqué sur mille niveaux.

La bonne réponse est un 400 rapide, pas un worker bloqué.

Tester la confusion de Content-Type

Testez les incohérences entre en-tête et corps :

En-tête déclaré Corps envoyé Ce que vous testez application/json XML Le serveur analyse-t-il le corps malgré l’incohérence ? application/xml XML avec entité externe Une sonde XXE est-elle traitée ? text/plain JSON Un analyseur laxiste accepte-t-il quand même le JSON ?

Le serveur doit exiger que le type déclaré et le contenu soient cohérents avant d’analyser quoi que ce soit.

Pourquoi les agents IA augmentent les enjeux

Tout ce qui précède était vrai avant les agents IA. Les agents changent le volume et la vitesse.

Un attaquant humain envoie généralement une requête hostile à la fois. Un agent IA peut générer et transmettre des charges utiles à la vitesse de la machine, y compris des entrées qu’une personne n’aurait jamais pensé à essayer.

Trois propriétés aggravent le problème :

  1. Les agents synthétisent l’entrée. Ils produisent des valeurs qu’aucun humain n’a écrites et qu’aucun test n’avait anticipées.
  2. Les agents réessaient et enchaînent les appels. Un document amont empoisonné peut déclencher des milliers de requêtes hostiles en quelques secondes.
  3. Les agents transmettent des données considérées comme fiables. Une charge utile cachée dans un ensemble de données ou un webhook devient une requête réelle vers votre API.

Le modèle où « charger ces données » devient « exécuter ce code » correspond exactement au type d’instruction qu’un agent peut transporter au-delà d’une limite de confiance sans le remarquer.

Notre article sur l’injection de prompt pour les équipes API approfondit cette transmission.

La défense ne change pas : elle doit devenir automatique, car vous ne pouvez pas examiner manuellement le trafic des agents.

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

Transformez les cas précédents en suite exécutée sur chaque pull request.

Voici une version compacte avec pytest qui cible un environnement de staging et vérifie un refus contrôlé :

import httpx
import pytest

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

HOSTILE_CONFIGS = [
    {"loader": "pickle://s3/models/payload.pkl", "format": "auto"},
    {"loader": "csv", "name": "{{ 7*7 }}"},
    {"loader": "csv", "name": "{{ config.__class__ }}"},
    {"loader": "csv", "filter": "1); DROP TABLE datasets;--"},
    {"loader": "csv", "name": "A" * 5_000_000},
]

@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
    response = httpx.post(
        f"{BASE}/datasets",
        json={"config": config},
        timeout=10,
    )

    assert response.status_code in (400, 413, 422), response.text
    assert response.status_code < 500, (
        "Un 5xx signifie que la charge utile a atteint une logique "
        "qu'elle n'aurait pas dû atteindre."
    )
    assert "49" not in response.text, (
        "Modèle rendu : injection de modèle côté serveur."
    )

Enter fullscreen mode Exit fullscreen mode

Ajoutez ensuite cette suite à votre CI afin qu’elle bloque les fusions.

Exemple minimal avec GitHub Actions :

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

Enter fullscreen mode Exit fullscreen mode

Utiliser un contrat OpenAPI dans la boucle

C’est là qu’un outil orienté schéma est utile. Dans Apidog, vous concevez le point de terminaison à partir d’un contrat OpenAPI, puis vérifiez les requêtes et réponses par rapport à ce contrat pendant les tests.

Ajoutez les scénarios négatifs à côté des scénarios de chemin heureux :

  • champs surdimensionnés ;
  • types incorrects ;
  • propriétés inattendues ;
  • chaînes d’injection SQL, shell et modèle ;
  • corps mal formés ;
  • incohérences de type de contenu.

Pour chaque scénario, ajoutez une assertion indiquant que le statut est un 4xx. Exécutez ensuite les mêmes scénarios en CI via l’interface de ligne de commande Apidog. Un changement qui assouplit silencieusement la validation échouera à la compilation au lieu d’être déployé.

Pour commencer, téléchargez Apidog et ajoutez un scénario négatif à un point de terminaison existant.

Soyez clair sur la limite : Apidog est un outil de conception, de test, de simulation et de documentation. Il n’exécute pas de WAF, ne filtre pas le trafic en direct et ne remplace pas un SIEM. La validation de contrat pendant les tests ne détectera pas toutes les exploitations.

En revanche, elle rend le contrat explicite et vous oblige à vérifier ce qu’un point de terminaison accepte. La catégorie « nous n’avons jamais vérifié » cesse alors d’être celle 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, choisies délibérément selon les échecs qui vous intéressent.

Le fuzzing envoie de grands volumes d’entrées aléatoires ou mutées pour découvrir des cas auxquels vous n’aviez pas pensé.

Commencez par les tests négatifs : ils sont rapides, déterministes et faciles à exécuter en CI. Ajoutez le fuzzing lorsque vous avez besoin d’une couverture plus large que votre propre imagination.

Ces tests doivent-ils être exécutés en production ?

Non. Exécutez-les dans un environnement de staging ou isolé.

Certains cas, comme les charges utiles surdimensionnées ou les sondes d’injection de commande, sont conçus pour stresser le système. Certains pourraient modifier des données si un bug existe.

Un environnement de test dédié vous permet d’être agressif sans risque pour les utilisateurs réels.

Un pare-feu ou un WAF ne devrait-il pas 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 connaît pas votre logique métier. L’objectif de ces tests est de prouver que le point de terminaison lui-même dit non, sans dépendre uniquement 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 et par classe d’échec applicable :

  • type incorrect ;
  • valeur hors plage ;
  • valeur trop longue ;
  • champ interdit ;
  • chaîne d’injection adaptée au format du champ.

Cela représente généralement une poignée de cas par point de terminaison, pas des centaines. La couverture des classes d’échec est plus importante que le volume brut.

La validation de schéma bloque-t-elle complètement l’injection ?

Non. Elle ne doit pas être votre seule couche de défense.

Un schéma strict élimine une grande partie des entrées mal formées ou surdimensionnées et bloque les champs inattendus. Mais une valeur peut être valide selon le schéma tout en servant d’injection SQL ou de modèle.

Conservez donc :

  • les requêtes paramétrées ;
  • une désérialisation sûre ;
  • l’encodage de sortie ;
  • les contrôles d’autorisation adaptés ;
  • la validation de schéma pour réduire la surface à défendre.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다