Comment sécuriser une API GraphQL en production
- Accueil
- Architecture Cyber
- Comment sécuriser une API GraphQL en production
Comment sécuriser une API GraphQL en production
Section intitulée « Comment sécuriser une API GraphQL en production »GraphQL expose une surface d’attaque différente d’une interface de programmation applicative (API) REST (Representational State Transfer) : le client choisit la forme des données et peut amplifier le coût serveur dans une seule requête. La recommandation est nette : imposer des budgets de profondeur et de complexité, désactiver l’introspection publique en production, puis traiter chaque résolveur comme un point d’autorisation et de consommation de ressources.
Une API GraphQL ressemble à un comptoir où l’on peut demander exactement les éléments voulus sur une même commande. C’est pratique pour éviter les allers-retours inutiles, mais une commande interminable peut bloquer toute la cuisine et révéler le contenu du menu réservé au personnel.
→ Lire l’explication complète de la marmotte
GraphQL, une surface d’attaque pilotée par le client
Section intitulée « GraphQL, une surface d’attaque pilotée par le client »Dans une API REST classique, le serveur fixe largement le coût d’un appel : GET /users/42 et GET /orders correspondent à des traitements connus, même si leur implémentation peut être désastreuse. GraphQL déplace une partie de cette décision chez le client. Une requête peut parcourir plusieurs relations, sélectionner de nombreux champs coûteux et demander des collections à chaque niveau.
Le compromis central est là : GraphQL réduit les échanges inutiles et donne une API expressive, mais il rend le coût d’exécution programmable par le client. Ce n’est pas un défaut de protocole. C’est une responsabilité d’architecture. Publier un schéma sans budget d’exécution revient à laisser chaque consommateur écrire un mini-plan de requête contre les dépendances internes. Les slides parlent de flexibilité, le processeur voit surtout une liste de courses sans fin.
Une requête imbriquée peut déclencher une explosion combinatoire. Un champ projects retourne 50 projets, chaque projet expose 50 membres, chaque membre expose 50 événements. Trois niveaux suffisent à produire 125 000 objets potentiels, avant sérialisation, contrôle d’accès ou appels vers une base de données. Limiter uniquement la taille de la réponse est insuffisant : le travail peut déjà avoir été exécuté avant que la réponse soit tronquée.
Le risque ne se limite pas au déni de service (DoS, Denial of Service) intentionnel. Un client légitime peut publier une requête coûteuse après une évolution du schéma, sans réaliser qu’un résolveur appelle un service distant par élément retourné. Le schéma reste valide, les tests unitaires passent, puis le pool de connexions tombe à genoux avec la dignité d’un serveur de démonstration pendant un salon.
Chaque champ GraphQL doit donc recevoir un coût explicite. Un champ scalaire local, tel qu’un identifiant ou un libellé déjà chargé, coûte peu. Une pagination sur base distante, un agrégat, une recherche plein texte ou un appel à un service tiers coûtent davantage. Cette classification doit refléter le chemin réel d’exécution, pas la valeur métier supposée du champ.
La limitation de profondeur seule ne suffit pas. Une profondeur de trois avec des listes non bornées reste coûteuse. La limitation de complexité seule ne suffit pas non plus si un résolveur déclaré peu coûteux déclenche en réalité une boucle d’appels distante. Les deux contrôles sont complémentaires, et leur calibration dépend des limites établies pour les bases de données, les services aval et les mécanismes d’observabilité.
Profondeur, complexité et pagination des requêtes
Section intitulée « Profondeur, complexité et pagination des requêtes »La profondeur mesure le nombre de niveaux traversés depuis la racine de la requête. Une limite comprise entre 6 et 10 convient souvent aux schémas métier exposés à des clients externes, mais aucun chiffre ne remplace l’examen des relations réelles. Une profondeur maximale de 8 ne protège rien si users(first: 10000) est autorisé à deux niveaux.
La complexité attribue un coût total à la requête. Pour une relation paginée, le coût doit dépendre de l’argument de pagination demandé. Un modèle courant consiste à calculer :
coût du champ = coût de base + (nombre d'éléments demandés × coût des enfants)Le paramètre first, ou son équivalent, doit être borné côté serveur, y compris lorsque le client fournit une valeur supérieure. Une valeur par défaut faible et un maximum strict sont nécessaires. Autoriser first: 10000 parce que le schéma annonce une pagination est une pagination de Schrödinger : elle existe dans la documentation, pas dans la protection de capacité.
Une requête doit également être rejetée avant l’exécution lorsqu’elle dépasse ses budgets. Interrompre après plusieurs secondes de calcul ne protège ni les connexions, ni les threads, ni les dépendances aval. La validation structurelle doit intervenir après le parsing et avant tout résolveur métier.
Les alias sont un contournement fréquent des limites naïves. Un attaquant peut appeler le même champ coûteux plusieurs fois sous des noms différents :
query { a: searchUsers(query: "x") { id } b: searchUsers(query: "x") { id } c: searchUsers(query: "x") { id }}Le compteur de profondeur reste bas. La complexité doit compter chaque occurrence, même lorsqu’un champ est répété avec les mêmes arguments. Une déduplication applicative peut améliorer les performances, mais elle ne doit pas devenir le contrôle de sécurité principal : elle dépend du comportement du résolveur et des caches.
Les fragments, directives conditionnelles et variables doivent être inclus dans le calcul. Compter uniquement les champs écrits en ligne dans une requête est une invitation à déplacer le problème dans un fragment. Les parseurs ne se laissent pas impressionner par la mise en page, heureusement pour eux.
Exemple de validation GraphQL avant exécution
Section intitulée « Exemple de validation GraphQL avant exécution »L’exemple suivant configure Apollo Server avec une limite de profondeur, une limite de complexité et l’interdiction de l’introspection. Les seuils sont des valeurs initiales à mesurer sous charge, jamais des constantes décoratives gravées dans un dépôt.
// npm install @apollo/server graphql graphql-depth-limit graphql-validation-complexity
import { ApolloServer } from "@apollo/server";import depthLimit from "graphql-depth-limit";import { createComplexityLimitRule,} from "graphql-validation-complexity";import { NoSchemaIntrospectionCustomRule,} from "graphql";
// Chaque champ de type liste doit aussi borner `first` dans son résolveur.const server = new ApolloServer({ typeDefs, resolvers,
validationRules: [ // Rejette une navigation trop profondément imbriquée. depthLimit(8),
// Rejette la requête avant l'appel des résolveurs si son coût dépasse 300. createComplexityLimitRule(300, { onCost: (cost) => console.info({ cost }, "graphql query cost"), }),
// À activer pour tout endpoint public de production. NoSchemaIntrospectionCustomRule, ],});Cette configuration n’est correcte que si les résolveurs imposent aussi leurs propres plafonds de pagination. Une règle de complexité estime le coût à partir du document GraphQL, pas du nombre réel de lignes que la base retourne après jointures, filtres et défauts d’indexation. Une recherche déclarée à coût 10 peut coûter 10 millisecondes ou 10 secondes selon l’index. Le budget GraphQL ne remplace ni les limites de requêtes de base de données, ni les délais d’expiration côté client et serveur.
Les requêtes persistées renforcent le modèle. Le client envoie l’identifiant cryptographique d’une requête préenregistrée au lieu d’un document libre, et le serveur refuse tout identifiant inconnu. Pour une API utilisée par des clients mobiles ou un frontal contrôlé, c’est le meilleur choix : l’espace des requêtes devient fini et auditable. Pour une API réellement destinée à des intégrateurs externes, cette approche impose un processus de publication de requêtes et réduit l’exploration. Le choix dépend donc de la gouvernance des consommateurs, pas d’une préférence esthétique pour les acronymes.
Les pipelines Git et CI doivent tester les requêtes persistées et les règles de validation contre le schéma versionné. Une modification de coût sur un champ sensible mérite un test de non-régression, au même titre qu’un changement de contrat.
Introspection de schéma et énumération de l’API
Section intitulée « Introspection de schéma et énumération de l’API »L’introspection permet à un client GraphQL de demander la description du schéma : types, champs, arguments, mutations et relations. Elle alimente les explorateurs interactifs, la complétion des éditeurs et la génération de documentation. En développement, elle est attendue. Sur un endpoint public de production, elle offre à un attaquant un catalogue très précis des capacités exposées.
Désactiver l’introspection en production est la position par défaut pour les API externes. Le gain est modeste contre un adversaire déterminé, qui peut encore observer les erreurs, tester les opérations connues ou lire un client public. Mais il supprime l’énumération exhaustive, réduit les erreurs de configuration et évite de divulguer des mutations administratives oubliées dans le schéma. La sécurité par obscurité n’est pas une stratégie, mais donner le plan du bâtiment à l’entrée n’en est pas une non plus.
Cette désactivation doit couvrir __schema et __type, y compris lorsqu’ils sont demandés via alias ou fragments. Un filtrage textuel de la chaîne de requête est à proscrire : les commentaires, espaces, alias et fragments le contournent immédiatement. La règle doit opérer sur l’arbre syntaxique analysé par le serveur GraphQL.
L’introspection désactivée ne remplace pas une revue du schéma. Les noms de champs, messages d’erreur, descriptions et types de retour ne doivent pas exposer d’informations internes inutilement. Une mutation resetAllTenantKeys reste un problème, même si personne ne peut la découvrir avec une requête automatique. L’autorisation doit être vérifiée dans chaque résolveur et, lorsque possible, dans la couche métier sous-jacente.
Une séparation d’endpoints est parfois justifiée : introspection et explorateur activés sur un environnement d’intégration authentifié, endpoint de production sans introspection. Exposer un bac à sable de production avec un jeton administrateur parce que l’outil est pratique transforme vite la commodité en incident. La commodité est un excellent ingénieur commercial, moins bon gardien de secrets.
Autorisation, erreurs et conditions d’exploitation
Section intitulée « Autorisation, erreurs et conditions d’exploitation »GraphQL ne garantit ni l’authentification, ni l’autorisation par champ, ni l’isolation entre locataires. Un jeton JSON Web Token (JWT) valide prouve seulement ce que son émetteur affirme sur son porteur, jusqu’à expiration ou révocation effective. Il ne permet pas de déduire qu’un utilisateur peut lire une ressource référencée par un identifiant fourni dans une requête.
Chaque résolveur qui accède à une donnée protégée doit vérifier l’identité, le périmètre de locataire et l’autorisation métier. Vérifier uniquement le champ racine est insuffisant : une relation enfant peut traverser une frontière de tenant ou révéler un attribut sensible. Le chargement par lot réduit le problème classique N+1, où une liste déclenche une requête de base par élément, mais il ne remplace aucune vérification d’accès.
Les erreurs GraphQL doivent être classées. En production, retourner une erreur générique au client et journaliser le détail côté serveur. Les traces de pile, requêtes de base de données, noms de tables et exceptions de dépendances sont des données de reconnaissance. Les journaux ne doivent pas contenir les jetons d’accès, cookies, mots de passe ou variables contenant des secrets. Une API qui masque ses erreurs au client mais copie ses en-têtes dans les logs vient seulement de déplacer la fuite.
La supervision doit suivre au minimum : profondeur observée, complexité calculée, requêtes rejetées, durée par opération, nombre de résolveurs, erreurs par champ et saturation des dépendances aval. Les identifiants d’opération persistée sont plus exploitables que le texte complet des requêtes, qui peut contenir des données personnelles. Les principes de sécurité réseau et identité restent applicables : chiffrement du transport, filtrage d’entrée, limitation de débit par identité et par origine, délais d’expiration cohérents.
Les secrets de signature des jetons, clés d’accès aux bases et identifiants de services aval doivent être fournis au processus par un gestionnaire de secrets, jamais ajoutés au schéma, aux variables de requête ou au dépôt. Leur rotation doit accepter un recouvrement contrôlé entre ancienne et nouvelle clé, sinon les sessions actives cessent brutalement de fonctionner. Les sauvegardes doivent inclure les configurations de limites, les requêtes persistées et les politiques d’autorisation. Restaurer les données sans restaurer les garde-fous recrée une API fonctionnelle, donc dangereuse.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans budget calculé par opération, la limitation de débit comptée par requête HTTP ne mesure plus rien d’utile : une seule requête peut porter des centaines d’alias ou un lot d’opérations, et chacun s’exécute comme un appel distinct. Une mutation de connexion, de vérification de code à usage unique ou de réinitialisation de mot de passe peut alors être essayée des centaines de fois dans un seul envoi, alors que le pare-feu applicatif et les journaux d’accès n’enregistrent qu’un appel réussi. Le verrouillage de compte fondé sur le nombre de requêtes ne se déclenche jamais, et la détection perd sa corrélation : aucun pic de trafic n’apparaît, seule la durée d’exécution trahit l’essai massif, à condition qu’elle soit mesurée par opération.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Une API GraphQL permet à un programme client de demander plusieurs données dans une seule requête. Le client choisit les champs demandés et peut suivre des liens entre les données, par exemple demander des comptes, leurs projets et les membres de chaque projet. Le serveur lit cette demande, vérifie qu’elle respecte les règles du schéma, puis lance les traitements nécessaires pour produire la réponse.
Le problème apparaît quand la demande contient trop de niveaux ou réclame trop d’éléments à chaque niveau. Le serveur peut alors faire beaucoup de calculs, interroger plusieurs bases de données et attendre des services externes. Si trop de demandes coûteuses arrivent, le service ralentit ou devient indisponible.
Avant d’exécuter une demande, le serveur doit donc compter ses niveaux et estimer son coût. Si la limite est dépassée, il refuse la demande sans lancer les traitements. Chaque liste doit aussi avoir un nombre maximal d’éléments, même si le client demande davantage.
Le serveur peut révéler la liste complète de ses données et opérations disponibles. Cette fonction aide les développeurs pendant la création du service, mais elle donne aussi des informations à un attaquant. Elle doit être désactivée sur le service public de production.
Enfin, une demande autorisée par sa taille ne donne pas automatiquement le droit de lire les données. Chaque accès doit vérifier l’identité de la personne, ses droits et son périmètre.
À lire aussi
- Sécuriser une API gateway comme point de contrôleArchitecture de sécurité d'une API gateway comme point de contrôle centralisé, avec ses limites, ses attaques et ses conditions d'exploitation.
- Sécuriser un contrat API-first dès la conceptionConcevoir un contrat d’API comme une frontière de confiance, avec authentification, autorisation, validation et exploitation sécurisées.
- Comment sécuriser les webhooks entrantsAuthentifier un webhook par signature HMAC, bloquer les rejouements et rendre son traitement idempotent.
- Principes et architecture de l'événementielConcevoir une architecture événementielle fiable : contrats, livraison, cohérence, sécurité, exploitation et documentation.
Pages voisines
La citent
- Circuit breaker et bulkhead contre la propagation des pannes
- Sécuriser un contrat API-first dès la conception
- Threat modeling STRIDE avant écrire le code
Comment sécuriser une API GraphQL en production
Elle cite
