Sicherheitsgrundlagen
Welche API-Schlüssel im Frontend sicher sind, und welche nicht
Dein Supabase-anon-Schlüssel darf öffentlich sein. Der service_role-Schlüssel nicht. Er ignoriert jede Regel, die du setzt. So erkennst du den Unterschied.
Kurz gesagt
- Einen API-Schlüssel im Code deiner App zu finden ist nicht automatisch ein Problem. Manche Schlüssel gehören genau dorthin.
- Öffentliche Schlüssel sind im Browser sicher. Geheime nicht. Einer davon im Frontend ist eine offene Kasse.
- Zwei Prüfungen trennen sie in unter einer Minute: Präfix lesen, und bei einem älteren Supabase-Schlüssel die Rolle dekodieren.
Wer seine App mit Lovable, Bolt, v0, Cursor oder Replit gebaut hat, erlebt früher oder später Folgendes: Jemand öffnet die Seite, drückt F12 und teilt mit, der API-Schlüssel sei „offengelegt". Über eine App, deren Code man selbst nicht lesen kann, ist das eine beunruhigende Nachricht.
Hier ist der Punkt, den Anleitung um Anleitung falsch darstellt: Manche dieser Schlüssel gehören genau dorthin. Jeden sichtbaren Schlüssel als Sicherheitsleck zu behandeln führt entweder zu Panik ohne Anlass oder, deutlich schlimmer, dazu, die Warnung irgendwann zu ignorieren. Auch an dem Tag, an dem sie zählt.
Ist es schlimm, dass mein API-Schlüssel sichtbar ist?
Meistens nicht. Es hängt vollständig davon ab, um welchen Schlüssel es geht.
Jeder Dienst, der mit einem Browser spricht, vergibt zwei verschiedene Arten von Zugangsdaten. Die eine ist eine Adresse. Die andere ist ein Schlüsselbund zum Gebäude. Beide heißen „API-Schlüssel", und das ist der größte Teil der Verwirrung.
Eine Adresse darf veröffentlicht werden. Sie sagt nur, zu welchem Projekt eine Anfrage gehört; die eigentliche Rechteprüfung passiert woanders. Ein Schlüsselbund darf das nicht, denn er ist die Rechteprüfung: Wer ihn hat, darf alles, was er erlaubt, von überall.
Deine App braucht die Adresse im Browser, um überhaupt zu funktionieren. Den Schlüsselbund sollte sie dort nie brauchen.
Warum deine App überhaupt Schlüssel an den Browser schickt
Weil die Anfrage vom Browser deiner Besucherin kommt, nicht von deinem Server.
Wenn deine App die Bestellungen deiner Nutzer lädt, geht diese Anfrage direkt vom Browser der Besucherin an deinen Datenbankanbieter. Sie muss sagen, zu welchem Projekt sie gehört, und diese Kennung muss in der Seite stehen, weil die Anfrage genau von dort gestellt wird.
Es gibt keine Variante, in der diese Kennung geheim bleibt. Sie erreicht jede Besucherin, so ist es gedacht. Und genau deshalb teilen Anbieter ihre Zugangsdaten in zwei Sorten: Sie wissen, dass eine davon öffentlich wird, also haben sie diese harmlos gemacht.
Die Sicherheit entsteht nicht dadurch, die Adresse zu verstecken. Sie entsteht durch die Regeln am anderen Ende: bei Supabase durch Row Level Security, die Zeile für Zeile entscheidet, wer was sehen darf. Diese Regeln sind das, was zu prüfen sich lohnt, und sie anzuschalten ist nicht dasselbe wie geschützt zu sein.
Die zwei Familien von Schlüsseln
| Anbieter | Schlüssel | Im Browser sicher? | Was er tut |
|---|---|---|---|
| Supabase | sb_publishable_…, in älteren Projekten anon | Gehört hierhin | Sagt, um welches Projekt es geht. Jede Anfrage wird weiterhin von deinen Row-Level-Security-Regeln gefiltert. |
| Supabase | sb_secret_…, in älteren Projekten service_role | Niemals | Umgeht Row Level Security vollständig. Liest und schreibt jede Zeile in jeder Tabelle, egal was deine Regeln sagen. |
| Stripe | pk_live_… (öffentlich) | Gehört hierhin | Erstellt Bezahlformulare. Kann kein Geld bewegen und keine Kundendaten lesen. |
| Stripe | sk_live_… (geheim) | Niemals | Vollzugriff auf das Konto: Abbuchungen, Erstattungen, Auszahlungen, Kundendaten. |
| OpenAI / Anthropic | jeder API-Schlüssel | Niemals | Es gibt keine öffentliche Variante. Jeder Schlüssel belastet direkt dein Konto. |
Das Muster gilt über diese drei hinaus. Bietet ein Anbieter nur eine Sorte Schlüssel an, geh davon aus, dass er geheim ist und auf einen Server gehört.
So erkennst du den Unterschied in 60 Sekunden
Bei Stripe und den meisten Anbietern reicht das Präfix. pk_ ist öffentlich
und sicher. sk_ ist geheim und nicht. Manche Anbieter schreiben _test_ oder
_live_ in die Mitte. Ein durchgesickerter Testschlüssel ist ein deutlich
kleineres Problem als ein Live-Schlüssel, aber tausche beide aus.
Bei Supabase hängt es davon ab, wie alt dein Projekt ist. Supabase hat zwei verschiedene Sorten Schlüssel ausgegeben, und beide sind heute in laufenden Apps unterwegs.
Neuere Projekte: Präfix lesen, genau wie bei Stripe. sb_publishable_… ist
der, der in deine App gehört. sb_secret_… ist der, den du heute austauschst. Da
ist nichts zu dekodieren. Wofür der Schlüssel da ist, steht vorne drauf.
Was sich geändert hat und was das für deine App bedeutet,
falls du diese Schlüssel noch nicht kennst.
Ältere Projekte: Du musst in den Schlüssel hineinsehen. Das ursprüngliche
Paar, anon und service_role, sind JWTs: drei durch Punkte getrennte Blöcke
Kauderwelsch, und der mittlere Block ist lesbare Information, keine
Verschlüsselung. Dort steht die Rolle im Klartext.
Ein Werkzeug braucht es in keinem Fall. Öffne im Supabase-Dashboard Settings → API Keys, dort steht, welcher Schlüssel welcher ist. Willst du lieber den Schlüssel prüfen, den du tatsächlich in deiner App gefunden hast: Der mittlere Abschnitt eines älteren Schlüssels dekodiert zu etwas wie diesem hier.
{
"iss": "supabase",
"ref": "abcdefghij…",
"role": "anon", ← darauf kommt es an
"iat": 1750000000
}
"role": "anon" ist der sichere. "role": "service_role" ist der, den du heute
austauschst. Bei einem älteren Schlüssel ist dieses eine Wort der ganze
Unterschied, und der Grund, warum ein Scanner, der bloß schlüsselförmige
Zeichenketten sucht, Rauschen statt einer Antwort liefert: Er kann dir nicht
sagen, welchen von zwei identisch aussehenden Schlüsseln du ausgeliefert hast.
Wenn du deine App nicht Schlüssel für Schlüssel durchgehen willst: Unser kostenloser Scan liest deine Live-Seite und sagt dir, welche davon von außen zu sehen sind. Das dauert etwa 20 Sekunden und braucht kein Konto: App scannen.
Was passiert, wenn ein geheimer Schlüssel durchsickert
Jede Zeile in deiner Datenbank wird für den lesbar und beschreibbar, der den
Schlüssel gefunden hat, einschließlich Tabellen, die du deiner App nie
zugänglich gemacht hast, und einschließlich der persönlichen Daten deiner Nutzer.
Row Level Security gilt für einen geheimen Supabase-Schlüssel nicht
(sb_secret_…, oder service_role in einem älteren Projekt). Das ist der Zweck
dieses Schlüssels.
Der Schaden ist auch nicht theoretisch. Meist fällt er als Supportnachricht über Daten auf, die sich von selbst geändert haben, oder als Tabelle, die plötzlich leer ist.
Ein durchgesickerter sk_live_-Schlüssel von Stripe bedeutet Erstattungen,
Abbuchungen und Kundendaten. Ein durchgesickerter Schlüssel eines
KI-Anbieters bedeutet eine Rechnung, manchmal eine sehr große, die eintrifft,
bevor es jemandem auffällt.
Nichts davon verlangt einen versierten Angreifer. Automatische Scanner durchsuchen öffentliche Seiten genau nach solchen Zeichenketten, und sie müssen nicht wissen, wer du bist, um deine zu finden.
Wenn du die plattformspezifische Fassung möchtest, was zu prüfen ist: Wir haben eine verständliche Anleitung für Supabase-Apps.
Was jetzt zu tun ist
Was zu tun ist
- Finde jeden Schlüssel in deiner App und bestimme, welcher es ist. Bei Stripe und bei neueren Supabase-Schlüsseln das Präfix; bei älteren Supabase-Schlüsseln das Feld
roleim Inneren. - Findest du einen geheimen Schlüssel im Frontend, tausche ihn zuerst aus. Ihn aus dem Code zu löschen schließt die Tür nicht: Der alte Wert steckt weiterhin in deiner Versionsgeschichte und in zwischengespeicherten Kopien deiner Seite.
- Verschiebe das, wofür dieser Schlüssel nötig war, auf einen Server: eine Edge Function, eine Serverless-Route, irgendetwas, das nicht der Browser ist.
- Schalte Row Level Security für jede Tabelle ein und prüfe dann, dass sie wirklich greift. Ein öffentlicher Schlüssel (
sb_publishable_…oderanon) ist nur wegen dieser Regeln sicher; ohne sie liest er deine ganze Datenbank. - Sieh nach jeder Offenlegung eines geheimen Schlüssels in Abrechnung und Protokolle. Der Austausch stoppt, was danach kommt, nicht das, was schon passiert ist.
Wenn du das lieber als Liste durchgehst: Die Sicherheits-Checkliste in 10 Minuten deckt diesen Punkt ab und die anderen Dinge, die man in einer frisch veröffentlichten App abschalten sollte.
Und falls ein Leck je eine Tabelle leert, hängt es ganz davon ab, was du gesichert hattest, ob du sie zurückbekommst.
FAQ
Jemand sagt, mein API-Schlüssel sei offengelegt. Sollte ich in Panik geraten?
Nicht bevor du weißt, um welchen Schlüssel es geht. Beginnt er mit pk_ oder ist es ein Supabase-anon-Schlüssel, ist er für die Öffentlichkeit gedacht und alles ist in Ordnung. Beginnt er mit sk_ oder ist es ein service_role-Schlüssel, tausche ihn sofort aus und sieh dann nach, worauf er Zugriff hatte.
Kann ich den Schlüssel nicht einfach verstecken?
Nein. Alles, was dein Browser benutzen kann, kann ein Besucher auch lesen. Minifizieren, Umbenennen oder Verschleiern hält jemanden nur ein paar Sekunden auf. Die Lösung ist nie, einen geheimen Schlüssel im Frontend zu verstecken, sondern ihn auf einen Server zu verschieben oder einen öffentlichen Schlüssel zu verwenden, der von vornherein sichtbar sein durfte.
Warum gibt mir Supabase einen Schlüssel, den jeder lesen kann?
Weil der anon-Schlüssel nicht das ist, was deine Daten schützt. Er sagt nur, mit welchem Projekt du sprichst. Der eigentliche Schutz ist Row Level Security, die Zeile für Zeile entscheidet, was jeder Besucher sehen darf. Deshalb ist ein anon-Schlüssel mit eingeschalteter RLS unproblematisch, und derselbe Schlüssel ohne RLS eine offene Datenbank.
Mein Supabase-Schlüssel beginnt mit sb_publishable_. Ist das derselbe wie der anon-Schlüssel?
Er hat dieselbe Aufgabe. Supabase hat seinen Schlüsseln neue Namen gegeben: sb_publishable_ hat den anon-Schlüssel abgelöst, sb_secret_ den service_role-Schlüssel. Ein Schlüssel, der mit sb_publishable_ beginnt, gehört also in deine App. Einer, der mit sb_secret_ beginnt, niemals. Ältere Projekte tragen weiterhin die ursprünglichen anon- und service_role-Schlüssel, und die funktionieren weiter; unter Umständen siehst du beide Paare nebeneinander im Dashboard.
Ich habe bereits einen geheimen Schlüssel in meine App eingefügt. Was jetzt?
Tausche ihn zuerst aus: Erzeuge im Dashboard des Anbieters einen neuen Schlüssel und widerrufe den alten. Nur das schließt die Tür wirklich. Ihn aus dem Code zu löschen reicht nicht, weil die alte Version noch in deiner Versionsgeschichte und in zwischengespeicherten Kopien steckt. Verschiebe danach die Arbeit, die ihn brauchte, auf einen Server, und prüfe Abrechnung und Protokolle auf Dinge, die nicht von dir stammen.