Quand tu demandes à une IA de « connecter ton app à Stripe, OpenAI ou Supabase », elle met souvent la clé directement dans le code du frontend. Ça marche du premier coup, et c'est précisément pour ça que personne ne vérifie. Le problème est simple et absolu : tout ce qui arrive au navigateur est public. Il n'existe aucun moyen de cacher un texte que le navigateur a besoin de lire. Ce guide explique pourquoi ça arrive, comment un attaquant le trouve en une minute, quelles clés sont dangereuses et lesquelles ne le sont pas, comment les chercher toi-même et quoi faire si l'une d'elles a déjà fuité.
Pourquoi la clé finit dans le navigateur
Il y a trois chemins habituels. Le premier, c'est le préfixe public des variables d'environnement : dans Next.js, tout ce qui commence par NEXT_PUBLIC_ est intégré au JavaScript côté client, comme VITE_ dans Vite, NUXT_PUBLIC_ dans Nuxt ou REACT_APP_ dans Create React App. L'IA voit que « la variable n'arrive pas » et lui ajoute le préfixe pour qu'elle arrive. Le deuxième, c'est l'appel direct : l'assistant écrit fetch('https://api.openai.com/...') dans un composant d'interface avec la clé dans l'en-tête, parce que c'est l'exemple le plus court qu'il connaisse. Le troisième, c'est un fichier de constantes partagé, importé à la fois par le serveur et par le client, où quelqu'un a laissé la clé « temporairement ».
Dans les trois cas, le résultat est le même : la clé voyage dans un fichier .js que n'importe qui peut télécharger, et elle reste dans le cache des CDN, dans l'historique du navigateur de tes utilisateurs et dans les archives des robots qui parcourent internet à la recherche de précisément ça.
Comment un attaquant la voit en 60 secondes
- DevTools. Ouvre ton site, appuie sur F12, va dans l'onglet Sources, appuie sur Ctrl+Maj+F pour chercher dans tous les fichiers et tape
sk_live,sk-ant,AKIA,service_roleou-----BEGIN. Si ça apparaît, il l'a déjà. - Code source. Clic droit, afficher le code source, et ouvrir chaque fichier
.jslié. Les bundles sont minifiés mais pas chiffrés ; une recherche de texte suffit. - Source maps. Si tu publies les
.map, en plus du code il voit les noms d'origine des variables et les commentaires, y compris les// TODO: déplacer côté serveur. - Scanners automatiques. Des bots parcourent les nouveaux domaines en téléchargeant leur JavaScript et en testant chaque motif de clé auprès du fournisseur correspondant. Une clé AWS qui fuite dans un bundle est utilisée en quelques minutes, pas en quelques jours.
Quelles clés sont dangereuses et ce qui se passe si elles fuitent
- Stripe
sk_live_. Permet de créer des paiements, des remboursements et de lire les données des clients. C'est de l'argent réel. - OpenAI, Anthropic et similaires (
sk-…). Celui qui la détient consomme sur ton compte. Les factures de plusieurs milliers d'euros pour une clé dans un dépôt public sont une histoire qui se répète. - AWS
AKIA…et Google Cloud. Selon les permissions : accès aux buckets, aux bases de données et création de machines pour miner à tes frais. - Supabase
service_role. Contourne entièrement le Row Level Security : lecture et écriture totales sur ta base de données. La plus grave de toutes dans les apps vibe-codées. - Comptes de service Firebase (JSON). Équivalent du
service_role: contrôle total du projet. - Fournisseurs d'email et de SMS (Resend, SendGrid, Twilio). Envoyer des mails et des messages en ton nom, autrement dit du phishing avec ton domaine.
- Chaînes de connexion à la base de données avec utilisateur et mot de passe, et secrets JWT : avec le secret, on signe des sessions pour n'importe quel utilisateur.
- Tokens d'accès stockés dans
localStorage. Ce ne sont pas tes clés mais celles de tes utilisateurs, et n'importe quel XSS les lit et les vole.
Quelles clés peuvent aller côté client
Toute clé n'est pas un secret. Certaines sont conçues pour vivre dans le navigateur, à condition que ce qui se trouve derrière soit bien configuré :
- La clé
anonoupublishablede Supabase, protégée par les politiques RLS de chaque table. - La clé publique de Stripe (
pk_live_), qui ne sert qu'à démarrer des paiements, pas à les encaisser. - La configuration Firebase (apiKey, projectId…), protégée par les règles de sécurité et, mieux, par App Check.
- Une clé Google Maps restreinte par domaine depuis la console Google.
- Les identifiants d'analytics et de widgets tiers.
La règle pour les distinguer : une clé peut aller côté client si le fournisseur la qualifie explicitement de publique et si, même en la détenant, un inconnu ne peut rien faire que tu ne veuilles pas. En cas de doute, elle est secrète.
Comment les chercher toi-même
Dans le bundle déjà construit, cherche les motifs connus. Avec Next.js ou Vite, après le build :
grep -rEo "sk_live_[0-9a-zA-Z]+|sk-ant-[0-9a-zA-Z-]+|AKIA[0-9A-Z]{16}|service_role|eyJhbGciOi[0-9a-zA-Z._-]+" .next/static dist 2>/dev/null | head
Dans le dépôt, avant qu'elle n'atteigne le bundle, des outils comme gitleaks ou trufflehog parcourent tout l'historique, pas seulement le dernier commit. Et dans le panneau de ton hébergeur, vérifie quelles variables portent un préfixe public : chacune d'elles est envoyée au navigateur.
Trois endroits où les gens ne regardent pas : les composants marqués 'use client' qui importent un fichier de configuration partagé ; les paramètres de l'URL, où voyage parfois un token qui finit dans les logs et l'historique ; et les dépôts publics sur GitHub, où la clé peut traîner depuis des mois même si elle n'est plus dans le code actuel.
Comment corriger : la clé reste sur le serveur
Le bon schéma, c'est que le navigateur parle à ton serveur et que ton serveur parle au fournisseur. Un endpoint à toi reçoit la requête, vérifie la session de l'utilisateur, applique une limite d'usage et fait l'appel avec la clé lue depuis une variable d'environnement sans préfixe public. Dans Next.js :
// app/api/summarize/route.ts (serveur uniquement)
import { getSession } from '@/lib/auth'
export async function POST(req: Request) {
const session = await getSession(req)
if (!session) return new Response('Unauthorized', { status: 401 })
const { text } = await req.json()
if (typeof text !== 'string' || text.length > 5000) {
return new Response('Bad request', { status: 400 })
}
const r = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: text }] }),
})
return new Response(r.body, { headers: { 'Content-Type': 'application/json' } })
}
Avec Supabase, même idée : les opérations qui ont besoin de service_role vont dans une Edge Function ou dans ton backend, jamais côté client. Avec Firebase, dans une Cloud Function. Et dans tous les cas, la variable d'environnement s'appelle OPENAI_API_KEY, pas NEXT_PUBLIC_OPENAI_API_KEY.
Pour les tokens de session, préfère les cookies HttpOnly à localStorage : un cookie HttpOnly n'est pas lisible depuis JavaScript, donc un XSS ne peut pas le voler. Si ta bibliothèque stocke la session dans localStorage par défaut, comme le fait le client Supabase dans le navigateur, compense avec une CSP stricte ou utilise sa variante serveur avec cookies.
Si elle a déjà fuité : faire tourner, pas supprimer
Retirer la clé du code ne sert à rien : elle est déjà dans des caches, dans l'historique git et, probablement, entre les mains de quelqu'un. Le bon ordre :
- Créer une nouvelle clé chez le fournisseur et la déployer sur le serveur.
- Révoquer l'ancienne immédiatement après. Chez Stripe, OpenAI et AWS, c'est un clic dans le tableau de bord.
- Vérifier l'utilisation : facturation, logs d'API et paiements des derniers jours. En cas d'abus, ouvre un ticket chez le fournisseur ; il arrive qu'ils annulent les frais.
- Mettre des plafonds de dépenses et des alertes chez chaque fournisseur qui le permet, pour que la prochaine fois fasse moins mal.
- Si la clé donnait accès à des données personnelles d'utilisateurs, évalue si tu dois le notifier au titre du RGPD.
Nettoyer l'historique git avec des outils comme BFG ou git filter-repo, c'est de la bonne hygiène, mais ça ne remplace pas la rotation.
Comment Scanaris le vérifie
Scanaris télécharge le JavaScript que sert ton site, comme le ferait un attaquant, et cherche les motifs de clés des fournisseurs habituels, les tokens JWT, les URL de webhooks et les chaînes de connexion. Il te prévient aussi si tes source maps sont accessibles et si des fichiers sensibles sont à portée. Chaque résultat vient avec un prompt à coller dans ton IA avec la correction exacte : déplacer l'appel côté serveur et faire tourner la clé. Si tu actives la surveillance, il te prévient quand une nouvelle session de vibe coding remet une clé côté client. Ça fait partie de la checklist de lancement et du guide de sécurité des apps créées par IA.
Questions fréquentes
La clé anon de Supabase est-elle un secret ?
Non. Elle est faite pour le navigateur et c'est le RLS de chaque table qui la protège. Ce qui est secret, c'est service_role. Si ton RLS est mauvais, le problème n'est pas la clé anon mais les politiques.
Et si la clé est restreinte par domaine ?
Les restrictions par referer (comme pour Google Maps) réduisent les abus depuis d'autres sites web, mais pas depuis des scripts qui falsifient l'en-tête. Elles conviennent aux clés à faible risque, pas aux clés qui dépensent de l'argent ou lisent des données.
Suffit-il de la supprimer du dépôt ?
Non. Une clé qui a été dans un bundle public ou un commit public est compromise. Fais-la toujours tourner, puis nettoie l'historique si tu veux.