Zum Inhalt springen

Sicherheitsgrundlagen

Vite-Env-Variablen offengelegt: der Präfix heißt veröffentlichen

Vite-Env-Variablen offengelegt: Deine App hat getan, was der Präfix verlangt. VITE_ und NEXT_PUBLIC_ heißen veröffentlichen, und die KI kannte den Preis nie.

Vlad Tkachenko10 Min. Lesezeit
Eine .env-Datei mit drei Werten, zwei mit dem Label VITE_, und dieselben zwei Werte noch einmal in einem Browserfenster daneben.

Kurz gesagt

  • Vite-Env-Variablen sind in deiner App offengelegt, weil der Präfix genau das verlangt hat. Eine Variable namens VITE_ oder NEXT_PUBLIC_ wird in das JavaScript kopiert, das jeder Besucher herunterlädt, und dass sie in einer .env-Datei liegt, ändert daran nichts.
  • Der Präfix ist das richtige Label für eine Adresse oder einen veröffentlichbaren Schlüssel und das falsche für alles, was Geld ausgibt oder deine Datenbankregeln ignoriert. Der Build kann die beiden nicht unterscheiden, und die KI, die die Zeile geschrieben hat, auch nicht.
  • Von 30.998 live gescannten vibe-gecodeten Apps lieferten 1.332 etwas Schlüsselförmiges aus. 1.142 davon waren Google-API-Schlüssel, die meist eine Einschränkung brauchen und keine Rotation. 52 lieferten einen Schlüssel aus, der Geld ausgibt oder alles liest.

Jemand hat deine Lovable-App geöffnet, F12 gedrückt und im Code einen Wert gefunden, von dem du sicher bist, dass du ihn in eine .env-Datei gelegt hast. Oder du hast den Builder um ein Feature gebeten, er hat eine Zeile geschrieben, die mit VITE_ beginnt, und irgendetwas, das du seitdem gelesen hast, sagt, dass genau dieser Präfix der Weg ist, auf dem Schlüssel durchsickern. Die Datei hat .env im Namen, und jedes Tutorial sagt, dass man sie nie weitergibt. Vite-Env-Variablen, die für jeden Besucher offengelegt sind, klingen nach einem Fehler in Vite.

Hier kommt der Teil, den eine Anleitung nach der anderen falsch erklärt: nichts hat versagt, diesen Wert zu verstecken. VITE_ und NEXT_PUBLIC_ sind eine Anweisung an dein Build-Tool, und die Anweisung lautet, den Wert in die App zu legen, die jeder Besucher herunterlädt. Das Tool hat das Label gelesen und getan, was darauf stand.

Ein Build packt deine App in eine Kiste, und jeder Besucher bekommt eine Kopie der Kiste. Der Präfix ist ein Label an einem Wert, das sagt: pack das auch ein. Der Rest dieses Beitrags handelt davon, für welche Werte dieses Label richtig ist, warum die KI es an die falschen hängt, und wie du siehst, was in deiner eigenen Kiste steckt.

Sind Vite-Env-Variablen für Besucher offengelegt?

Die, die mit VITE_ beginnen, ja, und genau dafür ist der Präfix da.

Vite, das Build-Tool hinter den meisten Lovable- und Bolt-Apps, liest deine .env-Datei bei jedem Build. Eine Variable namens VITE_SUPABASE_URL wird in das Bundle kopiert, die komprimierte JavaScript-Datei, die jeder Besucher herunterlädt, wo dein Code sie als import.meta.env.VITE_SUPABASE_URL liest. Eine Variable namens DB_PASSWORD, ohne Präfix, kommt im Browser leer zurück. Vites eigene Dokumentation sagt, dass die Werte mit Präfix beim Build in deinen Quellcode gebündelt werden und keine API-Schlüssel enthalten sollten.

Next.js, mit dem v0 baut, hat dieselbe Regel unter anderem Namen: NEXT_PUBLIC_. Seine Dokumentation beschreibt den Wert als inlined, ein fest eingetragener String, der beim Build in das Browser-Bundle geschrieben wird. Expo benutzt EXPO_PUBLIC_ und warnt mit denselben Worten. Ältere Create-React-App-Projekte benutzen REACT_APP_. Jeder Präfix sagt seinem Build-Tool dasselbe: der hier kommt in die Kiste.

Warum sich eine .env-Datei privat anfühlt und es nicht ist

Weil die Gewohnheit, einen Schlüssel in einer .env-Datei zu halten, von Servern stammt, wo sie funktioniert.

Auf einem Server liegen die Datei und der Code, der sie liest, auf einer Maschine, die du kontrollierst. Ein Besucher bekommt eine Antwort von dieser Maschine und sieht die Datei nie. Den Schlüssel aus dem Code herauszuhalten und in eine Datei zu legen, die der Server beim Start liest, ist dort gute Praxis, und dort wurde jedes Tutorial geschrieben, das sagt: „leg ihn in .env“.

Deine .env-Datei hat zwei Leser, und die Tutorials handeln von einem davon. Der erste ist jeder, der dein Projekt sehen kann: ein Mitstreiter, ein öffentliches GitHub-Repository, die Dateiansicht des Builders selbst. Ein Eintrag in .gitignore, einer Liste von Dateien, die git auslässt, hält .env von diesem Leser fern. Der zweite Leser ist der Build, der die Datei bei jedem Veröffentlichen öffnet und alles herauskopiert, was den Präfix trägt. .gitignore sagt ihm nichts.

Eine Datei, zwei Leser. Die .gitignore-Wand hält einen davon auf. Der Build trägt jeden Wert mit Präfix zum anderen.

„Meine .env steht in .gitignore“ stimmt also, und es beantwortet eine andere Frage. Die Datei ist aus deinem Repository herausgeblieben. Die Werte mit Präfix sind trotzdem in die App gewandert, weil das der Weg ist, den der Präfix öffnet, und in einem Lovable-, Bolt- oder v0-Projekt läuft der meiste Code, den du bearbeitet hast, im Browser, wo es keinen Server gibt, hinter dem die Datei bleiben könnte.

Warum die KI zum Präfix gegriffen hat

Weil man so einen Wert im Browsercode zum Laufen bringt, und weil das Modell keine Ahnung hat, was der Wert kostet.

Du hast um eine Karte gebeten, oder um ein Chat-Feature, das Fragen zu deinem Produkt beantwortet. Der Code, den der Builder dafür geschrieben hat, läuft im Browser des Besuchers, und Browsercode, der process.env.OPENAI_API_KEY liest, bekommt gar nichts. Der Weg, den Wert ankommen zu lassen, ist der Präfix. Der Builder benennt die Variable in VITE_OPENAI_API_KEY um, das Feature funktioniert in der Vorschau, und nirgends wird ein Fehler gemeldet, weil aus Sicht des Build-Tools nichts schiefgelaufen ist.

Der Präfix urteilt nicht. Er ist dieselbe Anweisung für einen Google-Maps-Schlüssel, der öffentlich sein soll, sobald er eingeschränkt ist, und für einen geheimen Stripe-Schlüssel, der jedem deiner Kunden Geld zurückerstatten kann. Ein Mensch, der wüsste, dass der zweite deine Karte belastet, würde innehalten. Das Modell weiß, dass das Feature nicht lief, bis die Zeile da war, und dann lief es.

Deshalb taucht das bei Apps auf, deren Besitzer alles getan haben, was man ihnen gesagt hat. Der Wert wurde aus dem Code herausgenommen, er lag in .env, die Datei stand in .gitignore, und die App liefert ihn trotzdem aus, weil der eine Schritt, der ihn veröffentlicht, aussieht wie der Schritt, der ihn zum Laufen bringt.

Welche Werte hinter den Präfix gehören

Eine Adresse und ein veröffentlichbarer Schlüssel. Alles, was Geld ausgibt oder deine Datenbankregeln ignoriert, nicht.

WertHinter VITE_ oder NEXT_PUBLIC_?Warum
VITE_SUPABASE_URLGehört hierherEine Adresse. Sie sagt, mit welchem Projekt deine App spricht, und sonst nichts.
VITE_SUPABASE_PUBLISHABLE_KEY, oder VITE_SUPABASE_ANON_KEY bei einem älteren ProjektGehört hierherFür den Browser gebaut. Jede Anfrage, die er stellt, wird weiterhin von Row Level Security gefiltert, den Regeln an jeder Tabelle, die Zeile für Zeile entscheiden, wer was lesen darf.
Ein Stripe-Schlüssel pk_live_Gehört hierherBaut Zahlungsformulare. Kann weder abbuchen noch erstatten noch Kunden lesen.
Ein Google-Maps-SchlüsselGehört hierher, sobald eingeschränktVon Natur aus öffentlich. Eine Referrer-Einschränkung in Google Cloud ist das, was einen Fremden davon abhält, dir damit Kosten zu verursachen.
Ein OpenAI- oder Anthropic-SchlüsselNieEs gibt keine veröffentlichbare Variante. Wer ihn hält, gibt dein Geld aus.
Ein Supabase-Schlüssel service_role oder sb_secret_NieUmgeht Row Level Security und liest jede Zeile in jeder Tabelle.
Ein Stripe-Schlüssel sk_live_NieAbbuchungen, Erstattungen, Auszahlungen und jeder Kundendatensatz.
Ein AWS-ZugangsschlüsselNieWas immer dieses Konto kann, von überall.

Wenn deine App VITE_SUPABASE_URL und daneben einen veröffentlichbaren Schlüssel hat, ist das das Paar, das Supabase für einen Browser vorgesehen hat, und unser Scan markiert es als „gehört hierher“. Die Adresse und der veröffentlichbare Schlüssel sind der Grund, warum es den Präfix gibt.

Der Test für alles andere ist, ob es dich stören würde, den Wert auf deiner Startseite abgedruckt zu sehen. Der Präfix legt ihn einen Klick weiter weg als das, in eine Datei statt auf die Seite, und wer die Datei will, hat sie. Die beiden Familien auseinanderzuhalten, am Präfix und bei einem älteren Supabase-Schlüssel an der Rolle darin, ist ein eigener Artikel.

Was 30.998 Apps hinter dem Präfix ausgeliefert haben

Meistens Google-API-Schlüssel. 52 Apps lieferten einen Schlüssel aus, der Geld ausgibt oder alles liest.

Im August 2026 haben wir dieselben neun Checks auf 30.998 live geschalteten vibe-gecodeten Apps laufen lassen. 1.332 davon, 4 %, lieferten etwas Schlüsselförmiges in dem Code aus, den jeder Besucher herunterlädt. 1.142 davon waren Google-API-Schlüssel, die meist eine Einschränkung in Google Cloud brauchen und keine Rotation. 204 lieferten einen zufällig aussehenden Wert neben einem Namen wie secret oder password aus, der ein echter Zugang sein kann oder auch nicht.

Die teuren waren selten. 33 Apps lieferten einen OpenAI-Schlüssel aus, 9 einen AWS-Zugangsschlüssel, 5 einen Anthropic-Schlüssel, 3 einen geheimen Stripe-Schlüssel und 3 einen Supabase-service_role-Schlüssel: 52 Apps insgesamt, da eine davon zwei trug. Der Wert hinter dem Präfix ist also meist ein Google-Schlüssel, und die Lösung dafür ist eine Einstellung. Der seltene Fall ist der, in dem der Schaden liegt, und was ein geleakter OpenAI-Schlüssel kostet ist der Beitrag dazu.

Wie du prüfst, was deine eigene App ausliefert

Öffne die live geschaltete App, drücke F12 und durchsuche jede geladene Datei nach dem Wert.

  1. Öffne deine veröffentlichte App in einem Browser, unter ihrer echten Adresse. Die Vorschau des Builders ist ein anderer Build und kann eine Version hinterherhängen.
  2. Drücke F12, um die Entwicklerwerkzeuge zu öffnen, und wähle dann den Reiter Sources.
  3. Drücke Strg+Umschalt+F, oder Cmd+Wahl+F auf einem Mac. Das öffnet eine Suche über jede Datei, die die Seite geladen hat.
  4. Füge die ersten etwa zehn Zeichen des Werts ein, um den du dir Sorgen machst, und füge ihn nirgends sonst ein.

Ein Treffer heißt, dass der Wert in der Kiste liegt, die jeder Besucher bekommt. Suche nach dem Wert statt nach dem Namen: Der Build ersetzt import.meta.env.VITE_OPENAI_API_KEY meist durch den Wert selbst, sodass eine Suche nach VITE_ leer ausgehen kann, während jeder Wert dahinter da ist.

Der Build ersetzt den Namen durch den Wert. Wer die live geschaltete App nach VITE_ durchsucht, findet nichts; wer nach dem Wert sucht, findet ihn.

Wenn du deine App lieber nicht Wert für Wert durchgehen willst, liest unser kostenloser Scan deine live geschaltete Seite von außen, alle neun Checks, in etwa 20 Sekunden und ohne Konto. Er benennt jeden gefundenen Schlüssel nach seiner Art, sagt, welche in einen Browser gehören, und schreibt „Konnte nicht prüfen“ für alles, was er nicht beantworten konnte, statt ein Häkchen zu setzen: scanne deine App.

Wo ein echtes Geheimnis stattdessen hingehört

Auf eine Maschine, von der deine Besucher nie etwas herunterladen. In einem Supabase-Projekt ist das eine Edge Function, ein kleines Stück Servercode, das Supabase für dich ausführt; in einer Next.js-App ist es eine Server-Route; in einer Replit-App ist es die Serverhälfte.

Die Form ist überall dieselbe. Dein Browsercode bittet deine Funktion, die Arbeit zu erledigen. Die Funktion hält den Schlüssel, ruft OpenAI oder Stripe auf und schickt die Antwort zurück. Der Schlüssel bleibt auf der Maschine, und der Besucher bekommt ein Ergebnis. Der OpenAI-Beitrag zeichnet es als ein Objekt, das um eine Box nach rechts wandert, und mehr ist die Änderung nicht.

Zwei Dinge sagen dir, dass der Builder getan hat, worum du gebeten hast. Die Variable hat ihren Präfix verloren, heißt also OPENAI_API_KEY, und sie lebt in den eigenen Secrets der Funktion, gesetzt im Supabase-Dashboard unter Edge Functions, ohne etwas in der .env der App. Und die Datei, die sie liest, liegt unter supabase/functions/ oder app/api/, irgendwo, wo der Build nie einpackt, statt unter src/.

Eine Reihenfolge ist wichtig, und man dreht sie leicht um. Wenn ein geheimer Schlüssel schon hinter einem Präfix ausgeliefert wurde, hilft das Verschieben nichts gegen die Kopien, die schon heruntergeladen sind. Rotiere ihn zuerst beim Anbieter, dann verschiebe die Arbeit. Ob zuerst rotieren oder zuerst das Leck schließen hängt davon ab, ob die Kopie schon öffentlich ist, und sobald sie in einem veröffentlichten Bundle war, ist sie es.

Bei einem Replit-Projekt hat dieselbe Trennung einen eigenen Namen, Secrets, und was das Secrets-Werkzeug abdeckt und was nicht ist ein eigener Beitrag.

Was du jetzt tun solltest

Was zu tun ist

  • Durchsuche dein Projekt nach VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_ und REACT_APP_. Jeder Treffer ist ein Wert, den dein Build absichtlich veröffentlicht. Entscheide für jeden, ob er öffentlich sein darf.
  • Behalte die Adresse und den veröffentlichbaren Schlüssel. VITE_SUPABASE_URL und VITE_SUPABASE_PUBLISHABLE_KEY, oder der anon-Schlüssel bei einem älteren Projekt, sind das Paar, für das es den Präfix gibt.
  • Ein geheimer Schlüssel hinter einem Präfix wird zuerst beim Anbieter rotiert, dann verschoben. Die Zeile zu löschen holt keine Kopie zurück, die schon heruntergeladen wurde.
  • Verschiebe die Arbeit, die den Schlüssel brauchte, in eine Edge Function oder eine Server-Route, mit dem Schlüssel in den Secrets dieser Funktion und ohne Präfix im Namen.
  • Schränke einen Google-Schlüssel in Google Cloud per Referrer ein. Der braucht eine Einstellung und behält seinen Wert.
  • Durchsuche nach dem nächsten Veröffentlichen die live geschaltete App nach jedem Wert, den du verschoben hast.

Jedes Veröffentlichen packt eine neue Kiste. Das nächste Feature, um das du bittest, ist eine weitere Gelegenheit für einen Wert, den Präfix zu bekommen, und nichts zwischen dem Builder und dem Internet liest das Bundle auf dem Weg nach draußen. Ein Scan vom letzten Monat hat das Bundle vom letzten Monat gelesen.

Reeve Monitor liest das Bundle für dich. Er führt alle neun Checks jede Stunde auf bis zu drei Apps aus, sagt dir Bescheid, wenn sich ein Ergebnis ändert, prüft die Erreichbarkeit alle 60 Sekunden und schickt einen Monatsbericht. Ein Schlüssel, der mit dem Veröffentlichen am Dienstag ins Bundle gelangt, steht im Re-Scan dieser Stunde, ob du daran gedacht hast nachzusehen oder nicht. Er kostet €12 im Monat zum Listenpreis, mit sieben Tagen kostenlos, bevor er dir etwas berechnet; die Preisseite liegt manchmal unter der Zahl hier und nie darüber.

Wenn du das lieber als Liste abarbeiten willst, deckt die 10-Minuten-Sicherheitscheckliste das hier und die anderen Dinge ab, die man in einer frisch gestarteten App abschalten sollte.

FAQ

Sind .env-Dateien geheim?

Vor deinem Repository ja, wenn die Datei in .gitignore steht. Vor deinen Besuchern nein. Der Build liest .env bei jedem Veröffentlichen und kopiert jeden Wert mit dem Präfix VITE_ oder NEXT_PUBLIC_ in das JavaScript, das deine App ausliefert. Die Datei selbst verlässt deinen Rechner nie; die Werte, die du für den Browser markiert hast, schon.

Ist es sicher, VITE_SUPABASE_ANON_KEY offenzulegen?

Er gehört dorthin. Der anon-Schlüssel, bei einem neueren Projekt publishable key genannt, ist dafür gebaut, im Browser zu liegen. Er sagt nur, zu welchem Projekt eine Anfrage gehört, und jede Anfrage, die er stellt, wird von deinen Row-Level-Security-Regeln gefiltert. Das gilt genau so lange, wie diese Regeln an und korrekt sind, und das ist eine eigene Prüfung.

Ist NEXT_PUBLIC_ irgendwie anders als VITE_?

Dieselbe Regel, ein anderes Build-Tool. Next.js schreibt den Wert jeder NEXT_PUBLIC_-Variable beim Build als fest eingetragenen String in das Browser-Bundle, und eine Variable ohne den Präfix kommt im Browsercode leer zurück. Expo macht dasselbe mit EXPO_PUBLIC_, ältere Create-React-App-Projekte mit REACT_APP_. Egal, welches Tool deine App gebaut hat, der Präfix heißt veröffentlichen.

Wie prüfe ich, was in meinem Bundle steckt?

Öffne die live geschaltete App, drücke F12, wähle Sources und drücke Strg+Umschalt+F (Cmd+Wahl+F auf einem Mac), um jede Datei zu durchsuchen, die die Seite geladen hat. Füge die ersten paar Zeichen des Werts ein. Suche nach dem Wert statt nach dem Variablennamen, denn der Build ersetzt den Namen meist durch den Wert, sodass VITE_ fehlen kann, während der Schlüssel da ist. Unser kostenloser Scan macht dieselbe Lesung von außen in etwa 20 Sekunden.

Wo sollte ein geheimer Schlüssel in einer Lovable- oder Bolt-App liegen?

In einer Supabase Edge Function, mit dem Schlüssel in den Secrets dieser Funktion im Supabase-Dashboard und ohne VITE_-Präfix im Namen. Dein Browsercode ruft die Funktion auf, die Funktion ruft den Anbieter mit dem Schlüssel auf, und der Schlüssel erreicht nie einen Besucher. Wenn der Schlüssel schon ausgeliefert wurde, rotiere ihn beim Anbieter, bevor du ihn verschiebst.

Schützt .gitignore meine Schlüssel?

Es hält die .env-Datei aus git heraus, sodass niemand, der dein Repository liest, sie sieht. Auf den Build hat es keine Wirkung; der liest die Datei direkt und veröffentlicht jeden Wert mit Präfix. Eine .env in .gitignore mit VITE_OPENAI_API_KEY darin liefert diesen Schlüssel trotzdem an jeden Besucher aus.

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.