Comment fonctionne cette vérification
Scanaris charge ta page d'accueil, rassemble les fichiers JavaScript qu'elle lie depuis ton propre domaine et les lit à la recherche de motifs d'identifiants connus : clés secrètes et restreintes de Stripe (sk_live_, rk_live_), identifiants d'accès AWS (AKIA…), clés d'API Google (AIza…), clés OpenAI, jetons GitHub (ghp_…) et clés privées au format PEM. Il décode aussi tout JWT qu'il trouve et, si sa charge contient role: service_role, le signale comme clé de service Supabase exposée, ce qui est critique. Les clés publiques (pk_live_ de Stripe, l'anon ou la publishable de Supabase, la configuration Firebase) sont exclues volontairement : elles sont conçues pour aller dans le navigateur.
Ce qu'un attaquant fait avec ça
Exactement ce que fait le scanner, mais à grande échelle : des robots téléchargent le JavaScript des nouveaux domaines dès qu'ils apparaissent dans les journaux de certificats et testent chaque clé trouvée. Une clé secrète Stripe permet de créer des remboursements ou de lire les clients ; une clé OpenAI, de dépenser ton quota ; une service_role Supabase, de lire et supprimer toute ta base en contournant les policies. Rien à « pirater » : le fichier est public par définition, chaque visiteur le reçoit.
Comment une clé finit dans le bundle
Presque toujours via une variable d'environnement avec le préfixe public : NEXT_PUBLIC_ en Next.js, VITE_ en Vite, NUXT_PUBLIC_ en Nuxt. Le framework incruste ces variables dans le JavaScript au moment du build, ce qui est exactement ce que tu veux pour une clé publique et exactement ce que tu ne veux pas pour une clé secrète. L'autre chemin habituel, c'est de coller la clé dans le chat de Lovable, Bolt ou Cursor pour « connecter » un service : le modèle l'écrit dans le composant qui en a besoin. Plus de détails dans les secrets que ton appli laisse fuiter dans le navigateur.
Comment corriger
- Fais tourner la clé d'abord. Considère-la compromise même si tu la retires du code : les bundles précédents sont encore dans les caches et dans l'historique des déploiements. Génère-en une nouvelle dans le tableau de bord du service et révoque l'ancienne.
- Déplace l'appel côté serveur. En Next.js, vers un Route Handler ou une Server Action ; en Nuxt, vers une route
server/api; avec Supabase, vers une Edge Function ; sur Netlify, vers une fonction. Le navigateur appelle ton serveur, et le serveur, avec la clé, appelle le service. - Retire le préfixe public de la variable et stocke-la dans le tableau de bord de ton hébergeur comme variable serveur. Si une variable a besoin du préfixe, c'est qu'elle est publique : demande-toi si elle devrait l'être.
- Vérifie l'historique git. Si la clé a un jour été poussée dans un dépôt, elle est dans l'historique même si le fichier actuel est propre.
Vérifie que c'est propre
curl -s https://tondomaine.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -5
curl -s https://tondomaine.com/assets/index-XXXX.js | grep -oE "sk_live_[A-Za-z0-9]+|rk_live_[A-Za-z0-9]+|AKIA[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{35}|ghp_[A-Za-z0-9]{36}|BEGIN (RSA |EC )?PRIVATE KEY" | head
La première commande liste tes scripts ; la seconde cherche les motifs dans l'un d'eux. Si rien ne sort dans aucun, le bundle est propre. Répète après chaque gros déploiement, parce qu'une régénération par IA peut remettre la clé.
Questions fréquentes
La clé anon de Supabase ou la pk_live de Stripe sont-elles un problème ?
Non. Elles sont publiques par conception et le scanner ne les signale pas. Ce qui protège tes données avec l'anon, c'est le RLS ; ce qui protège Stripe avec la pk_live, c'est qu'elle ne sert qu'à créer des jetons de paiement, pas à encaisser.
Il signale une clé Google que j'utilise pour Maps. C'est grave ?
Les clés AIza peuvent légitimement être publiques si elles sont restreintes par domaine et par API dans la console Google Cloud. Sans restrictions, n'importe qui peut dépenser ton quota. Restreins-la et le risque réel disparaît, même si le motif apparaît toujours.
Je l'ai déjà retirée du code et elle apparaît encore.
Le bundle déployé est peut-être l'ancien, ou la clé est dans un autre fichier (un chunk partagé, un script tiers hébergé sur ton domaine). Redéploie, rescanne et, dans tous les cas, fais tourner la clé.