Das OpenAI-Abrechnungs-Dashboard zeigte Kosten an, die der Gründer sich nicht erklären konnte. Die Anwendung hatte etwa 800 Nutzer mit einem Freemium-Modell, aber die API-Nutzung bewegte sich auf 2.000 US-Dollar pro Monat zu – weit höher, als es die Nutzeraktivität auf der Plattform rechtfertigte. Der Gründer vermutete, dass irgendwo ein ineffizienter Prompt verwendet wurde, und notierte sich, dies zu untersuchen.
Sieben Minuten nach Beginn eines Penetrify-Scans wurde der Grund klar: Der OpenAI API-Schlüssel wurde in den HTTP-Antwort-Headern jedes proxiierten API-Aufrufs an die Nutzer zurückgegeben. 800 Nutzer hatten ihn gesehen. Einige von ihnen nutzten ihn.
Die Architektur: Woher das Leck stammte
Die Anwendung bestand aus einem FastAPI-Backend, das ein React-Frontend bediente. Ihre Kernfunktionalität bestand darin, Benutzeranfragen an die OpenAI API weiterzuleiten, benutzerdefinierte System-Prompts hinzuzufügen, den Konversationsverlauf zu speichern und die proprietäre Prompt-Engineering-Schicht des Gründers anzuwenden. Dies ist ein gängiges Muster für AI-Wrapper-Produkte – der Wert liegt nicht im Modell, sondern im Produkt, das darum herum aufgebaut ist.
So funktionierte die Anwendung:
- Der Nutzer sendet einen Prompt vom React-Frontend
- Das Frontend sendet ihn an
POST /api/generate - Der FastAPI-Handler fügt den System-Prompt hinzu und ruft die OpenAI API auf
- FastAPI gibt die Vervollständigung an das Frontend zurück
Irgendwo in der FastAPI-Routenimplementierung wurde der Authorization-Header der ausgehenden OpenAI-Anfrage – der den API-Schlüssel im Bearer-Token-Format enthielt – in der Antwort zurückgeleitet. Dies ist eine spezifische Art von Header-Weiterleitungsfehler: Die Anwendung leitete Antwort-Header vom vorgelagerten OpenAI API-Aufruf durch, anstatt eigene Antwort-Header zu konstruieren.
Die Antwort-Header bei jedem /api/generate-Aufruf enthielten:
HTTP/1.1 200 OK
Content-Type: application/json
Authorization: Bearer sk-proj-...[OpenAI API key]
...
{"completion": "..."}
Jeder Nutzer, der die Generierungsfunktion jemals genutzt hatte – alle 800 von ihnen – hatte den API-Schlüssel in den Antwort-Headern seiner Anfragen erhalten. Er war im DevTools-Netzwerk-Tab des Browsers, in jedem HTTP-Proxy und in jedem programmatischen Client, der Antwort-Header auslas, sichtbar.
Was ein OpenAI API-Schlüssel Ihnen ermöglicht
Ein OpenAI API-Schlüssel ohne Nutzungsbeschränkungen gewährt dem Inhaber vollen Zugriff auf das API-Kontingent des entsprechenden Kontos. Das bedeutet:
- Unbegrenzter Modellzugriff auf Kosten des Schlüsselbesitzers – GPT-4o, o1, o3, Bilderzeugung, Embeddings, Fine-Tuning
- Keine Obergrenze pro Anfrage, bis das monatliche Ausgabenlimit des Kontos erreicht ist
- Zugriff auf alle Fine-Tuned-Modelle, die das Konto erstellt hat
- Möglichkeit, gespeicherte Dateien zu lesen, wenn das Konto die Files API nutzt
Für einen einzelnen Gründer, dessen Anwendung 200–400 US-Dollar pro Monat an legitimer Nutzung verarbeitet, kann der externe Missbrauch ihres Schlüssels die monatliche Rechnung auf 2.000, 5.000 US-Dollar oder mehr treiben – je nachdem, wie weit der Schlüssel verbreitet ist und was die Missbraucher generieren.
Das Kostenmodell für den Missbrauch der OpenAI API ist asymmetrisch: Der Angreifer zahlt nichts, der Schlüsselbesitzer zahlt für alles.
Die unerklärlichen Abrechnungsspitzen, erklärt
Sobald die Schlüssel-Exposition identifiziert war, ergaben die Abrechnungsspitzen einen Sinn. Der Gründer rief das OpenAI-Nutzungs-Dashboard auf, gefiltert nach Endpunkt und Zeit. Das Spitzenmuster zeigte Anfragen mit hohem Volumen, die nicht mit der Nutzeraktivität auf der Plattform korrelierten – Anfragen um 3 Uhr morgens, Anfragen von IP-Bereichen, die keiner bekannten Nutzergeografie entsprachen, Anfragen für Modelltypen, die die Anwendung nicht nutzte.
Jemand hatte den Schlüssel extrahiert — möglicherweise mehrere Personen — und nutzte ihn direkt gegen die OpenAI API, wodurch die Anwendung vollständig umgangen wurde. Die Anfragen gingen direkt an OpenAI unter Verwendung der extrahierten Zugangsdaten, nicht über die Anwendung des Gründers.
Der Schlüssel war seit etwa der ersten Woche des öffentlichen Starts der Anwendung exponiert. Zum Zeitpunkt des Scans war er bereits seit mehreren Monaten aktiv und leckte.
Die weiteren Ergebnisse
Die Exposition des OpenAI-Schlüssels war das unmittelbar schädlichste Ergebnis, aber es wurden drei weitere Probleme gemeldet:
MITTEL — IDOR auf /api/history/:userId
Die Anwendung speicherte den Konversationsverlauf pro Benutzer und machte ihn an einem vorhersagbaren Endpunkt zugänglich:
GET /api/history/abc123
Der Routen-Handler rief den Konversationsverlauf für die Benutzer-ID im Pfadparameter ab, ohne zu überprüfen, ob der anfragende Benutzer diese Datensätze besaß. Jeder authentifizierte Benutzer konnte den Konversationsverlauf jedes anderen Benutzers lesen, indem er dessen ID ersetzte. Da die Konversationen vom Benutzer bereitgestellte Prompts enthielten, stellte dies auch eine Datenschutzgefährdung dar: Ein Angreifer konnte lesen, welche Fragen andere Benutzer dem KI-Tool gestellt hatten.
MITTEL — FastAPI-Debug-Modus in Produktion aktiviert
Die Anwendung lief mit FastAPI(debug=True). Im Debug-Modus gibt jede unbehandelte Ausnahme einen vollständigen Stack-Trace in der HTTP-Antwort zurück, einschließlich interner Dateipfade, Abhängigkeitsversionen und Namen von Umgebungsvariablen (jedoch nicht deren Werte). Diese Informationen sind direkt nützlich für die Planung weiterer Angriffe — das Wissen um die genaue FastAPI-Version, Pydantic-Version und Python-Version reduziert die Liste der anwendbaren CVEs erheblich.
Der Debug-Modus aktiviert standardmäßig auch die interaktive Dokumentation von FastAPI unter /docs und /redoc, die in der Produktion zugänglich war und jeden internen API-Endpunkt dokumentierte, einschließlich derer, die nicht für den Benutzerzugriff vorgesehen waren.
NIEDRIG — HTTP leitet nicht auf HTTPS um
Die HTTP-Version der Anwendung lieferte vollständige Inhalte, ohne auf HTTPS umzuleiten. In öffentlichen oder gemeinsam genutzten Netzwerken könnte ein Angreifer, der einen Man-in-the-Middle-Angriff durchführt, unverschlüsselte Sitzungen abfangen und Sitzungstoken, vom Benutzer übermittelte Prompts und API-Antworten extrahieren.
Die Behebung: Noch am selben Abend implementiert
Der Gründer implementierte Korrekturen für alle Ergebnisse innerhalb von drei Stunden nach Erhalt des Berichts.
Schlüssel zuerst rotieren
Bevor Code angefasst wurde, bestand die sofortige Maßnahme darin, den kompromittierten Schlüssel im OpenAI-Dashboard zu widerrufen und einen neuen zu generieren. Dies unterband sofort jeglichen fortgesetzten Missbrauch. Die Schlüsselrotation von OpenAI erfolgt sofort — der alte Schlüssel funktioniert in dem Moment nicht mehr, in dem er gelöscht wird.
Den Header-Weiterleitungsfehler beheben
Die Grundursache war, dass die FastAPI-Route einen generischen HTTP-Client verwendete, der alle Antwort-Header vom Upstream-OpenAI-Aufruf weiterleitete. Die Lösung bestand darin, explizite Antwort-Header zu konstruieren, anstatt Upstream-Header durchzureichen:
# Vorher (anfällig) — Weiterleitung aller Upstream-Header
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) # ← dies leitet den Authorization-Header zurück
)
# Nachher (behoben) — explizite Antwortkonstruktion
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"]})
# Nur die Daten, die wir explizit zurückgeben möchten — keine Upstream-Header weitergeleitet
Den IDOR beheben
Der Endpunkt für den Gesprächsverlauf wurde aktualisiert, um die Benutzer-ID aus dem verifizierten JWT und nicht aus dem Pfadparameter zu extrahieren:
@router.get("/api/history")
async def get_history(current_user: User = Depends(get_current_user)):
# Die Benutzer-ID stammt aus dem verifizierten JWT – kann nicht gefälscht werden
history = await db.get_history(user_id=current_user.id)
return history
Debug-Modus deaktivieren
# In config.py
app = FastAPI(
debug=settings.DEBUG, # liest aus Umgebungsvariable
docs_url=None if not settings.DEBUG else "/docs", # Dokumentation in der Produktion ausblenden
redoc_url=None if not settings.DEBUG else "/redoc"
)
Mit DEBUG=false in der Produktionsumgebung verschwanden die interaktive Dokumentation und die ausführlichen Fehlermeldungen sofort bei der nächsten Bereitstellung.
OpenAI-Nutzungslimits als Sicherheitsnetz hinzufügen
Über die Behebung des Lecks hinaus fügte der Gründer zwei Abwehrmaßnahmen hinzu, um den Schaden bei zukünftiger Schlüsselpreisgabe zu begrenzen:
Nutzungslimits: Im OpenAI-Dashboard unter Abrechnung → Nutzungslimits legen Sie ein monatliches Hard Limit und einen Soft Notification Threshold fest. Selbst wenn ein Schlüssel erneut kompromittiert wird, ist die Möglichkeit des Angreifers, Kosten zu verursachen, begrenzt.
Dedizierte Schlüssel pro Dienst: Erstellen Sie einen separaten API key für jede Anwendung oder Umgebung. Wird ein Schlüssel kompromittiert, können Sie nur diesen Schlüssel rotieren, ohne andere Dienste zu stören, und die Nutzungslogs für jeden Schlüssel sind sauber getrennt – was unbefugten Zugriff wesentlich einfacher erkennbar macht.
Wie häufig ist das?
Die Preisgabe von API keys in HTTP-Antworten ist seltener als die Preisgabe in JavaScript-Bundles, aber wir sehen sie regelmäßig speziell in AI-Wrapper-Anwendungen. Das Muster hat fast immer dieselbe Grundursache: Ein Entwickler, der eine Proxy-Schicht aufbaut, verwendet einen generischen HTTP-Client, der Antwort-Header weiterleitet, und prüft nicht, was diese Header enthalten.
Der Fehler beim Weiterleiten von Headern ist leicht zu machen, da er oft die Implementierung vereinfacht. Warum eine neue Antwort konstruieren, wenn man die Upstream-Antwort weiterleiten kann? Die Antwort ist in diesem Fall, dass die Upstream-Antwort Anmeldeinformationen enthält, die Sie nicht mit Ihren Benutzern teilen möchten.
Wenn Ihre Anwendung Aufrufe an OpenAI, Anthropic oder eine andere externe API proxyt, prüfen Sie Ihre Antwort-Header explizit. Verwenden Sie ein Tool wie curl -v oder die DevTools Ihres Browsers, um jeden Header zu überprüfen, der von jedem API-Endpunkt zurückgegeben wird. Header sind leicht zu übersehen, gerade weil sie meistens uninteressant sind – was sie zu einem so effektiven Versteck für ein Leck macht.
Der YC-Bewerbungskontext
Der Gründer bereitete zum Zeitpunkt des Scans eine YC-Bewerbung vor. Die Kombination aus unerklärlichen Abrechnungsspitzen, einem offengelegten API key und einer IDOR-Schwachstelle, die den Gesprächsverlauf aller Benutzer betrifft, wäre ein erhebliches Problem gewesen, Investoren zu erklären – oder, schlimmer noch, nach der Finanzierung zu entdecken.
Sicherheitsprobleme in der Pre-Launch- oder frühen Traktionsphase sind innerhalb von Stunden behebbar. Dieselben Probleme, die nach einem Sicherheitsvorfall, einer Benachrichtigung über eine Datenschutzverletzung oder einer feindseligen Medienberichterstattung entdeckt werden, brauchen Monate zur Behebung und können ein Unternehmen beenden, das noch nicht das Vertrauen aufgebaut hat, um den Nachrichtenzyklus zu überleben.
Der Gründer führte Penetrify erneut aus, bevor er die YC-Bewerbung einreichte. Der Bericht war sauber.
