Wie diese Prüfung funktioniert
Scanaris lädt deine Startseite, sammelt die JavaScript-Dateien, die sie von deiner eigenen Domain einbindet, und durchsucht sie nach Mustern bekannter Zugangsdaten: geheime und eingeschränkte Stripe-Schlüssel (sk_live_, rk_live_), AWS-Zugangsschlüssel-IDs (AKIA…), Google-API-Schlüssel (AIza…), OpenAI-Schlüssel, GitHub-Tokens (ghp_…) und private Schlüssel im PEM-Format. Außerdem dekodiert es jedes gefundene JWT, und wenn dessen Payload role: service_role enthält, meldet es einen offengelegten Supabase-Service-Schlüssel, was kritisch ist. Öffentliche Schlüssel (Stripes pk_live_, Supabases anon oder publishable, die Firebase-Konfiguration) werden absichtlich ausgenommen: Sie sind dafür gemacht, im Browser zu liegen.
Was ein Angreifer damit tut
Genau das, was der Scanner tut, nur massenhaft: Bots laden das JavaScript neuer Domains herunter, sobald sie in Zertifikatsprotokollen auftauchen, und probieren jeden gefundenen Schlüssel aus. Ein geheimer Stripe-Schlüssel erlaubt Rückerstattungen oder das Lesen von Kunden; ein OpenAI-Schlüssel, dein Kontingent zu verbrauchen; ein Supabase-service_role, deine gesamte Datenbank unter Umgehung der Policies zu lesen und zu löschen. Nichts muss „gehackt" werden: Die Datei ist per Definition öffentlich, jeder Besucher bekommt sie.
Wie ein Schlüssel ins Bundle gerät
Fast immer über eine Umgebungsvariable mit dem öffentlichen Präfix: NEXT_PUBLIC_ in Next.js, VITE_ in Vite, NUXT_PUBLIC_ in Nuxt. Das Framework bettet diese Variablen beim Build ins JavaScript ein, was für einen öffentlichen Schlüssel genau richtig und für einen geheimen genau falsch ist. Der andere übliche Weg: den Schlüssel in den Chat von Lovable, Bolt oder Cursor einfügen, um einen Dienst „anzubinden"; das Modell schreibt ihn in die Komponente, die ihn braucht. Mehr dazu in Geheimnisse, die deine App im Browser preisgibt.
Wie du es behebst
- Rotiere zuerst den Schlüssel. Betrachte ihn als kompromittiert, auch wenn du ihn aus dem Code entfernst: Frühere Bundles liegen noch in Caches und im Deployment-Verlauf. Erzeuge im Dashboard des Dienstes einen neuen und widerrufe den alten.
- Verlagere den Aufruf auf den Server. In Next.js in einen Route Handler oder eine Server Action; in Nuxt in eine
server/api-Route; mit Supabase in eine Edge Function; bei Netlify in eine Function. Der Browser ruft deinen Server auf, und der Server ruft mit dem Schlüssel den Dienst auf. - Entferne das öffentliche Präfix von der Variable und hinterlege sie im Dashboard deines Hostings als Servervariable. Braucht eine Variable das Präfix, ist sie öffentlich: Frag dich, ob sie das sein sollte.
- Prüf die Git-Historie. Wurde der Schlüssel je in ein Repository gepusht, steckt er in der Historie, auch wenn die aktuelle Datei sauber ist.
Prüf, ob es sauber ist
curl -s https://deinedomain.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -5
curl -s https://deinedomain.com/assets/index-XXXX.js | grep -oE "sk_live_[A-Za-z0-9]+|rk_live_[A-Za-z0-9]+|AKIA[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{35}|ghp_[A-Za-z0-9]{36}|BEGIN (RSA |EC )?PRIVATE KEY" | head
Der erste Befehl listet deine Skripte; der zweite sucht die Muster in einem davon. Kommt in keinem etwas heraus, ist das Bundle sauber. Wiederhole das nach jedem größeren Deployment, denn eine KI-Neugenerierung kann den Schlüssel zurückbringen.
Häufige Fragen
Sind der Supabase-anon-Schlüssel oder Stripes pk_live ein Problem?
Nein. Sie sind absichtlich öffentlich, und der Scanner meldet sie nicht. Deine Daten schützt beim anon-Schlüssel das RLS; Stripe schützt beim pk_live, dass er nur Zahlungstokens erzeugen, aber nichts abbuchen kann.
Er meldet einen Google-Schlüssel, den ich für Maps nutze. Ist das schlimm?
AIza-Schlüssel dürfen legitim öffentlich sein, wenn sie in der Google-Cloud-Konsole auf Domain und API eingeschränkt sind. Ohne Einschränkung kann jeder dein Kontingent verbrauchen. Schränke ihn ein, und das reale Risiko verschwindet, auch wenn das Muster weiter auftaucht.
Ich habe ihn schon aus dem Code entfernt, und er wird trotzdem gemeldet.
Das deployte Bundle ist vielleicht noch das alte, oder der Schlüssel steckt in einer anderen Datei (einem geteilten Chunk, einem Drittskript auf deiner Domain). Deploye erneut, scanne noch einmal und rotiere den Schlüssel in jedem Fall.