Zum Inhalt springen

Sicherheitsgrundlagen

Supabase Security Checker: die fünf Prüfungen selbst machen

Ein Supabase Security Checker liest deine veröffentlichte App statt deiner Projekteinstellungen. Hier sind die fünf Prüfungen und wie du jede selbst ausführst.

Vlad Tkachenko9 Min. Lesezeit
Ein Terminalfenster von vorn, mit fünf kurzen Befehlen unter einem grünen Prompt und einem Cursor, der in der sechsten Zeile wartet.

Kurz gesagt

  • Ein Supabase Security Checker liest deine Live-App so, wie ein Fremder sie liest: das ausgelieferte Bundle, die Antworten deiner Tabellen, deine Storage-Buckets, deine Header.
  • Der Security Advisor von Supabase liest die andere Hälfte, nämlich die Konfiguration deines Projekts. Keiner von beiden sieht, was der andere sieht.
  • Fünf Prüfungen decken die Außenansicht ab. Jede davon läuft im Terminal, braucht kein Konto und dauert zusammen etwa zehn Minuten.

Du hast nach einem Supabase Security Checker gesucht und neun davon bekommen. Einer will dein Repository. Einer will eine Browser-Erweiterung. Einer will Lesezugriff auf dein ganzes Supabase-Konto, was nah genug an der Sache ist, vor der du dich sorgst, um dich stutzig zu machen.

Hier steht, was keine dieser Tool-Seiten laut ausspricht. Es sind fünf Prüfungen, und du kannst jede einzelne selbst ausführen, ohne Konto, ohne Installation und ohne die Zugangsdaten, deren Weitergabe gefährlich wäre. Zehn Minuten im Terminal decken dasselbe ab wie die Tools.

Die fünf unten sind die, die unser eigener Scanner gegen ein Supabase-Projekt laufen lässt, als Befehle ausgeschrieben. Wenn du mit Lovable, Bolt, v0, Cursor, Replit, Windsurf oder Base44 gebaut hast und deine App überhaupt etwas speichert, steht dahinter sehr wahrscheinlich Supabase, und das sind die Fragen, die ein Fremder ihr stellen würde.

Was prüft ein Supabase Security Checker eigentlich?

Deine veröffentlichte App. Die Einstellungen in deinem Projekt sind Aufgabe eines anderen Werkzeugs, und Supabase liefert dieses Werkzeug schon mit.

Der Security Advisor in deinem Dashboard liest den eigenen Katalog deines Projekts: welche Tabellen Row Level Security abgeschaltet haben, welche Funktionen einen lockeren Search Path führen, welche Extensions im Public-Schema liegen. Er liest, was du konfiguriert hast.

Ein Checker liest, was deine App einem Fremden aushändigt. Das JavaScript-Bundle, das deine Besucher herunterladen, die Antwort einer Tabelle auf eine Anfrage ohne Login, den Inhalt eines Storage-Buckets, die Header auf deinen Seiten. Von deiner Konfiguration sieht er nichts und muss es auch nicht, weil er auf die Folge davon schaut.

Die beiden Hälften gehen häufiger auseinander, als die Namen vermuten lassen. Eine Tabelle kann den Advisor mit eingeschaltetem Schalter und geschriebener Policy passieren und trotzdem ihre Zeilen an jeden aushändigen, weil die Policy, die geschrieben wurde, alle hereinlässt. Das ist einen eigenen Artikel wert, falls es neu für dich ist: der Schalter ist nicht der Schutz.

Prüfung 1: welchen Supabase-Schlüssel liefert deine App aus?

Öffne deine App im Browser, drücke F12 und sieh dir eine einzige Anfrage an.

Geh auf den Tab Netzwerk, lade die Seite neu und tippe supabase in das Filterfeld. Deine App stellt mindestens eine Anfrage an eine Adresse, die auf .supabase.co endet. Klick sie an. Zwei Dinge, die du für den Rest dieses Artikels brauchst, stehen darin:

  • Die Anfrageadresse beginnt mit deiner Projekt-URL, etwa https://abcdefghij.supabase.co. Prüfung 2 und 3 zielen darauf.
  • Der Request-Header apikey trägt den Schlüssel, den deine App jedem Besucher aushändigt.

Kopier beides irgendwohin und lies dann den Schlüssel selbst. Einer, der mit sb_publishable_ beginnt, gehört in deine App und da ist nichts zu reparieren. Einer, der mit sb_secret_ beginnt, gehört nie dorthin, und ihn zu finden beendet die Liste vorzeitig: rotiere ihn heute, noch vor allem anderen auf dieser Seite. Ältere Projekte tragen ein Paar ohne lesbares Präfix, anon und service_role, und die beiden auseinanderzuhalten heißt, die Rolle im Schlüssel zu lesen.

Der letzte Fall ist selten. Über 30.998 Live-Apps, die wir im August 2026 gescannt haben, tauchte ein geheimer Supabase-Schlüssel im Browser drei Mal auf.

Solange der Netzwerk-Tab offen ist: schreib dir die Tabellennamen auf, die du in diesen Anfrageadressen sehen kannst. Die nächste Prüfung braucht sie.

Alles Weitere zielt auf eine von zwei Adressen: die deiner App und die deines Projekts. Prüfung 1 ist das, was dir die zweite verschafft.

Prüfung 2: antworten deine Tabellen einem Fremden?

Frag die Datenbank, wie viele Zeilen sie jemandem ohne Login geben würde, und lies die Zahl aus der Antwort.

curl -s -i \
  -H "apikey: DEIN_PUBLISHABLE_KEY" \
  -H "Prefer: count=exact" \
  -H "Range: 0-0" \
  "https://DEIN_PROJEKT.supabase.co/rest/v1/profiles?select=count" \
  | grep -i content-range

Setz den Schlüssel und die Projektadresse aus Prüfung 1 ein und schreib deinen eigenen Tabellennamen dorthin, wo profiles steht. curl ist auf macOS, auf Linux und auf aktuellem Windows schon installiert, es gibt also vorher nichts zu holen.

Diese Anfrage holt keine Zeile. select=count fragt nach einer Anzahl, Range: 0-0 fragt nach keiner der Zeilen selbst, und die ganze Antwort kommt in einem einzigen Header an. Es ist dieselbe Anfrage, die unser Scanner stellt, und deshalb kann er gegen eine fremde Live-App laufen, ohne deren Daten anzufassen.

Vier Dinge können zurückkommen, und eines davon ist ein Fund:

Was ausgegeben wirdWas es bedeutet
content-range: */0Ohne Login nichts lesbar. Diese Tabelle tut ihre Arbeit.
content-range: 0-0/128128 Zeilen erreichbar für jeden, der den Schlüssel aus deiner Seite hat.
401 oder 403Der Schlüssel wurde abgewiesen, bevor die Tabelle erreicht war. Unbeantwortet, nicht sauber.
404Keine Tabelle dieses Namens. Entweder geraten, oder sie heißt anders.
Beide Anfragen waren erfolgreich. Ein Header ist der ganze Unterschied zwischen einer Tabelle, die ihre Arbeit tut, und einer, die jedem Zeilen aushändigt.

Die Abweisung ist die Zeile, bei der Vorsicht gilt. Ein 401 sagt, dass die Anfrage stehen blieb, bevor sie die Tabelle je erreicht hat, was dir nichts darüber sagt, ob die Tabelle geschützt ist, und von den vieren lässt sie sich am leichtesten zu einem Haken aufrunden.

Führ den Befehl für jede Tabelle aus, die deine App in Prüfung 1 genannt hat, danach für die gewöhnlichen, die eine App meistens hat: users, profiles, customers, orders, messages, invoices. In diesen sechs liegen die Daten anderer Leute.

Das ist die Prüfung, die am meisten findet. Von den 3.680 Supabase-Apps, bei denen wir sie abschließen konnten, hatten 2.096 mindestens eine Tabelle, die einer Anfrage ohne Login antwortete, und bei 394 davon trug die offene Tabelle einen Personennamen. Die vollständige Messung und wovon die Zahl ein Anteil ist steht in einem eigenen Artikel.

Prüfung 3: listet ein Storage-Bucket seine eigenen Dateien auf?

Frag einen Bucket nach seinem Inhalt und sieh nach, ob etwas zurückkommt.

curl -s -X POST \
  -H "apikey: DEIN_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prefix":"","limit":1}' \
  "https://DEIN_PROJEKT.supabase.co/storage/v1/object/list/avatars"

Schick den Body mit. Ein POST an diese Adresse ohne Inhalt kommt für jeden Bucket in jedem Projekt mit "Body cannot be empty" zurück, ganz egal, wie er konfiguriert ist. Wir wissen das, weil unsere eigene Bucket-Prüfung so ausgeliefert wurde und monatelang munter meldete, dass nichts auflistbar sei.

Zwei Formen von Antwort:

  • [] heißt, der Bucket ist verschlossen, oder es gibt keinen Bucket dieses Namens. Von außen kannst du nicht sagen, was von beidem, eine leere Liste ist also kein Beleg für irgendetwas.
  • Ein Array mit einer Datei darin heißt, ein Fremder kann dein Projekt nach einem Verzeichnis dieses Buckets fragen und bekommt eines.

Probier die Bucket-Namen, die deine App verwendet hat, danach die gängigen: avatars, public, uploads, files, images, documents.

Auflistbar und öffentlich sind zwei verschiedene Einstellungen an zwei verschiedenen Stellen, und der Unterschied entscheidet, wie sehr ein öffentlicher Bucket zählt. Die Auflistung ist die, die man schließt, denn sie erspart einem Fremden die Arbeit, einen Dateinamen zu raten. Von den 27.269 Apps, bei denen wir fragen konnten, antworteten 792 mit einer Liste.

Prüfung 4: wird dein Quellcode neben deiner App veröffentlicht?

Lies die letzte Zeile deines JavaScript-Bundles.

Zurück im Netzwerk-Tab aus Prüfung 1: such die größte .js Datei, die deine App geladen hat, und kopier ihre Adresse. Dann:

curl -s "https://deine-app.example/assets/index-abc123.js" | tail -c 120

Eine letzte Zeile mit //# sourceMappingURL=index-abc123.js.map heißt, dein Build hat eine Source Map geschrieben und Browsern gesagt, wo sie liegt. Ob er sie auch veröffentlicht hat, klärt eine weitere Anfrage:

curl -s -o /dev/null -w "%{http_code}\n" \
  "https://deine-app.example/assets/index-abc123.js.map"

200 heißt, jeder kann sie herunterladen. Eine Source Map macht aus dem komprimierten JavaScript wieder deine Originaldateien, mit deiner Ordnerstruktur, deinen Kommentaren und deiner Logik. Sie legt deinen Code offen, und jeder Schlüssel, den sie zeigt, lag ohnehin schon daneben im Bundle, worum es in Prüfung 1 ging. 3.885 der 30.987 prüfbaren Apps veröffentlichen ihre, und bei manchen Buildern ist das eine Plattform-Voreinstellung und keine Entscheidung, die jemand getroffen hat: was eine veröffentlichte Map zeigt.

Prüfung 5: was dein Hoster mit jeder Seite mitschickt

Eine Anfrage an die Adresse deiner App, und lies, was mit ihr zurückkam.

curl -s -i "https://deine-app.example" | grep -i -E \
  "content-security-policy|strict-transport-security|x-frame-options|x-content-type-options|referrer-policy"

Fünf Header, und die, die nicht ausgegeben werden, sind die, die dir fehlen. Sie sagen einem Browser, er soll die Seite ablehnen, wenn sie in der Seite von jemand anderem geladen wird, beim nächsten Mal auf einer verschlüsselten Verbindung bestehen und aufhören, den Dateityp zu raten.

Fast jede App fällt hier durch und fast niemand kann etwas dagegen tun. 30.756 der 30.981 prüfbaren Apps fehlte mindestens einer, weil die Hosting-Plattform diese Header sendet und Eigentümer auf einer Builder-Subdomain nichts daran ändern können. Wir zählen es und deckeln es bei mittel: was fehlende Header aussagen und was nicht.

Was du gefunden hast, richtig lesen

Nach Schwere sortiert, und das ist nicht die Reihenfolge, in der du sie ausgeführt hast.

Ein geheimer Schlüssel im Bundle kommt zuerst. Er überspringt jede Regel, die du auf jeder Tabelle geschrieben hast, also rotiere ihn, bevor du dir hier irgendetwas anderes ansiehst.

Eine Tabelle voller Personen, die auf Prüfung 2 antwortet, kommt als Nächstes. In users, profiles, orders und messages liegen die Daten anderer Leute, und sie sind gerade jetzt erreichbar. Eine products Tabelle, die genauso antwortet, kann völlig richtig sein, und du bist die einzige Person, die die beiden unterscheiden kann.

Danach ein auflistbarer Bucket, danach Maps und Header, die meistens die Voreinstellungen deiner Plattform sind und nichts, was du gewählt hast.

Eine Sache aus allen fünf. Eine Prüfung ohne Antwort hat nicht bestanden. Ein 401, eine Anfrage, die in eine Zeitüberschreitung lief, eine Tabelle, deren Namen du falsch geraten hast: jedes davon ist eine offene Frage. Unser Scanner schreibt "Konnte nicht geprüft werden" in diese Zeilen und lässt die Note unberührt, und von Hand heißt das, diese Liste selbst zu führen.

Alles hier beschreibt außerdem die App, die in dem Moment live war, in dem du gefragt hast. Row Level Security wird abgeschaltet, damit eine Seite lädt, ein Bucket wird für einen Upload geöffnet, ein Schlüssel wird eingefügt, damit ein Feature heute Abend rausgeht.

Was diese Woche zu tun ist

Was zu tun ist

  • Führ Prüfung 2 gegen jede Tabelle aus, die deine App nennt, danach gegen users, profiles, customers, orders und messages. Dort liegen die Funde.
  • Wenn Prüfung 1 einen geheimen Schlüssel zutage gefördert hat, rotiere ihn zuerst. Ihn aus deinem Code zu löschen lässt den alten Wert weiterlaufen für alle, die ihn schon haben.
  • Schreib jede Probe auf, die eine Abweisung oder gar keine Antwort bekam. Die sind unbeantwortet, und eine unbeantwortete Frage ist kein sauberes Ergebnis.
  • Lass die Tabellen in Ruhe, die öffentlich sein sollen. Eine Produktliste, die einem Fremden antwortet, ist deine App, die richtig arbeitet.
  • Führ alle fünf nach einem Deploy erneut aus, und nachdem jemand eine Datenbankregel geändert hat. Keines von beidem meldet sich von selbst.
Dieselben fünf Prüfungen und vier weitere, zurückgemeldet. Der Punktwert ist 85 und die Note ein C, weil ein hoher Fund den Buchstaben deckelt, was die Rechnung auch sagt. Die gestrichelte Zeile ist die Datenbank, die nie geantwortet hat.

Unser eigener kostenloser Sicherheitsscanner führt diese fünf und vier weitere gegen jede Live-URL aus, in etwa zwanzig Sekunden, ohne Konto und ohne dass du Zugangsdaten herausgibst. Er liest, schreibt nie, und wo er keine Antwort bekommt, schreibt er das in die Zeile.

Alle fünf kannst du heute selbst machen. Was dir kein einzelner Durchgang sagen kann, ist, ob die Antworten nächsten Monat noch dieselben sind, und dafür haben wir Reeve Care gebaut: es führt diese Prüfungen nach Zeitplan erneut aus, mailt dir, wenn eine Antwort schlechter wird, und hält geprüfte Backups deiner Supabase-Datenbank vor, dazu die Dateien deiner Nutzer, sobald du einen Storage-Zugang verbindest. Was es beobachtet und was es kostet.

Wenn ein Terminal nicht der Ort ist, an dem du sein möchtest: die Anleitung in klarer Sprache für Supabase-Apps erklärt, wofür jede dieser Einstellungen da ist und wo du sie im Dashboard findest.

FAQ

Findet der Supabase Security Advisor alles?

In seiner eigenen Hälfte findet er alles. Der Advisor liest die Konfiguration deines Projekts und meldet Tabellen mit abgeschalteter Row Level Security, Funktionen mit lockerem Search Path und ähnliche Einstellungen, die du im Dashboard steuerst. Was er nicht sieht, ist deine veröffentlichte App: welcher Schlüssel im JavaScript deiner Besucher gelandet ist, ob eine geschriebene Policy eine anonyme Anfrage wirklich stoppt, oder welche Header dein Hoster sendet. Lass den Advisor und die fünf Prüfungen aus diesem Artikel laufen, denn sie schauen auf verschiedene Dinge.

Ist es gefahrlos, diese Prüfungen gegen mein eigenes Projekt laufen zu lassen?

Ja. Jeder Befehl hier liest, und keiner schreibt. Die Tabellenprüfung fragt deine Datenbank nach einer Zeilenanzahl und liest die Zahl aus einem Response-Header, es wird also nie eine Zeile geholt. Die Bucket-Prüfung fragt nach einer Auflistung und hört dort auf, ohne eine Datei herunterzuladen. Die letzten beiden sind eine gewöhnliche Seitenanfrage, wie deine Besucher sie den ganzen Tag stellen. Sie gegen ein fremdes Projekt laufen zu lassen ist eine andere Frage, und die Antwort lautet: vorher die Eigentümerin oder den Eigentümer fragen.

Mein anon key steht in meinem Bundle. Ist das ein Problem?

Nein. Der anon key in älteren Projekten und sb_publishable_ in neueren ist dafür gemacht, in dem Code zu stehen, den deine Besucher herunterladen. Er benennt dein Projekt und gewährt für sich genommen nichts; Row Level Security entscheidet, welche Zeilen eine Anfrage zurückbekommt. Der Schlüssel, der dort niemals stehen darf, ist der geheime: service_role in älteren Projekten und sb_secret_ in neueren, weil er jede Regel überspringt, die du geschrieben hast.

Was bedeutet content-range: */0?

Das ist deine Datenbank, die sagt, dass sie diesem Aufrufer null Zeilen aus dieser Tabelle gibt. Der Stern heißt, dass die Antwort überhaupt keine Zeilen enthält, und die Zahl nach dem Schrägstrich ist die Anzahl, die der Aufrufer sehen darf. */0 ist also die Antwort, die du von einer Tabelle mit Personendaten willst, und 0-0/128 heißt, dass 128 Zeilen für jeden erreichbar sind, der den Schlüssel aus deiner App hat.

Brauche ich meinen service_role key, um das zu testen?

Nein, und ein Checker, der danach fragt, fragt nach dem Falschen. Jede Prüfung hier nutzt den publishable key, der ohnehin in deiner App steht, denn genau den hätte ein Fremder. Ein Test mit einem geheimen Schlüssel sagt dir, was eine Administratorin erreichen kann, und das stand nie zur Debatte. Gib keinen service_role oder sb_secret_ Schlüssel in irgendeinen Scanner ein, unseren eingeschlossen: nichts, was wir tun, braucht ihn.

Geschrieben von

Vlad Tkachenko

Gründer, Reeve

Ich schaue mir den ganzen Tag Apps an, die mit Lovable, Bolt, v0, Cursor und Replit gebaut wurden, und die kurze Liste von Fehlern, die darin immer wieder auftauchen.

Mehr über den Autor

Weiterlesen

Alle Artikel

Unsicher, wie es um deine eigene App steht?

Starte einen kostenlosen Scan und erhalte in rund 20 Sekunden eine verständliche Note von A bis F. Ohne Konto, ohne Karte.

App kostenlos scannen

Automatisierte externe Prüfung, kein vollständiges Audit. Das Fehlen von Funden ist keine Garantie für Sicherheit.