Sécurité GraphQL : Les requêtes qui peuvent faire tomber tout votre backend 🌀

GraphQL a révolutionné le développement d’API en offrant une flexibilité sans précédent dans la récupération de données. Contrairement aux API REST traditionnelles qui nécessitent plusieurs points d’accès, GraphQL permet aux clients de demander exactement les données dont ils ont besoin via un seul endpoint. Cependant, cette puissance s’accompagne d’un revers—des vulnérabilités de sécurité qui n’existent pas dans les architectures REST. Une seule requête malveillante peut mettre à genoux toute votre infrastructure backend, et de nombreux développeurs ne réalisent pas le danger avant qu’il ne soit trop tard.
La crise de sécurité GraphQL dont personne ne parle
Alors que l’adoption de GraphQL continue de croître dans les environnements d’entreprise et de startups, la sensibilisation à la sécurité n’a pas suivi le rythme. Des recherches récentes montrent que 13,4 % des vulnérabilités détectées dans les implémentations GraphQL sont spécifiques au langage et à ses frameworks, indiquant que les organisations n’ont pas encore appris à gérer correctement les nouveaux risques liés à cette technologie. Plus alarmant encore, 80 % de ces problèmes auraient pu être évités en appliquant des bonnes pratiques de sécurité de base.
Le problème fondamental vient de la philosophie de conception de GraphQL : donner aux clients un contrôle maximal sur leurs requêtes de données. Si cela accélère le développement frontend et réduit le sur-fetching, cela ouvre aussi la porte à l’abus. Dans les API REST, les points d’accès sont prédéfinis et limités—un attaquant sait exactement ce que fait chaque endpoint et ne peut pas combiner arbitrairement des opérations. À l’inverse, GraphQL considère chaque requête comme une demande personnalisée que le serveur doit analyser, valider et exécuter.
L’anatomie d’une attaque par déni de service (DoS) sur GraphQL
Les API GraphQL sont particulièrement vulnérables aux attaques de type Denial of Service, où des attaquants envoient une ou plusieurs requêtes qui submergent le serveur d’application en raison du fonctionnement fondamental de GraphQL. Examinons les vecteurs d’attaque les plus dangereux.
Requêtes récursives profondes : la mort par nesting
La vulnérabilité la plus insidieuse exploite la nature relationnelle de votre schéma. Imaginez une plateforme de médias sociaux avec un schéma simple où les Utilisateurs ont des Amis, et les Amis sont aussi des Utilisateurs. Un attaquant peut créer une requête comme celle-ci :
query MaliciousQuery {
user(id: "123") {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
id
name
email
}
}
}
}
}
}
}
}
}
}
}
Cette requête apparemment innocente peut faire exploser exponentiellement vos opérations en base de données. Si chaque utilisateur a seulement 50 amis, une requête à 10 niveaux de profondeur tente de charger 50^10 utilisateurs—soit 97 656 250 000 000 000 de records. Votre base de données s’effondrera bien avant d’avoir terminé cette opération.
Ces requêtes imbriquées massives augmentent considérablement le nombre d’objets chargés et peuvent finir par faire planter tout le serveur. Même avec l’optimisation des requêtes et le caching, la surcharge computationnelle du traitement de relations profondément imbriquées devient insurmontable.
Attaques par requêtes cycliques : boucles infinies dans votre schéma
Lié à la récursion profonde, les requêtes cycliques exploitent les relations bidirectionnelles dans votre schéma. Si les Albums contiennent des Chansons, et que les Chansons référencent leur Album parent, un attaquant peut créer une boucle infinie :
query CyclicAttack {
album(id: "456") {
songs {
album {
songs {
album {
songs {
# Cela continue indéfiniment
}
}
}
}
}
}
}
Sans protections appropriées, l’aspect relationnel de GraphQL devient une vulnérabilité exploitable via des requêtes profondes et cycliques, provoquant la surcharge et le crash de votre API.
Amplification par batch de requêtes
GraphQL supporte le batching, permettant aux clients d’envoyer plusieurs opérations dans une seule requête HTTP. Si cela améliore la performance pour des cas légitimes, un attaquant peut en profiter en envoyant des centaines de requêtes complexes simultanément :
[
{ query: expensiveQuery1 },
{ query: expensiveQuery2 },
{ query: expensiveQuery3 },
# ... répété 100 fois
]
Chaque requête peut être gérée individuellement, mais exécuter 100 opérations gourmandes en ressources en parallèle peut submerger votre CPU, votre mémoire et vos connexions à la base. Cet effet d’amplification transforme une seule requête HTTP en une attaque distribuée de déni de service depuis l’intérieur.
Reconnaissance via introspection
La fonctionnalité d’introspection de GraphQL permet aux clients d’interroger le schéma lui-même, découvrant tous les types, champs et opérations disponibles. Bien qu’indispensable en développement, laisser l’introspection activée en production est un cauchemar pour la sécurité. Les attaquants peuvent utiliser l’introspection pour cartographier tout votre modèle de données, identifier les champs sensibles, et élaborer des attaques ciblées.
query IntrospectionQuery {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
Cette requête unique révèle toute la surface de votre API, y compris des champs que vous n’aviez pas prévu d’exposer publiquement. Avec cette information, les attaquants peuvent systématiquement rechercher des vulnérabilités et élaborer des stratégies d’exploitation précises.
Le coût computationnel caché
Contrairement aux endpoints REST où le coût computationnel est relativement prévisible, les requêtes GraphQL ont une complexité variable entièrement déterminée par le client. Une requête demandant le nom d’un seul utilisateur coûte presque rien, alors qu’une requête traversant plusieurs relations et demandant des centaines de champs peut déclencher des jointures complexes et des agrégations dans votre base.
Les problèmes de timeout dus à des requêtes excessivement complexes ou détaillées constituent une vulnérabilité majeure, qu’elle soit accidentelle ou malveillante. Sans analyse du coût des requêtes, vous ne pouvez pas distinguer une requête légitime complexe d’une attaque jusqu’à ce que vos serveurs soient déjà en difficulté.
Impact réel et études de cas
Les risques théoriques d’attaques GraphQL se sont concrétisés dans des incidents de sécurité réels. CVE-2021-41248 a documenté une vulnérabilité dans GraphiQL, un IDE GraphQL populaire, où les réponses d’introspection du schéma pouvaient être exploitées pour des attaques XSS. Bien que cette vulnérabilité spécifique ciblait l’outil de développement plutôt que les API en production, elle montre comment les fonctionnalités spécifiques à GraphQL créent de nouvelles surfaces d’attaque.
Plus largement, des organisations utilisant GraphQL en production sans mesures de sécurité appropriées ont subi des interruptions complètes de service suite à des attaques par requêtes récursives. Dans plusieurs cas documentés, même de petits tests de requêtes profondément imbriquées ont accidentellement mis hors service des environnements de staging, révélant la fragilité des API GraphQL non protégées.
Stratégies de mitigation complètes
Protéger votre API GraphQL nécessite une stratégie de défense multicouche qui aborde chaque vecteur d’attaque de manière systématique.
Limitation de la profondeur des requêtes : votre première ligne de défense
Mettre en place une limite maximale de profondeur de requête est la protection la plus simple contre les attaques par requêtes récursives. Des outils comme graphql-depth-limit permettent une mise en œuvre facile de restrictions de profondeur sur les requêtes entrantes. Lors de la configuration, analysez vos requêtes légitimes pour déterminer la profondeur maximale nécessaire.
Par exemple, si votre requête la plus complexe nécessite 7 niveaux de nesting, fixez la limite à 10—offrant une marge tout en bloquant les requêtes profondément malveillantes. La mise en œuvre implique généralement l’ajout d’une règle de validation à votre serveur GraphQL :
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)]
});
Cependant, la limitation simple de la profondeur a ses limites. Si votre schéma utilise le pattern Relay Cursor Connection, une profondeur plus grande peut être nécessaire. De plus, la protection contre les cycles auto-référencés nécessite une configuration séparée, avec des profondeurs maximales recommandées de seulement 2 pour éviter que des relations circulaires soient exploitées.
Analyse de complexité et de coût des requêtes
La limitation de profondeur seule n’est pas suffisante, car une requête peut être coûteuse sans être profonde. L’analyse de complexité de requête attribue un coût computationnel à chaque champ de votre schéma et calcule le coût total des requêtes entrantes. Les champs qui déclenchent des jointures, des calculs complexes ou des appels API tiers reçoivent des scores de coût plus élevés.
Vous pouvez implémenter une analyse de coût avec des bibliothèques intégrées à votre serveur GraphQL, en établissant un seuil maximum de coût de requête. Lorsqu’une requête dépasse ce seuil, elle est rejetée avant exécution :
const costAnalysis = require('graphql-cost-analysis');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
costAnalysis({
maximumCost: 1000,
defaultCost: 1,
variables: {},
onComplete: (cost) => console.log('Coût de la requête :', cost)
})
]
});
L’enjeu réside dans l’attribution précise des coûts aux champs. Cela nécessite de profiler vos resolvers pour comprendre leur impact réel et ajuster les attributions de coûts en conséquence. Avec le temps, vous développerez un modèle de coût précis qui protège contre l’épuisement des ressources tout en permettant des requêtes complexes légitimes.
Application de timeouts à plusieurs niveaux
Mettre en place des timeouts agressifs garantit qu’une requête malveillante, même si elle passe outre la profondeur et la complexité, ne monopolise pas indéfiniment les ressources du serveur. Configurez des timeouts à plusieurs niveaux :
- Timeout d’exécution GraphQL : arrêter le traitement après une durée fixe
- Timeout des requêtes à la base : empêcher les opérations longues
- Timeout HTTP : limiter le temps total de traitement de la requête
Ces timeouts en couches créent une défense en profondeur, empêchant qu’une seule requête malveillante ne bloque indéfiniment le serveur.
Désactivation de l’introspection en production
Les vulnérabilités d’introspection proviennent généralement de défauts d’implémentation et de conception, notamment lorsque cette fonctionnalité reste active en production. La désactiver en production doit être une pratique standard :
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production'
});
Cela empêche les attaquants de cartographier facilement votre schéma tout en laissant l’introspection active en développement et en test, où elle est réellement utile.
Limitation de débit et throttling des requêtes
La limitation de débit classique basée sur l’IP ou les clés API reste essentielle pour les API GraphQL. Elle empêche les clients d’envoyer trop de requêtes et doit toujours être combinée avec d’autres protections spécifiques à GraphQL. Envisagez d’utiliser des limites glissantes qui suivent la complexité des requêtes dans le temps plutôt que de simples comptages de requêtes.
Les implémentations avancées peuvent ajuster les limites en fonction du coût des requêtes—permettant plus de requêtes simples mais moins de requêtes complexes dans la même fenêtre temporelle. Cette approche offre une meilleure protection des ressources tout en maintenant une bonne expérience utilisateur.
Liste blanche de requêtes pour les environnements à haute sécurité
Pour les environnements nécessitant une sécurité maximale, envisagez la mise en place d’une liste blanche de requêtes (aussi appelée requêtes persistantes). Dans ce modèle, les clients ne peuvent exécuter que des requêtes pré-approuvées, revues et enregistrées sur le serveur. Toute requête non sur la liste est automatiquement rejetée.
Bien que cette approche sacrifie la flexibilité de GraphQL, elle offre une protection infaillible contre les requêtes malveillantes. Elle est particulièrement adaptée aux API publiques ou aux scénarios où vous contrôlez à la fois le client et le serveur.
Surveillance et alertes
Mettre en œuvre des contrôles de sécurité n’a de sens que si vous surveillez leur efficacité. Établissez une surveillance complète pour :
- Taux et raisons de rejet des requêtes (profondeur, complexité, timeout)
- Tendances de la complexité moyenne des requêtes
- Modèles de requêtes anormaux
- Utilisation des ressources du serveur en corrélation avec la complexité des requêtes
Alertez en cas d’augmentation soudaine des requêtes rejetées, ce qui peut indiquer une attaque en cours, ou une augmentation progressive de la complexité moyenne, signalant des motifs d’attaque évolutifs ou la nécessité d’ajuster les seuils de sécurité.
Bonnes pratiques pour une implémentation sécurisée de GraphQL
Au-delà des contrôles techniques spécifiques, suivez ces principes architecturaux :
Concevez des schémas de manière défensive. Minimisez les relations bidirectionnelles qui permettent des requêtes cycliques. Lorsqu’elles sont nécessaires, documentez-les clairement et assurez-vous que des limites de profondeur appropriées sont configurées.
Mettez en œuvre une pagination correcte. Utilisez la pagination par curseur avec des limites de taille maximale pour éviter que les attaquants ne demandent des ensembles de données entiers en une seule requête.
Validez et nettoyez rigoureusement les entrées. Les attaques par injection comme SQL injection ou cross-site scripting sont adaptées à GraphQL via des entrées non nettoyées permettant l’exécution de code malveillant. Traitez les variables de requête GraphQL avec la même paranoïa que pour les formulaires classiques.
Appliquez une authentification et une autorisation au niveau des champs. La granularité de GraphQL nécessite une sécurité au niveau des champs. Ne vous fiez pas uniquement à l’authentification au niveau de l’endpoint.
Audits de sécurité réguliers et tests. Testez périodiquement votre API GraphQL avec des requêtes malveillantes intentionnelles pour vérifier que vos contrôles de sécurité fonctionnent comme prévu. Des outils de scan de sécurité automatisés spécifiquement conçus pour GraphQL peuvent aider à identifier les vulnérabilités avant que les attaquants ne le fassent.
La voie à suivre
Les défis de sécurité de GraphQL ne sont pas insurmontables, mais ils nécessitent un effort conscient et une discipline architecturale. La flexibilité qui rend GraphQL puissant le rend aussi dangereux s’il n’est pas correctement sécurisé. À mesure que l’adoption de GraphQL s’accélère, les pratiques de sécurité doivent évoluer, passant de simples considérations à des éléments fondamentaux de la conception d’API.
Avec 80 % des problèmes de sécurité GraphQL évitables via des bonnes pratiques de base, il n’y a aucune excuse pour faire fonctionner des API GraphQL non protégées en production. Les techniques décrites ici—limitation de la profondeur des requêtes, analyse de complexité, application de timeouts, contrôle de l’introspection, et surveillance complète—offrent une posture de sécurité robuste qui préserve les avantages de GraphQL tout en atténuant ses risques.
La question n’est pas de savoir si votre API GraphQL sera confrontée à des requêtes malveillantes, mais quand. En mettant en œuvre ces protections de manière proactive, vous vous assurez que lorsque les attaquants frapperont à votre porte, votre backend restera debout. Dans le paysage évolutif de la sécurité des API, GraphQL représente à la fois une opportunité énorme et un risque important. Votre choix entre ces deux options déterminera si GraphQL devient votre avantage concurrentiel ou votre vulnérabilité la plus critique.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.