Le AI Pentest Engine découvre une BFLA critique sur WebSocket dans des subscriptions GraphQL
L'AI Pentest Engine d'Ostorlab a mis au jour de manière systématique une vulnérabilité critique de Broken Function-Level Authorization (BFLA) sur un endpoint WebSocket GraphQL, qui permettait un accès non authentifié à un service de traduction en temps réel. Cette étude de cas détaille, étape par étape, la démarche de l'IA, de la découverte à la preuve de concept.
Introduction
Les applications web modernes s'appuient de plus en plus sur des protocoles de communication en temps réel comme les WebSockets pour offrir des expériences utilisateur dynamiques. Associée à GraphQL, cette combinaison permet un échange de données puissant et efficace grâce aux subscriptions. Cependant, cette complexité introduit aussi de nouvelles surfaces d'attaque, en particulier autour de l'autorisation. Sécuriser les subscriptions GraphQL exige des contrôles rigoureux à plusieurs étapes : la connexion, l'initiation de la subscription et le filtrage des données à chaque événement.
L'AI Pentest Engine d'Ostorlab est conçu pour naviguer dans cette complexité. Il identifie et valide de façon systématique des failles d'autorisation que les tests manuels peuvent manquer. Cet article montre comment le moteur d'IA a découvert une vulnérabilité critique de Broken Function-Level Authorization (BFLA) sur un endpoint WebSocket GraphQL, qui permettait à n'importe quel utilisateur non authentifié d'exécuter une subscription et de recevoir des données sensibles.
Ostorlab AI Pentest sur une BFLA dans un WebSocket GraphQL
L'Engine avait reçu un objectif de haut niveau : valider l'autorisation des subscriptions WebSocket sur l'application cible, REDACTED. Ce qui a suivi est un processus méthodique, étape par étape, de découverte, de sondage et de confirmation.
Étape 1 : tester les connexions WebSocket GraphQL non authentifiées
La première action de l'IA a consisté à tester le comportement le plus fondamental : l'endpoint WebSocket accepte-t-il une connexion sans aucun identifiant ?
Baseline unauthenticated WS handshake to /subscriptions. Goal: verify whether the GraphQL WebSocket server accepts a connection without credentials.
Résumé de l'exécution :
L'Engine s'est connecté à wss://REDACTED/subscriptions en utilisant le sous-protocole graphql-transport-ws. Il a envoyé une trame connection_init standard avec un payload vide, sans fournir de jeton Authorization.
Résultat :
Le serveur a répondu par {"type":"connection_ack"}. En fait, il a envoyé deux trames ack, une anomalie relevée par l'IA.
Conclusion : Handshake non authentifié accepté. Le serveur n'exige pas d'authentification lors de la phase de connexion. C'est le premier signe d'une faiblesse potentielle.
Étape 2 : introspection du schéma des subscriptions GraphQL sans authentification
Une fois la connexion établie, l'étape logique suivante pour l'IA était de déterminer quelles opérations étaient disponibles. Elle a tenté d'exécuter une requête d'introspection GraphQL via la connexion WebSocket non authentifiée.
Attempt a single unauthenticated GraphQL introspection over WebSocket to enumerate subscription operations and validate whether unauthenticated operations are permitted.
Résumé de l'exécution :
L'Engine a envoyé une trame subscribe contenant une requête d'introspection centrée sur le type Subscription.
{"id":"1","type":"subscribe","payload":{"query":"query { __schema { subscriptionType { name fields { name } } } }","variables":{}}}
Résultat :
Le serveur a renvoyé une trame next contenant les données du schéma, suivie d'une trame complete.
{
"id": "1",
"type": "next",
"payload": {
"data": {
"__schema": {
"subscriptionType": {
"name": "Subscription",
"fields": [
{ "name": "generateSamplePromptResult" },
{ "name": "translateContent" },
{ "name": "upsertRewardSubscription" }
]
}
}
}
}
}
Conclusion : Le serveur autorise l'introspection non authentifiée via WebSocket, ce qui constitue une divulgation d'informations importante. Elle a révélé trois opérations de subscription, offrant à l'Engine des cibles claires pour tester l'autorisation.
Étape 3 : tester la subscription translateContent pour la BFLA
L'Engine a choisi translateContent pour son premier test direct. Après avoir introspecté son schéma d'entrée, il a forgé une requête de subscription syntaxiquement valide mais anodine.
Attempt a single unauthenticated subscription to translateContent with a minimal valid input to verify if unauthenticated subscriptions are processed.
Résumé de l'exécution :
L'Engine a envoyé une trame subscribe pour translateContent, avec un payload minimal et en ne demandant que __typename pour confirmer l'exécution.
{"id":"1","type":"subscribe","payload":{"query":"subscription($data: ContentTranslationSubscriptionInput!){ translateContent(data:$data){ __typename } }","variables":{"data":{"targetLanguage":"en","sourceContent":"hello"}}}}
Résultat :
Le serveur a accepté la subscription et renvoyé deux trames next, suivies de complete.
{"id":"1","type":"next","payload":{"data":{"translateContent":{"__typename":"TranslateContentResponse"}}}}
{"id":"1","type":"next","payload":{"data":{"translateContent":{"__typename":"TranslateContentResponse"}}}}
{"id":"1","type":"complete"}
Conclusion :
La subscription translateContent a été traitée avec succès sans aucune authentification. On passe ainsi de la divulgation d'informations à l'exécution active, non authentifiée, d'une fonction backend.
Étape 4 : vérifier l'exposition de données via la BFLA du WebSocket GraphQL
Recevoir un __typename prouve que l'opération s'est exécutée, mais renvoie-t-elle de vraies données ? La dernière étape de l'IA a consisté à demander des champs concrets pour confirmer l'exposition de données.
Confirm unauthenticated data return by requesting actual fields from translateContent.
Résumé de l'exécution :
L'Engine a répété la subscription, en demandant cette fois les champs content, isFinal et subscriptionId.
{"id":"1","type":"subscribe","payload":{"query":"subscription($data: ContentTranslationSubscription-Input!){ translateContent(data:$data){ content isFinal subscriptionId } }","variables":{"data":{"targetLanguage":"en","sourceContent":"hello"}}}}
Résultat :
Le serveur a renvoyé des trames next contenant le texte traduit.
{
"id": "1",
"type": "next",
"payload": {
"data": {
"translateContent": {
"content": "Hello",
"isFinal": false,
"subscriptionId": "yrpsrqkv7li34w23r25dx4b2"
}
}
}
}
{
"id": "1",
"type": "next",
"payload": {
"data": {
"translateContent": {
"content": "Hello",
"isFinal": true,
"subscriptionId": "yrpsrqkv7li34w23r25dx4b2"
}
}
}
}
Conclusion :
C'est la preuve définitive d'une vulnérabilité de Broken Function-Level Authorization (BFLA). Un attaquant non authentifié peut exécuter la fonction translateContent et recevoir les résultats.
Étape 5 : identifier une autorisation incohérente dans les subscriptions GraphQL
Pour comprendre l'étendue du problème, l'Engine a également testé une autre subscription découverte, generateSamplePromptResult.
Attempt a single unauthenticated subscription to generateSamplePromptResult to assess per-operation authorization.
Résumé de l'exécution :
L'Engine a envoyé une requête de subscription valide et non authentifiée pour generateSamplePromptResult.
Résultat :
Le serveur a immédiatement fermé la connexion avec le code 4500 et le motif : "You are not authorized to perform this action".
Conclusion : L'autorisation est correctement appliquée pour cette opération. Ce constat est crucial, car il révèle des contrôles de sécurité incohérents, un anti-pattern courant où les développeurs sécurisent certains endpoints mais en oublient d'autres.
Rapport final : les constats d'Ostorlab sur la BFLA d'un WebSocket GraphQL
1. Synthèse
L'Ostorlab AI Pentest Engine a découvert une vulnérabilité critique de Broken Function-Level Authorization (BFLA) sur l'endpoint WebSocket GraphQL wss://REDACTED/subscriptions. L'opération de subscription translateContent était exposée publiquement, ce qui permettait à n'importe quel utilisateur non authentifié de l'exécuter et de recevoir des résultats en flux. Il s'agit d'un contournement direct des privilèges, qui pourrait conduire à un abus de ressources et à une exposition potentielle de données. Le moteur a également relevé une autorisation incohérente, car d'autres subscriptions du même endpoint appliquaient correctement les contrôles d'accès.
2. Méthodologie
Le moteur d'IA a suivi une méthodologie systématique pour tester l'autorisation des WebSockets :
1. Découverte de l'endpoint : identification de l'endpoint WebSocket GraphQL et de son protocole (graphql-transport-ws).
2. Référence non authentifiée : confirmation que le serveur acceptait un handshake connection_init non authentifié.
3. Énumération du schéma : exploitation de l'introspection GraphQL non authentifiée pour découvrir les opérations de subscription disponibles.
4. Tests par opération : parcours des subscriptions découvertes (translateContent, generateSamplePromptResult) avec tentative de les exécuter sans authentification.
5. Confirmation par les preuves : confirmation de la fuite de données en demandant et en recevant des champs de données concrets, et pas seulement des messages de statut.
3. Constats
- BFLA dans la subscription
translateContent: le constat principal est que la subscriptiontranslateContentne comporte aucun contrôle d'autorisation. Un attaquant peut se connecter, s'abonner et recevoir des résultats de traduction sans authentification. Cela a été confirmé en envoyant un payload valide et en recevant en réponse un texte traduit. La vulnérabilité a été confirmée plus avant en montrant que même un jeton explicitement invalide était ignoré. - Divulgation d'informations via l'introspection non authentifiée : le schéma GraphQL des subscriptions est accessible publiquement via WebSocket, ce qui permet à un attaquant de découvrir facilement les opérations disponibles et leurs entrées requises.
- Contrôles d'autorisation incohérents : alors que
translateContentétait vulnérable, la subscriptiongenerateSamplePromptResultdu même endpoint rejetait correctement les requêtes non authentifiées. Cette incohérence suggère une annotation ou un contrôle de sécurité manquant sur le resolver vulnérable.
4. Remédiation
Pour corriger ces constats, nous recommandons ce qui suit : 1. Imposer l'authentification par défaut : mettez en œuvre une politique de refus par défaut. Tous les resolvers GraphQL, en particulier ceux des subscriptions, doivent exiger une session valide et authentifiée, sauf s'ils sont explicitement marqués comme publics. 2. Centraliser la logique d'autorisation : utilisez des middlewares ou des hooks au niveau du framework pour appliquer de façon cohérente les contrôles d'authentification et d'autorisation à toutes les opérations WebSocket, à la fois lors de la connexion et à chaque requête de subscription. 3. Désactiver l'introspection en production : utile en développement, l'introspection GraphQL doit être désactivée sur les endpoints accessibles publiquement afin d'empêcher les attaquants de cartographier facilement la surface de l'API.
5. Conclusion
Sécuriser les API modernes en temps réel exige une approche de défense en profondeur, dans laquelle l'autorisation est appliquée de manière cohérente. Cette étude de cas montre comment l'AI Pentest Engine d'Ostorlab peut mettre au jour de façon systématique des failles subtiles mais critiques, comme la BFLA et les contrôles de sécurité incohérents, dans des implémentations WebSocket complexes. En reproduisant la progression logique d'un attaquant, de la découverte à l'exploitation, l'Engine fournit des preuves définitives et exploitables pour aider les développeurs à bâtir des applications plus sûres.