Zum Inhalt springen

Leitfaden

Ihr LLM-API-Schlüssel steckt wahrscheinlich in Ihrem Frontend-Bundle

Warum VITE_- und NEXT_PUBLIC_-Variablen im Browser landen, wie Sie einen Build auf geleakte OpenAI- oder Anthropic-Keys prüfen und welche Server-Route das Problem löst.

· 7 Min. Lesezeit · Vom vetkit-Team

Ein großer Schlüssel liegt gut sichtbar in einem gläsernen Browserfenster; ein Roboter poliert ahnungslos das Glas, während ein Entwickler alarmiert darauf zeigt und eine Hand im Handschuh nach dem Schlüssel greift.Ein großer Schlüssel liegt gut sichtbar in einem gläsernen Browserfenster; ein Roboter poliert ahnungslos das Glas, während ein Entwickler alarmiert darauf zeigt und eine Hand im Handschuh nach dem Schlüssel greift.

Sie bitten einen Assistenten, Ihrer React-App eine Chatbox hinzuzufügen. Er installiert das OpenAI SDK, liest den Schlüssel aus import.meta.env.VITE_OPENAI_API_KEY, und die Funktion klappt auf Anhieb. Das Problem: Sie klappt auch für jeden, der die Entwicklertools des Browsers öffnet, den Schlüssel in Ihrem JavaScript findet und anfängt, auf Ihre Rechnung Requests zu senden.

Das ist einer der häufigsten Fehler, die wir in KI-gestützten Projekten sehen, und im Review leicht zu übersehen, weil nichts falsch aussieht. Der Schlüssel ist nicht fest codiert. Er liegt in einer .env-Datei, die von Git korrekt ignoriert wird. Der Code liest ihn aus einer Umgebungsvariablen, genau wie es alle empfehlen.

Wie es passiert

Frontend-Build-Tools haben zur Laufzeit keine Umgebungsvariablen. Der Browser führt ein statisches Bundle aus. Wenn Ihr Code also import.meta.env.VITE_SOMETHING oder process.env.NEXT_PUBLIC_SOMETHING liest, ersetzt der Bundler diesen Ausdruck beim Build durch den wörtlichen Wert. Nach dem Build gibt es keine Variable mehr, nur noch einen String in einer JavaScript-Datei, die jeder Besucher herunterlädt.

Die Frameworks sagen das deutlich. Laut Next.js werden öffentliche Variablen in jedes an den Browser gesendete JavaScript eingebettet. Vite gibt nur Variablen mit dem Präfix VITE_ an Client-Code weiter, gerade damit Sie die anderen nicht versehentlich leaken. Das Präfix ist die bewusste Freigabe. Wenn ein Assistent VITE_OPENAI_API_KEY wählt, weil der Name ohne Präfix im Browser nicht funktioniert hat, hat er diese Freigabe für Sie erteilt.

Es gibt auch eine unauffälligere Variante. Manche Starter-Templates kopieren den Schlüssel mit der Vite-Option define ins Bundle, zum Beispiel mit 'process.env.GEMINI_API_KEY': JSON.stringify(...) in vite.config.ts. Die Variable hat kein öffentliches Präfix und wirkt daher sicher, der Effekt ist aber genau derselbe.

Die offiziellen SDKs versuchen, das zu verhindern. Das OpenAI Node SDK verweigert die Ausführung im Browser, solange Sie nicht dangerouslyAllowBrowser: true übergeben, und die README erklärt, warum: Damit liegen Ihre geheimen Zugangsdaten im clientseitigen Code offen. Assistenten fügen dieses Flag manchmal hinzu, damit eine Fehlermeldung verschwindet. Andere Clients, etwa generische HTTP-Aufrufe oder SDKs ohne Browser-Prüfung, warnen Sie überhaupt nicht.

Welche Variablen öffentlich sind

Wenn eine Variable mit einem dieser Präfixe beginnt, gehen Sie davon aus, dass ihr Wert für jeden lesbar ist, der Ihre App lädt:

  • VITE_ in Vite (und darauf aufbauenden Frameworks)
  • NEXT_PUBLIC_ in Next.js
  • REACT_APP_ in Create React App
  • EXPO_PUBLIC_ in Expo und damit gebauten React-Native-Apps
  • VUE_APP_ in Vue CLI und GATSBY_ in Gatsby
  • PUBLIC_ in Astro und in den öffentlichen Env-Modulen von SvelteKit

Für Werte, die öffentlich sein sollen, etwa eine Analytics-Site-ID oder die URL Ihrer eigenen API, sind diese Präfixe in Ordnung. Für ein Secret eines Anbieters sind sie es nie: OpenAI, Anthropic, Google Gemini, Mistral, ein Stripe Secret Key, ein Service-Role-Key einer Datenbank.

Prüfen Sie Ihren Build

Der schnellste Test: Bauen Sie die App und durchsuchen Sie die Ausgabe nach dem Präfix des Schlüssels. OpenAI-Projektschlüssel beginnen mit sk-proj-, Anthropic-Schlüssel mit sk-ant- und Google-API-Schlüssel mit AIza.

bash
npm run build# Vite writes to dist/, Next.js static chunks live in .next/static/grep -rEo "sk-(proj|ant)-[A-Za-z0-9_-]{8}|AIza[0-9A-Za-z_-]{8}" dist .next/static 2>/dev/null

Wenn das irgendetwas ausgibt, steckt der Schlüssel in Ihrem Bundle. Eine deployte Website prüfen Sie genauso: Öffnen Sie die Entwicklertools, wechseln Sie zum Tab „Sources“ oder „Network“ und durchsuchen Sie die geladenen Skripte. Wenn Sie ihn in dreißig Sekunden finden, findet ihn auch ein Skript, das Websites gezielt danach durchsucht.

Die Lösung

Verlagern Sie den Aufruf des Anbieters auf einen Server, den Sie kontrollieren, und lassen Sie den Browser stattdessen mit diesem Server sprechen. In Next.js ist das ein Route Handler. In einer Vite-Single-Page-App kann es eine Serverless-Funktion auf demselben Host sein. Der Schlüssel behält einen Namen ohne Präfix, sodass der Bundler ihn nie einbettet.

app/api/chat/route.ts
import OpenAI from 'openai'; // Server-only: no NEXT_PUBLIC_ prefix, never shipped to the browser.const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export async function POST(request: Request) {  const user = await requireSignedInUser(request); // your auth check  await rateLimit(user.id); // your limiter: per user, per minute   const { message } = await request.json();  if (typeof message !== 'string' || message.length > 4000) {    return Response.json({ error: 'Invalid message' }, { status: 400 });  }   const response = await client.responses.create({    model: 'gpt-5-mini',    input: message,    max_output_tokens: 800,  });  return Response.json({ reply: response.output_text });}

Die drei Details rund um den Aufruf sind genauso wichtig wie das Verlagern selbst. Ohne Auth-Prüfung wird Ihre Route zu einem kostenlosen, anonymen Proxy für das Modell, also demselben Leak mit ein paar Extraschritten. Ohne Rate Limit kann ein einzelner angemeldeter Nutzer trotzdem eine hohe Rechnung verursachen. Ohne Begrenzung der Ausgabe kann ein einziger Request weit mehr Tokens erzeugen, als Ihre Funktion braucht.

Legen Sie auch Modellname und Limits auf dem Server fest

Wenn der Browser den Modellnamen oder max_output_tokens mitschickt, kann ein Besucher sie ändern. Bestimmen Sie diese Werte serverseitig und akzeptieren Sie vom Client nur die Nachricht des Nutzers.

Wenn der Schlüssel schon ausgeliefert wurde

Die Variable aus dem Code zu entfernen, hilft für sich allein nicht. Alte Builds bleiben in Browsern und CDNs gecacht, und der Schlüssel wurde womöglich schon kopiert. Behandeln Sie ihn als kompromittiert:

  1. Erstellen Sie im Dashboard des Anbieters einen neuen Schlüssel und widerrufen Sie den alten.
  2. Hinterlegen Sie den neuen Schlüssel nur in serverseitigen Umgebungseinstellungen, unter einem Namen ohne öffentliches Präfix.
  3. Prüfen Sie auf der Nutzungsseite des Anbieters, ob es Traffic gibt, den Sie nicht zuordnen können, und legen Sie ein Ausgabenlimit fest.
  4. Deployen Sie neu und wiederholen Sie dann die Build-Suche von oben, um zu bestätigen, dass das neue Bundle sauber ist.

Wenn der Schlüssel jemals in Git committet wurde, rotieren Sie ihn, auch wenn Sie die Datei gelöscht haben. Die Git-Historie behält den alten Wert.

Vor der Auslieferung abfangen

Dieser Fehler lässt sich leicht wegautomatisieren. Ein Secret-Scanner findet Schlüssel, die ins Repository committet wurden. Einen Schlüssel in einer ignorierten .env-Datei sieht er allerdings nicht, deshalb brauchen Sie zusätzlich eine Prüfung, die den Code liest und bemerkt, dass eine öffentliche Variable an einen LLM-Client übergeben wird.

Dieses zweite Muster ist eine der Regeln für LLM- und Agent-Sicherheit in den vollständigen Scans von vetkit: Sie meldet einen Wert aus einer clientseitigen Umgebungsvariablen (zum Beispiel import.meta.env.VITE_* oder process.env.NEXT_PUBLIC_*), der in Browser-Code einen LLM-SDK-Client erreicht, und erklärt, wie Sie den Aufruf auf den Server verlagern. Der kostenlose Security Quick Scan von vetkit deckt committete Secrets, bekannte verwundbare Abhängigkeiten und grundlegende Sicherheitskonfiguration ab. Keines von beiden ersetzt einen kurzen Blick auf Ihre eigene Build-Ausgabe, den direktesten Beleg, den Sie bekommen können.

Early Access anfordern

Erzählen Sie uns kurz von Ihrem Team und den Repositorys, die Sie prüfen möchten. Wir nehmen Teams in kleinen Gruppen auf und antworten per E-Mail.

Wir verwenden diese Angaben nur, um auf Ihre Anfrage zu antworten. Datenschutzerklärung