Guide
Votre clé d’API de LLM est probablement dans votre bundle frontend
Pourquoi les variables VITE_ et NEXT_PUBLIC_ finissent dans le navigateur, comment chercher une clé OpenAI ou Anthropic dans un build et la route serveur qui corrige.
· 7 min de lecture · Par l’équipe vetkit


Vous demandez à un assistant d’ajouter une fenêtre de chat à votre application React. Il installe le SDK OpenAI, lit la clé depuis import.meta.env.VITE_OPENAI_API_KEY, et la fonctionnalité marche du premier coup. Le problème, c’est qu’elle marche aussi pour quiconque ouvre les outils de développement du navigateur, trouve la clé dans votre JavaScript et commence à envoyer des requêtes sur votre compte.
C’est l’une des erreurs les plus fréquentes que nous voyons dans les projets développés avec l’aide de l’IA, et elle passe facilement inaperçue en revue, parce que rien n’a l’air faux. La clé n’est pas codée en dur. Elle se trouve dans un fichier .env correctement ignoré par git. Le code la lit depuis une variable d’environnement, exactement comme tout le monde recommande de le faire.
Comment cela arrive
Les outils de build frontend ne disposent pas de variables d’environnement à l’exécution. Le navigateur exécute un bundle statique. Ainsi, quand votre code lit import.meta.env.VITE_SOMETHING ou process.env.NEXT_PUBLIC_SOMETHING, le bundler remplace cette expression par la valeur littérale pendant le build. Une fois le build terminé, il n’y a plus de variable, seulement une chaîne posée dans un fichier JavaScript que chaque visiteur télécharge.
Les frameworks sont clairs sur ce point. Next.js indique que les variables publiques sont intégrées en ligne dans tout le JavaScript envoyé au navigateur. Vite n’expose au code client que les variables préfixées par VITE_, précisément pour que vous ne divulguiez pas les autres par accident. Le préfixe vaut consentement explicite. Quand un assistant choisit VITE_OPENAI_API_KEY parce que le nom sans préfixe ne fonctionnait pas dans le navigateur, il a donné ce consentement à votre place.
Il existe aussi une variante plus discrète. Certains templates de démarrage copient la clé dans le bundle via l’option define de Vite, par exemple 'process.env.GEMINI_API_KEY': JSON.stringify(...) dans vite.config.ts. La variable n’a pas de préfixe public, elle a donc l’air sûre, mais l’effet est exactement le même.
Les SDK officiels essaient d’empêcher cela. Le SDK Node d’OpenAI refuse de s’exécuter dans un navigateur sauf si vous passez dangerouslyAllowBrowser: true, et son README explique pourquoi : cela expose vos identifiants secrets dans le code côté client. Les assistants ajoutent parfois ce flag pour faire disparaître une erreur. D’autres clients, comme de simples appels HTTP ou des SDK sans vérification du navigateur, ne vous avertissent pas du tout.
Quelles variables sont publiques
Si une variable commence par l’un de ces préfixes, partez du principe que sa valeur est lisible par quiconque charge votre application :
VITE_dans Vite (et les frameworks construits dessus)NEXT_PUBLIC_dans Next.jsREACT_APP_dans Create React AppEXPO_PUBLIC_dans Expo et les applications React Native construites avecVUE_APP_dans Vue CLI etGATSBY_dans GatsbyPUBLIC_dans Astro et dans les modules d’environnement publics de SvelteKit
Ces préfixes conviennent aux valeurs destinées à être publiques, comme un identifiant de site d’analytics ou l’URL de votre propre API. Ils ne conviennent jamais à un secret de fournisseur : OpenAI, Anthropic, Google Gemini, Mistral, une clé secrète Stripe, une clé service role de base de données.
Vérifier votre build
Le test le plus rapide consiste à builder l’application et à chercher le préfixe de la clé dans le résultat. Les clés de projet OpenAI commencent par sk-proj-, les clés Anthropic par sk-ant- et les clés d’API Google par AIza.
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/nullSi la commande affiche quoi que ce soit, la clé est dans votre bundle. Examinez un site déployé de la même façon : ouvrez les outils de développement, allez dans l’onglet Sources ou Réseau, et cherchez dans les scripts chargés. Si vous la trouvez en trente secondes, un script qui parcourt les sites à la recherche de ce genre de chose la trouvera aussi.
La correction
Déplacez l’appel au fournisseur vers un serveur que vous contrôlez et faites dialoguer le navigateur avec ce serveur. Dans Next.js, c’est un route handler. Dans une single-page app Vite, ce peut être une fonction serverless sur le même hébergement. La clé garde un nom sans préfixe, si bien que le bundler ne l’intègre jamais.
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 });}Les trois détails autour de l’appel comptent autant que son déplacement. Sans contrôle d’authentification, votre route devient un proxy gratuit et anonyme vers le modèle : la même fuite, avec des étapes en plus. Sans limite de débit, un seul utilisateur connecté peut encore faire grimper la facture. Sans plafond sur la sortie, une seule requête peut générer bien plus de tokens que votre fonctionnalité n’en a besoin.
Gardez aussi le nom du modèle et les limites côté serveur
max_output_tokens, un visiteur peut les modifier. Fixez ces valeurs côté serveur et n’acceptez du client que le message de l’utilisateur.Si la clé a déjà été livrée
Retirer la variable du code ne suffit pas. Les anciens builds restent en cache dans les navigateurs et les CDN, et la clé a peut-être déjà été copiée. Considérez-la comme compromise :
- Créez une nouvelle clé dans le tableau de bord du fournisseur et révoquez l’ancienne.
- Placez la nouvelle clé uniquement dans les variables d’environnement côté serveur, sous un nom sans préfixe public.
- Consultez la page d’utilisation du fournisseur pour repérer un trafic que vous ne reconnaissez pas, et fixez un plafond de dépenses.
- Redéployez, puis refaites la recherche dans le build ci-dessus pour confirmer que le nouveau bundle est propre.
Si la clé a été commitée dans git à un moment ou à un autre, renouvelez-la même si vous avez supprimé le fichier. L’historique git conserve l’ancienne valeur.
La détecter avant la mise en ligne
Cette erreur est facile à éliminer par l’automatisation. Un scanner de secrets détecte les clés commitées dans le dépôt. Il ne peut pas voir, en revanche, une clé qui se trouve dans un fichier .env ignoré : il vous faut donc aussi un contrôle qui lit le code et remarque qu’une variable publique est transmise à un client LLM.
Ce second schéma fait partie des règles de sécurité LLM et agents des scans complets de vetkit : il signale une valeur issue d’une variable d’environnement côté client (par exemple import.meta.env.VITE_* ou process.env.NEXT_PUBLIC_*) qui atteint un client de SDK LLM dans du code exécuté par le navigateur, et explique comment déplacer l’appel côté serveur. Le Security Quick Scan gratuit de vetkit couvre les secrets commités, les dépendances présentant des vulnérabilités connues et les bases de la posture de sécurité. Ni l’un ni l’autre ne remplace un coup d’œil rapide au résultat de votre propre build, qui reste la preuve la plus directe que vous puissiez obtenir.