Le tableau de bord de facturation d'OpenAI affichait des frais que le fondateur ne pouvait pas expliquer. L'application comptait environ 800 utilisateurs sur un modèle freemium, mais l'utilisation de l'API tendait vers 2 000 $ par mois — bien plus élevée que ce que l'activité des utilisateurs sur la plateforme justifiait. Le fondateur a supposé qu'il y avait un prompt inefficace quelque part et a pris note d'enquêter.
Sept minutes après le début d'un scan Penetrify, la raison est devenue claire : la clé API OpenAI était renvoyée aux utilisateurs dans les en-têtes de réponse HTTP de chaque appel API proxifié. 800 utilisateurs l'avaient vue. Certains d'entre eux l'utilisaient.
L'Architecture : D'où venait la fuite
L'application était un backend FastAPI servant un frontend React. Sa fonctionnalité principale était de proxifier les requêtes des utilisateurs vers l'API d'OpenAI, en ajoutant des prompts système personnalisés, en stockant l'historique des conversations et en appliquant la couche d'ingénierie de prompt propriétaire du fondateur. C'est un modèle courant pour les produits d'enveloppement d'IA — la valeur n'est pas le modèle, c'est le produit construit autour de lui.
Le fonctionnement de l'application :
- L'utilisateur envoie un prompt depuis le frontend React
- Le frontend l'envoie à
POST /api/generate - Le gestionnaire FastAPI ajoute le prompt système et appelle l'API d'OpenAI
- FastAPI renvoie la complétion au frontend
Quelque part dans l'implémentation de la route FastAPI, l'en-tête Authorization de la requête OpenAI sortante — contenant la clé API au format de jeton Bearer — était renvoyé dans la réponse. Il s'agit d'une classe spécifique de bug de renvoi d'en-tête : l'application transmettait les en-têtes de réponse de l'appel API OpenAI en amont plutôt que de construire ses propres en-têtes de réponse.
Les en-têtes de réponse de chaque appel à /api/generate incluaient :
HTTP/1.1 200 OK
Content-Type: application/json
Authorization: Bearer sk-proj-...[OpenAI API key]
...
{"completion": "..."}
Chaque utilisateur ayant utilisé la fonctionnalité de génération — les 800 — avait reçu la clé API dans les en-têtes de réponse de ses requêtes. Elle était visible dans l'onglet Réseau des DevTools du navigateur, dans tout proxy HTTP, et dans tout client programmatique lisant les en-têtes de réponse.
Ce qu'une clé API OpenAI vous donne
Une clé API OpenAI sans restrictions d'utilisation donne au détenteur un accès complet au quota API du compte correspondant. Cela signifie :
- Accès illimité aux modèles aux frais du propriétaire de la clé — GPT-4o, o1, o3, génération d'images, embeddings, fine-tuning
- Aucune limite par requête tant que la limite de dépenses mensuelles du compte n'est pas atteinte
- Accès à tous les modèles fine-tunés que le compte a créés
- Possibilité de lire les fichiers stockés si le compte utilise l'API Files
Pour un fondateur individuel dont l'application traite 200 à 400 $ par mois d'utilisation légitime, voir sa clé abusée en externe peut faire grimper la facture mensuelle à 2 000 $, 5 000 $ ou plus — selon l'étendue de la circulation de la clé et ce que les abuseurs génèrent.
Le modèle de coût pour l'abus de l'API OpenAI est asymétrique : l'attaquant ne paie rien, le propriétaire de la clé paie tout.
Les pics de facturation inexpliqués, expliqués
Une fois l'exposition de la clé identifiée, les pics de facturation ont pris tout leur sens. Le fondateur a consulté le tableau de bord d'utilisation d'OpenAI filtré par endpoint et par heure. Le modèle de pics montrait des requêtes à volume élevé qui ne correspondaient pas à l'activité des utilisateurs sur la plateforme — des requêtes à 3h du matin, des requêtes provenant de plages d'adresses IP qui ne correspondaient à aucune géographie d'utilisateur connue, des requêtes pour des types de modèles que l'application n'utilisait pas.
Quelqu'un avait extrait la clé — peut-être plusieurs personnes — et l'utilisait directement contre l'API OpenAI, contournant entièrement l'application. Les requêtes allaient directement à OpenAI en utilisant les identifiants extraits, et non via l'application du fondateur.
La clé avait été exposée depuis environ la première semaine du lancement public de l'application. Au moment de l'analyse, elle était active et fuyait depuis plusieurs mois.
Les autres découvertes
L'exposition de la clé OpenAI était la découverte la plus immédiatement dommageable, mais trois problèmes supplémentaires ont été signalés :
MOYEN — IDOR sur /api/history/:userId
L'application stockait l'historique des conversations par utilisateur et l'exposait à un point d'accès prévisible :
GET /api/history/abc123
Le gestionnaire de route récupérait l'historique des conversations pour l'ID utilisateur dans le paramètre de chemin sans vérifier si l'utilisateur demandeur était propriétaire de ces enregistrements. Tout utilisateur authentifié pouvait lire l'historique des conversations de tout autre utilisateur en substituant son ID. Étant donné que les conversations incluaient des invites fournies par l'utilisateur, il s'agissait également d'une exposition de la vie privée : un attaquant pouvait lire les questions que d'autres utilisateurs avaient posées à l'outil d'IA.
MOYEN — Mode de débogage FastAPI activé en production
L'application fonctionnait avec FastAPI(debug=True). En mode débogage, toute exception non gérée renvoie une trace de pile complète dans la réponse HTTP, y compris les chemins de fichiers internes, les versions des dépendances et les noms des variables d'environnement (mais pas leurs valeurs). Ces informations sont directement utiles pour planifier de nouvelles attaques — connaître la version exacte de FastAPI, de Pydantic et de Python réduit considérablement la liste des CVEs applicables.
Le mode débogage active également par défaut la documentation interactive de FastAPI aux adresses /docs et /redoc, qui était accessible en production et documentait chaque point d'accès API interne, y compris ceux non destinés à l'accès utilisateur.
FAIBLE — HTTP ne redirigeant pas vers HTTPS
La version HTTP de l'application servait le contenu complet sans rediriger vers HTTPS. Sur les réseaux publics ou partagés, un attaquant effectuant une attaque de l'homme du milieu (man-in-the-middle) pourrait intercepter des sessions non chiffrées et extraire des jetons de session, des invites soumises par l'utilisateur et des réponses API.
La correction : Déployée le soir même
Le fondateur a déployé des correctifs pour toutes les découvertes dans les trois heures suivant la réception du rapport.
Faire pivoter la clé en premier
Avant de toucher à tout code, l'action immédiate a été de révoquer la clé compromise dans le tableau de bord OpenAI et d'en générer une nouvelle. Cela a instantanément mis fin à tout abus en cours. La rotation des clés d'OpenAI est immédiate — l'ancienne clé cesse de fonctionner dès que vous la supprimez.
Corriger le bug de transmission des en-têtes
La cause première était que la route FastAPI utilisait un client HTTP générique qui transmettait tous les en-têtes de réponse de l'appel OpenAI en amont. La correction consistait à construire des en-têtes de réponse explicites plutôt que de transmettre ceux en amont :
# Before (vulnerable) — forwarding all upstream headers
upstream_response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.OPENAI_API_KEY}", ...},
json=payload
)
return Response(
content=upstream_response.content,
headers=dict(upstream_response.headers) # ← this forwards the Authorization header back
)
# After (fixed) — explicit response construction
upstream_response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.OPENAI_API_KEY}", ...},
json=payload
)
completion_data = upstream_response.json()
return JSONResponse(content={"completion": completion_data["choices"][0]["message"]["content"]})
# Only the data we explicitly want to return — no upstream headers forwarded
Corriger l'IDOR
Le point de terminaison de l'historique des conversations a été mis à jour pour extraire l'ID utilisateur du JWT vérifié plutôt que du paramètre de chemin :
@router.get("/api/history")
async def get_history(current_user: User = Depends(get_current_user)):
# L'ID utilisateur provient du JWT vérifié — ne peut pas être usurpé
history = await db.get_history(user_id=current_user.id)
return history
Désactiver le mode débogage
# Dans config.py
app = FastAPI(
debug=settings.DEBUG, # lit la variable d'environnement
docs_url=None if not settings.DEBUG else "/docs", # masque la documentation en production
redoc_url=None if not settings.DEBUG else "/redoc"
)
Avec DEBUG=false défini dans l'environnement de production, la documentation interactive et les réponses d'erreur détaillées ont disparu immédiatement lors du déploiement suivant.
Ajout de limites d'utilisation OpenAI comme filet de sécurité
Au-delà de la correction de la fuite, le fondateur a ajouté deux mesures défensives pour limiter l'étendue des dommages en cas de future exposition de clé :
Limites d'utilisation : Dans le tableau de bord OpenAI, sous Facturation → Limites d'utilisation, définissez une limite mensuelle stricte et un seuil de notification souple. Même si une clé est à nouveau compromise, la capacité de l'attaquant à accumuler des frais est plafonnée.
Clés dédiées par service : Créez une clé API distincte pour chaque application ou environnement. Si une clé est compromise, vous pouvez révoquer uniquement cette clé sans perturber les autres services, et les journaux d'utilisation pour chaque clé sont clairement séparés — ce qui facilite grandement la détection des accès non autorisés.
Quelle est la fréquence de ce problème ?
L'exposition des clés API dans les réponses HTTP est moins courante que l'exposition dans les bundles JavaScript, mais nous la constatons régulièrement, en particulier dans les applications d'encapsulation d'IA. Le schéma a presque toujours la même cause profonde : un développeur construisant une couche proxy utilise un client HTTP générique qui transmet les en-têtes de réponse, et il n'audite pas ce que ces en-têtes contiennent.
L'erreur de transmission des en-têtes est facile à commettre car elle simplifie souvent l'implémentation. Pourquoi construire une nouvelle réponse quand on peut simplement transmettre celle du service en amont ? La réponse, dans ce cas, est que la réponse en amont contient des identifiants que vous ne voulez pas partager avec vos utilisateurs.
Si votre application proxyfie les appels vers OpenAI, Anthropic ou toute autre API externe, auditez explicitement vos en-têtes de réponse. Utilisez un outil comme curl -v ou les DevTools de votre navigateur pour examiner chaque en-tête renvoyé par chaque point de terminaison API. Les en-têtes sont faciles à négliger précisément parce que la plupart du temps, ils sont sans intérêt — ce qui en fait une cachette si efficace pour une fuite.
Le contexte de la candidature YC
Le fondateur préparait une candidature YC au moment de l'analyse. La combinaison de pics de facturation inexpliqués, d'une clé API exposée et d'une vulnérabilité IDOR affectant l'historique des conversations de tous les utilisateurs aurait été un problème majeur à expliquer aux investisseurs — ou, pire encore, à découvrir après le financement.
Les problèmes de sécurité au stade de pré-lancement ou de début de traction sont réparables en quelques heures. Les mêmes problèmes découverts après un incident de sécurité, une notification de violation de données ou un article de presse hostile prennent des mois à résoudre et peuvent mettre fin à une entreprise qui n'a pas encore acquis la bonne volonté nécessaire pour survivre au cycle médiatique.
Le fondateur a de nouveau exécuté Penetrify avant de soumettre la candidature YC. Le rapport est revenu vierge.
