Wenn du eine KI bittest, „deine App mit Stripe, OpenAI oder Supabase zu verbinden", legt sie den Key oft direkt im Frontend-Code ab. Es funktioniert auf Anhieb, und genau deshalb schaut niemand mehr hin. Das Problem ist einfach und absolut: Alles, was im Browser ankommt, ist öffentlich. Es gibt keine Möglichkeit, einen Text zu verstecken, den der Browser lesen muss. Dieser Guide erklärt, warum das passiert, wie ein Angreifer es in einer Minute findet, welche Keys gefährlich sind und welche nicht, wie du selbst danach suchst und was zu tun ist, wenn einer bereits geleakt ist.
Warum der Key im Browser landet
Es gibt drei übliche Wege. Der erste ist das öffentliche Präfix von Umgebungsvariablen: In Next.js wird alles, was mit NEXT_PUBLIC_ beginnt, ins Client-JavaScript eingebacken, genau wie VITE_ in Vite, NUXT_PUBLIC_ in Nuxt oder REACT_APP_ in Create React App. Die KI sieht, dass „die Variable nicht ankommt", und hängt das Präfix an, damit sie ankommt. Der zweite ist der direkte Aufruf: Der Assistent schreibt fetch('https://api.openai.com/...') in eine UI-Komponente mit dem Key im Header, weil das das kürzeste Beispiel ist, das er kennt. Der dritte ist eine geteilte Konstanten-Datei, die sowohl vom Server als auch vom Client importiert wird und in der jemand den Key „vorübergehend" abgelegt hat.
In allen drei Fällen ist das Ergebnis dasselbe: Der Key reist in einer .js-Datei, die jeder herunterladen kann, und bleibt in CDN-Caches, im Browserverlauf deiner Nutzer und in den Archiven der Crawler, die das Internet nach genau so etwas durchsuchen.
Wie ein Angreifer es in 60 Sekunden sieht
- DevTools. Öffne deine Website, drücke F12, geh in den Tab Sources, drücke Strg+Umschalt+F, um in allen Dateien zu suchen, und tippe
sk_live,sk-ant,AKIA,service_roleoder-----BEGIN. Taucht es auf, hat er es schon. - Quelltext. Rechtsklick, Seitenquelltext anzeigen, und jede verlinkte
.js-Datei öffnen. Bundles sind minifiziert, aber nicht verschlüsselt; eine Textsuche reicht. - Source Maps. Wenn du die
.map-Dateien veröffentlichst, sieht er neben dem Code auch die ursprünglichen Variablennamen und Kommentare, inklusive der// TODO: auf den Server verschieben. - Automatische Scanner. Bots crawlen neue Domains, laden deren JavaScript herunter und testen jedes Key-Muster beim passenden Anbieter. Ein in einem Bundle geleakter AWS-Key wird in Minuten benutzt, nicht in Tagen.
Welche Keys gefährlich sind und was bei einem Leak passiert
- Stripe
sk_live_. Erlaubt Zahlungen, Rückerstattungen und das Lesen von Kundendaten. Echtes Geld. - OpenAI, Anthropic und Ähnliche (
sk-…). Wer ihn hat, verbraucht auf deine Rechnung. Vierstellige Rechnungen wegen eines Keys in einem öffentlichen Repository sind eine Geschichte, die sich wiederholt. - AWS
AKIA…und Google Cloud. Je nach Berechtigungen: Buckets, Datenbanken und das Hochfahren von Maschinen zum Minen auf deine Kosten. - Supabase
service_role. Umgeht Row Level Security vollständig: voller Lese- und Schreibzugriff auf deine Datenbank. Der schwerste Fall von allen in Vibe-Coding-Apps. - Firebase-Dienstkonten (JSON). Entspricht dem
service_role: volle Kontrolle über das Projekt. - E-Mail- und SMS-Anbieter (Resend, SendGrid, Twilio). Mails und Nachrichten in deinem Namen verschicken, also Phishing mit deiner Domain.
- Datenbank-Verbindungsstrings mit Nutzer und Passwort sowie JWT-Secrets: Mit dem Secret lassen sich Sessions für jeden beliebigen Nutzer signieren.
- Zugriffstokens im
localStorage. Nicht deine Keys, sondern die deiner Nutzer, und jedes XSS liest und stiehlt sie.
Welche Keys in den Client dürfen
Nicht jeder Key ist ein Secret. Manche sind dafür gemacht, im Browser zu leben, solange das, was dahinter steht, richtig konfiguriert ist:
- Der
anon- oderpublishable-Key von Supabase, geschützt durch die RLS-Policies jeder Tabelle. - Der öffentliche Stripe-Key (
pk_live_), der Zahlungen nur startet, nie einzieht. - Die Firebase-Konfiguration (apiKey, projectId…), geschützt durch die Sicherheitsregeln und, besser, durch App Check.
- Ein Google-Maps-Key, der in der Google-Konsole auf deine Domain beschränkt ist.
- Analytics-IDs und IDs von Drittanbieter-Widgets.
Die Regel zur Unterscheidung: Ein Key darf in den Client, wenn der Anbieter ihn ausdrücklich öffentlich nennt und wenn ein Fremder selbst mit dem Key nichts tun kann, was du nicht willst. Im Zweifel ist er geheim.
Wie du selbst danach suchst
Im fertig gebauten Bundle suchst du nach bekannten Mustern. Mit Next.js oder Vite, nach dem Build:
grep -rEo "sk_live_[0-9a-zA-Z]+|sk-ant-[0-9a-zA-Z-]+|AKIA[0-9A-Z]{16}|service_role|eyJhbGciOi[0-9a-zA-Z._-]+" .next/static dist 2>/dev/null | head
Im Repository, bevor es das Bundle erreicht, gehen Tools wie gitleaks oder trufflehog die gesamte Historie durch, nicht nur den letzten Commit. Und im Dashboard deines Hosters prüfst du, welche Variablen ein öffentliches Präfix tragen: Jede davon wird an den Browser geschickt.
Drei Stellen, an denen niemand hinschaut: Komponenten mit 'use client', die eine geteilte Konfigurationsdatei importieren; URL-Parameter, in denen manchmal ein Token reist, das in Logs und Verlauf landet; und öffentliche GitHub-Repositories, in denen der Key seit Monaten liegen kann, auch wenn er im aktuellen Code nicht mehr steht.
So behebst du es: Der Key bleibt auf dem Server
Das richtige Muster: Der Browser spricht mit deinem Server und dein Server spricht mit dem Anbieter. Ein eigener Endpoint nimmt die Anfrage entgegen, prüft die Session des Nutzers, wendet ein Nutzungslimit an und macht den Aufruf mit dem Key aus einer Umgebungsvariable ohne öffentliches Präfix. In Next.js:
// app/api/summarize/route.ts (nur Server)
import { getSession } from '@/lib/auth'
export async function POST(req: Request) {
const session = await getSession(req)
if (!session) return new Response('Unauthorized', { status: 401 })
const { text } = await req.json()
if (typeof text !== 'string' || text.length > 5000) {
return new Response('Bad request', { status: 400 })
}
const r = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: text }] }),
})
return new Response(r.body, { headers: { 'Content-Type': 'application/json' } })
}
Mit Supabase gilt dieselbe Idee: Operationen, die service_role brauchen, gehören in eine Edge Function oder dein Backend, nie in den Client. Mit Firebase in eine Cloud Function. Und in jedem Fall heißt die Umgebungsvariable OPENAI_API_KEY, nicht NEXT_PUBLIC_OPENAI_API_KEY.
Für Session-Tokens bevorzuge HttpOnly-Cookies gegenüber localStorage: Ein HttpOnly-Cookie ist aus JavaScript nicht lesbar, also kann ein XSS es nicht stehlen. Speichert deine Bibliothek die Session standardmäßig im localStorage, wie es der Supabase-Browser-Client tut, gleiche das mit einer strikten CSP aus oder nutze die Server-Variante mit Cookies.
Wenn er schon geleakt ist: rotieren, nicht löschen
Den Key aus dem Code zu entfernen bringt nichts: Er ist bereits in Caches, in der Git-Historie und wahrscheinlich in fremden Händen. Die richtige Reihenfolge:
- Einen neuen Key erstellen beim Anbieter und ihn auf dem Server deployen.
- Den alten widerrufen, sofort danach. Bei Stripe, OpenAI und AWS ist das ein Klick im Dashboard.
- Die Nutzung prüfen: Abrechnung, API-Logs und Zahlungen der letzten Tage. Bei Missbrauch ein Ticket beim Anbieter öffnen; manchmal erlassen sie die Kosten.
- Ausgabenlimits und Alarme setzen bei jedem Anbieter, der es erlaubt, damit es beim nächsten Mal weniger weh tut.
- Gab der Key Zugriff auf personenbezogene Nutzerdaten, prüfe, ob du nach DSGVO melden musst.
Die Git-Historie mit Tools wie BFG oder git filter-repo zu bereinigen ist gute Hygiene, ersetzt die Rotation aber nicht.
Wie Scanaris das prüft
Scanaris lädt das JavaScript herunter, das deine Website ausliefert, genau wie ein Angreifer, und sucht nach Key-Mustern der üblichen Anbieter, JWT-Tokens, Webhook-URLs und Verbindungsstrings. Es warnt auch, wenn deine Source Maps erreichbar sind und sensible Dateien in Reichweite liegen. Jeder Fund kommt mit einem Prompt für deine KI mit der exakten Korrektur: Aufruf auf den Server verschieben und Key rotieren. Mit aktiviertem Monitoring meldet es sich, wenn eine neue Vibe-Coding-Session wieder einen Key in den Client legt. Es ist Teil der Launch-Checkliste und des Sicherheitsguides für KI-gebaute Apps.
Häufig gestellte Fragen
Ist der Supabase-anon-Key ein Secret?
Nein. Er ist für den Browser gedacht und wird vom RLS jeder Tabelle geschützt. Geheim ist service_role. Ist dein RLS falsch, liegt das Problem nicht am anon-Key, sondern an den Policies.
Und wenn der Key auf eine Domain beschränkt ist?
Referer-Beschränkungen (wie bei Google Maps) reduzieren Missbrauch von anderen Websites aus, aber nicht durch Skripte, die den Header fälschen. Sie taugen für Keys mit geringem Risiko, nicht für Keys, die Geld ausgeben oder Daten lesen.
Reicht es, ihn aus dem Repository zu löschen?
Nein. Ein Key, der in einem öffentlichen Bundle oder Commit war, ist kompromittiert. Rotiere ihn immer, und bereinige danach die Historie, wenn du magst.