Aller au contenu

Bases de la sécurité

Variables d’environnement Vite exposées : le préfixe dit publier

Variables d’environnement Vite exposées : votre appli a fait ce que le préfixe demandait. VITE_ et NEXT_PUBLIC_ veulent dire publier, et l’IA ignorait le prix.

Vlad Tkachenko11 min de lecture
Un fichier .env avec trois valeurs, deux étiquetées VITE_, et ces deux valeurs redessinées dans une fenêtre de navigateur à côté.

En bref

  • Les variables d’environnement Vite exposées dans votre appli sont là parce que le préfixe l’a demandé. Une variable nommée VITE_ ou NEXT_PUBLIC_ est copiée dans le JavaScript que chaque visiteur télécharge, et la garder dans un fichier .env n’y change rien.
  • Le préfixe est la bonne étiquette pour une adresse ou une clé publiable, et la mauvaise pour tout ce qui dépense de l’argent ou ignore les règles de votre base de données. Le build ne fait pas la différence, et l’IA qui a écrit la ligne non plus.
  • Sur 30 998 applis vibe-codées en ligne que nous avons scannées, 1 332 livraient quelque chose qui ressemble à une clé. 1 142 d’entre elles étaient des clés d’API Google, qui ont en général besoin d’une restriction et pas d’une rotation. 52 livraient une clé qui dépense de l’argent ou lit tout.

Quelqu’un a ouvert votre appli Lovable, appuyé sur F12, et trouvé dans le code une valeur que vous êtes certain d’avoir mise dans un fichier .env. Ou vous avez demandé une fonctionnalité au builder, il a écrit une ligne commençant par VITE_, et quelque chose que vous avez lu depuis dit que ce préfixe est par là que les clés fuient. Le fichier a .env dans son nom et tous les tutoriels disent de ne jamais le partager. Des variables d’environnement Vite exposées à chaque visiteur, cela ressemble à un bug de Vite.

Voici la partie que guide après guide explique de travers : rien n’a échoué à cacher cette valeur. VITE_ et NEXT_PUBLIC_ sont une instruction pour votre outil de build, et l’instruction est de mettre la valeur dans l’appli que chaque visiteur télécharge. L’outil a lu l’étiquette et a fait ce qu’elle disait.

Un build emballe votre appli dans une boîte, et chaque visiteur reçoit une copie de la boîte. Le préfixe est une étiquette sur une valeur qui dit : emballe celle-ci aussi. Le reste de cet article porte sur les valeurs pour lesquelles cette étiquette est juste, sur la raison pour laquelle l’IA la colle sur les mauvaises, et sur la façon de voir ce qu’il y a dans votre propre boîte.

Les variables d’environnement Vite sont-elles exposées aux visiteurs ?

Celles qui commencent par VITE_, oui, et c’est à cela que sert le préfixe.

Vite, l’outil de build derrière la plupart des applis Lovable et Bolt, lit votre fichier .env à chaque build. Une variable nommée VITE_SUPABASE_URL est copiée dans le bundle, le fichier JavaScript compressé que chaque visiteur télécharge, où votre code la lit sous la forme import.meta.env.VITE_SUPABASE_URL. Une variable nommée DB_PASSWORD, sans préfixe, arrive vide dans le navigateur. La documentation de Vite elle-même dit que les valeurs préfixées sont intégrées à votre code source au moment du build et ne doivent pas contenir de clés d’API.

Next.js, avec lequel v0 construit, a la même règle sous un autre nom : NEXT_PUBLIC_. Sa documentation décrit la valeur comme inlined, une chaîne figée écrite dans le bundle du navigateur au moment du build. Expo utilise EXPO_PUBLIC_ et prévient avec les mêmes mots. Les anciens projets Create React App utilisent REACT_APP_. Chaque préfixe dit la même chose à son outil de build : celle-ci va dans la boîte.

Pourquoi un fichier .env semble privé et ne l’est pas

Parce que l’habitude de garder une clé dans un fichier .env vient des serveurs, où elle fonctionne.

Sur un serveur, le fichier et le code qui le lit sont sur une machine que vous contrôlez. Un visiteur reçoit une réponse de cette machine et ne voit jamais le fichier. Sortir la clé du code et la mettre dans un fichier que le serveur lit au démarrage est une bonne pratique là-bas, et c’est là qu’a été écrit chaque tutoriel qui dit « mettez-la dans .env ».

Votre fichier .env a deux lecteurs, et les tutoriels parlent d’un seul. Le premier, c’est quiconque peut voir votre projet : un collaborateur, un dépôt GitHub public, la vue des fichiers du builder lui-même. Une entrée dans .gitignore, qui est une liste de fichiers que git laisse de côté, tient .env à l’écart de ce lecteur. Le second lecteur est le build, qui ouvre le fichier à chaque publication et en copie tout ce qui porte le préfixe. .gitignore ne lui dit rien.

Un fichier, deux lecteurs. Le mur .gitignore arrête l’un d’eux. Le build porte chaque valeur préfixée jusqu’à l’autre.

Donc « mon .env est dans gitignore » est vrai, et répond à une autre question. Le fichier est resté hors de votre dépôt. Les valeurs préfixées sont quand même entrées dans l’appli, parce que c’est la route que le préfixe ouvre, et dans un projet Lovable, Bolt ou v0, la plus grande partie du code que vous avez modifié tourne dans le navigateur, où il n’y a aucun serveur derrière lequel le fichier pourrait rester.

Pourquoi l’IA a pris le préfixe

Parce que c’est ainsi qu’on fait fonctionner une valeur dans du code de navigateur, et que le modèle n’a aucune idée de ce que la valeur coûte.

Vous avez demandé une carte, ou un chat qui répond aux questions sur votre produit. Le code que le builder a écrit pour cela tourne dans le navigateur du visiteur, et du code de navigateur qui lit process.env.OPENAI_API_KEY ne reçoit rien du tout. Le moyen de faire arriver la valeur, c’est le préfixe. Le builder renomme la variable VITE_OPENAI_API_KEY, la fonctionnalité marche dans l’aperçu, et aucune erreur n’est levée nulle part, parce que du côté de l’outil de build rien ne s’est mal passé.

Le préfixe ne juge pas. C’est la même instruction pour une clé Google Maps, qui est censée être publique une fois restreinte, et pour une clé secrète Stripe, qui peut rembourser chacun de vos clients. Une personne qui saurait que la seconde débite votre carte s’arrêterait. Le modèle sait que la fonctionnalité ne marchait pas tant que la ligne n’était pas là, et qu’ensuite elle a marché.

C’est pour cela que cela apparaît sur des applis dont les propriétaires ont fait tout ce qu’on leur a dit. La valeur a été sortie du code, gardée dans .env, le fichier a été mis dans gitignore, et l’appli la livre quand même, parce que la seule étape qui la publie ressemble à l’étape qui la fait marcher.

Quelles valeurs vont derrière le préfixe

Une adresse et une clé publiable. Tout ce qui dépense de l’argent ou ignore les règles de votre base de données, non.

ValeurDerrière VITE_ ou NEXT_PUBLIC_ ?Pourquoi
VITE_SUPABASE_URLÀ sa placeUne adresse. Elle dit à quel projet votre appli parle, et rien d’autre.
VITE_SUPABASE_PUBLISHABLE_KEY, ou VITE_SUPABASE_ANON_KEY sur un projet plus ancienÀ sa placeConçue pour le navigateur. Chaque requête qu’elle fait reste filtrée par Row Level Security, les règles sur chaque table qui décident ligne par ligne qui peut lire quoi.
Une clé Stripe pk_live_À sa placeConstruit des formulaires de paiement. Ne peut ni débiter, ni rembourser, ni lire les clients.
Une clé Google MapsÀ sa place, une fois restreintePublique par conception. Une restriction par référent dans Google Cloud est ce qui empêche un inconnu de vous facturer avec.
Une clé OpenAI ou AnthropicJamaisIl n’existe pas de variante publiable. Qui la détient dépense votre argent.
Une clé Supabase service_role ou sb_secret_JamaisContourne Row Level Security et lit chaque ligne de chaque table.
Une clé Stripe sk_live_JamaisDébits, remboursements, virements et chaque fiche client.
Une clé d’accès AWSJamaisTout ce que ce compte peut faire, depuis n’importe où.

Si votre appli a VITE_SUPABASE_URL et une clé publiable à côté, c’est la paire que Supabase a prévue pour un navigateur, et notre scan la marque comme étant à sa place. L’adresse et la clé publiable sont la raison d’être du préfixe.

Le test pour tout le reste, c’est de savoir si cela vous gênerait de voir la valeur imprimée sur votre page d’accueil. Le préfixe la met un clic plus loin que cela, dans un fichier au lieu de la page, et quiconque veut le fichier l’a. Distinguer les deux familles, par le préfixe et, sur une ancienne clé Supabase, par le rôle qu’elle contient, est un article à part.

Ce que 30 998 applis livraient derrière le préfixe

Surtout des clés d’API Google. 52 applis livraient une clé qui dépense de l’argent ou lit tout.

En août 2026, nous avons lancé les neuf mêmes vérifications sur 30 998 applis vibe-codées en ligne. 1 332 d’entre elles, soit 4 %, livraient quelque chose qui ressemble à une clé dans le code que chaque visiteur télécharge. 1 142 de celles-ci étaient des clés d’API Google, qui ont en général besoin d’une restriction définie dans Google Cloud et pas d’une rotation. 204 livraient une valeur d’allure aléatoire à côté d’un nom comme secret ou password, qui peut être un vrai identifiant ou non.

Les coûteuses étaient rares. 33 applis livraient une clé OpenAI, 9 une clé d’accès AWS, 5 une clé Anthropic, 3 une clé secrète Stripe et 3 une clé service_role Supabase : 52 applis en tout, puisque l’une d’elles en portait deux. La valeur derrière le préfixe est donc en général une clé Google, et le remède pour cela est un réglage. Le cas rare est celui où se trouvent les dégâts, et ce que coûte une clé OpenAI qui a fui est l’article pour celui-là.

Comment vérifier ce que votre propre appli livre

Ouvrez l’appli en ligne, appuyez sur F12, et cherchez la valeur dans chaque fichier chargé.

  1. Ouvrez votre appli publiée dans un navigateur, à sa vraie adresse. L’aperçu du builder est un autre build et peut avoir une version de retard.
  2. Appuyez sur F12 pour ouvrir les outils de développement, puis choisissez l’onglet Sources.
  3. Appuyez sur Ctrl+Maj+F, ou Cmd+Option+F sur un Mac. Cela ouvre une recherche dans chaque fichier que la page a chargé.
  4. Collez les dix premiers caractères environ de la valeur qui vous inquiète, et ne la collez nulle part ailleurs.

Un résultat veut dire que la valeur est dans la boîte que reçoit chaque visiteur. Cherchez la valeur plutôt que le nom : le build remplace en général import.meta.env.VITE_OPENAI_API_KEY par la valeur elle-même, si bien qu’une recherche de VITE_ peut revenir vide alors que chaque valeur qu’il y avait derrière est là.

Le build remplace le nom par la valeur. Chercher VITE_ dans l’appli en ligne ne trouve rien ; chercher la valeur la trouve.

Si vous préférez ne pas parcourir votre appli valeur par valeur, notre scan gratuit lit votre site en ligne depuis l’extérieur, les neuf vérifications, en 20 secondes environ et sans compte. Il nomme chaque clé trouvée par son type, dit lesquelles ont leur place dans un navigateur, et écrit « Impossible à vérifier » pour tout ce à quoi il n’a pas pu répondre plutôt qu’une coche : scannez votre appli.

Où va un vrai secret à la place

Sur une machine depuis laquelle vos visiteurs ne téléchargent jamais rien. Dans un projet Supabase, c’est une Edge Function, un petit morceau de code serveur que Supabase exécute pour vous ; dans une appli Next.js, c’est une route serveur ; dans une appli Replit, c’est la moitié serveur.

La forme est la même partout. Votre code de navigateur demande à votre fonction de faire le travail. La fonction détient la clé, passe l’appel à OpenAI ou Stripe, et renvoie la réponse. La clé reste sur la machine et le visiteur reçoit un résultat. L’article sur OpenAI le dessine comme un objet qui se déplace d’une case vers la droite, et le changement se résume à cela.

Deux choses vous disent que le builder a fait ce que vous avez demandé. La variable a perdu son préfixe, donc c’est OPENAI_API_KEY, et elle vit dans les secrets propres à la fonction, définis dans le tableau de bord Supabase sous Edge Functions, sans rien dans le .env de l’appli. Et le fichier qui la lit se trouve sous supabase/functions/ ou app/api/, quelque part que le build n’emballe jamais, au lieu de sous src/.

Un ordre compte, et il est facile de l’inverser. Si une clé secrète a déjà été livrée derrière un préfixe, la déplacer ne fait rien pour les copies déjà téléchargées. Renouvelez-la d’abord chez le fournisseur, puis déplacez le travail. Renouveler d’abord ou fermer la fuite d’abord dépend de si la copie est déjà publique, et dès qu’elle a été dans un bundle publié, elle l’est.

Pour un projet Replit, la même séparation a son propre nom, Secrets, et ce que l’outil Secrets couvre et ce qu’il ne couvre pas est un article à part.

Quoi faire maintenant

Que faire

  • Cherchez dans votre projet VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_ et REACT_APP_. Chaque occurrence est une valeur que votre build publie exprès. Décidez pour chacune si elle peut être publique.
  • Gardez l’adresse et la clé publiable. VITE_SUPABASE_URL et VITE_SUPABASE_PUBLISHABLE_KEY, ou la clé anon sur un projet plus ancien, sont la paire pour laquelle le préfixe existe.
  • Une clé secrète derrière un préfixe est d’abord renouvelée chez le fournisseur, puis déplacée. Supprimer la ligne ne rappelle pas une copie déjà téléchargée.
  • Déplacez le travail qui avait besoin de la clé dans une Edge Function ou une route serveur, avec la clé dans les secrets de cette fonction et sans préfixe sur son nom.
  • Restreignez une clé Google par référent dans Google Cloud. Celle-là a besoin d’un réglage et garde sa valeur.
  • Après la prochaine publication, cherchez dans l’appli en ligne chaque valeur que vous avez déplacée.

Chaque publication emballe une nouvelle boîte. La prochaine fonctionnalité que vous demandez est une nouvelle occasion pour une valeur de recevoir le préfixe, et rien entre le builder et internet ne lit le bundle en sortie. Un scan du mois dernier a lu le bundle du mois dernier.

Reeve Monitor lit le bundle pour vous. Il relance les neuf vérifications toutes les heures sur trois applis au plus, vous prévient quand un résultat change, surveille la disponibilité toutes les 60 secondes, et envoie un rapport mensuel. Une clé qui atteint le bundle avec la publication de mardi figure dans le re-scan de cette heure-là, que vous ayez pensé à regarder ou non. Il coûte €12 par mois au tarif de base, avec sept jours gratuits avant de vous facturer ; la page des tarifs est parfois en dessous du chiffre donné ici et jamais au-dessus.

Si vous préférez traiter cela sous forme de liste, la checklist de sécurité en 10 minutes couvre ce point et les autres choses qu’il vaut la peine de désactiver dans une appli fraîchement lancée.

FAQ

Les fichiers .env sont-ils secrets ?

Pour votre dépôt, oui, si le fichier est listé dans .gitignore. Pour vos visiteurs, non. Le build lit .env à chaque publication et copie chaque valeur portant le préfixe VITE_ ou NEXT_PUBLIC_ dans le JavaScript que livre votre appli. Le fichier lui-même ne quitte jamais votre machine ; les valeurs que vous avez étiquetées pour le navigateur, si.

Peut-on exposer VITE_SUPABASE_ANON_KEY sans risque ?

Elle est faite pour être là. La clé anon, appelée clé publiable sur un projet récent, est conçue pour vivre dans un navigateur. Elle dit seulement à quel projet appartient une requête, et chaque requête qu’elle fait est filtrée par vos règles Row Level Security. Cela tient exactement tant que ces règles sont activées et correctes, ce qui est une vérification à part.

NEXT_PUBLIC_ est-il différent de VITE_ ?

Même règle, autre outil de build. Next.js écrit la valeur de toute variable NEXT_PUBLIC_ dans le bundle du navigateur comme une chaîne figée au moment du build, et une variable sans le préfixe arrive vide dans le code du navigateur. Expo fait pareil avec EXPO_PUBLIC_, et les anciens projets Create React App avec REACT_APP_. Quel que soit l’outil qui a construit votre appli, le préfixe veut dire publier.

Comment vérifier ce qu’il y a dans mon bundle ?

Ouvrez l’appli en ligne, appuyez sur F12, choisissez Sources et appuyez sur Ctrl+Maj+F (Cmd+Option+F sur un Mac) pour chercher dans chaque fichier que la page a chargé. Collez les premiers caractères de la valeur. Cherchez la valeur plutôt que le nom de la variable, parce que le build remplace en général le nom par la valeur, si bien que VITE_ peut être absent alors que la clé est là. Notre scan gratuit fait la même lecture depuis l’extérieur en 20 secondes environ.

Où doit vivre une clé secrète dans une appli Lovable ou Bolt ?

Dans une Supabase Edge Function, avec la clé définie dans les secrets de cette fonction dans le tableau de bord Supabase et sans préfixe VITE_ sur son nom. Votre code du navigateur appelle la fonction, la fonction appelle le fournisseur avec la clé, et la clé n’atteint jamais un visiteur. Si la clé a déjà été livrée, renouvelez-la chez le fournisseur avant de la déplacer.

.gitignore protège-t-il mes clés ?

Il garde le fichier .env hors de git, donc personne qui lit votre dépôt ne le voit. Il n’a aucun effet sur le build, qui lit le fichier directement et publie chaque valeur préfixée. Un .env ignoré par git avec VITE_OPENAI_API_KEY dedans livre quand même cette clé à chaque visiteur.

Écrit par

Vlad Tkachenko

Fondateur de Reeve

Je passe mes journées à examiner des applications créées avec Lovable, Bolt, v0, Cursor et Replit, et la courte liste d'erreurs qui y reviennent sans cesse.

En savoir plus sur l'auteur

À lire ensuite

Tous les articles

Vous ne savez pas où en est votre propre application ?

Lancez une analyse gratuite et obtenez une note claire de A à F en une vingtaine de secondes. Sans compte, sans carte.

Analyser mon application

Vérification externe automatisée, pas un audit complet. L'absence de problème n'est pas une garantie de sécurité.