Faille Vercel : J'ai Fait Tourner 6 Secrets. Une Commande d'Audit en a Divulgué 18 de Plus.
Je vois VERCEL piraté qui circule sur les réseaux. Puis ce matin, un email de VERCEL dans ma boîte. Bon, je n'avais prévu aucune maintenance aujourd'hui. Apparemment il y a une urgence.
J'ouvre le tableau de bord Vercel d'un projet qui tourne en prod. Six badges orange sur mes variables d'environnement. Need To Rotate. Clés JWT, secrets OAuth, tokens MCP. Arghh.
TLDR : une faille expose un secret. Votre réaction à la faille expose tous les autres. Les commandes d'audit par défaut que vous lancez dans la panique post-incident affichent les valeurs en clair par conception, et vos secrets vivent dans plus d'endroits que vous ne le pensez. Combien exactement ? C'est ça la vraie question.

Avant les badges, il y avait un cheat Roblox.
Il y a vingt-deux mois, un employé de Context.ai, un outil IA dont personne en dehors de leur équipe ne se souciait, a téléchargé un binaire depuis un lien douteux sur son laptop de travail. Il cherchait un moyen de casser un niveau Roblox. Le binaire était Lumma Stealer. Il a récupéré leurs sessions navigateur, leurs tokens OAuth sauvegardés, leur accès Google Workspace. Puis plus rien.
Vingt-deux mois de silence. Aucune alerte. Aucune anomalie. Aucune raison de regarder.
Puis début avril, l'attaquant utilise cet accès OAuth pour entrer dans les comptes Google Workspace des clients de Context.ai. Y compris un employé Vercel. Y compris, via l'accès de cet employé, les systèmes internes de Vercel. Ils trouvent les variables d'environnement que les clients avaient marquées non-sensibles, ce qui signifiait que Vercel les stockait non-chiffrées au repos, ce qui était le défaut. Ils copient. Ils mettent le dump en vente sur BreachForums. Prix demandé : deux millions de dollars.
Dimanche matin mon téléphone vibre avec l'email d'alerte.
Maintenant les badges orange.
Dimanche Matin, 6 Badges Orange

Les badges ne sont pas une supposition. Ils sont basés sur les logs d'accès de Vercel pendant la fenêtre d'incident. Chacun signifie qu'une variable spécifique a été lue par l'app OAuth compromise pendant la faille. Ce qui signifie que de l'autre côté, dans ce dump BreachForums, ces valeurs traînent en clair.
Six badges sur un projet : JWT_PRIVATE_KEY_JWK, OAUTH_SECRET, OAUTH_CLIENT_ID, DASHBOARD_PASSWORD, MCP_AUTH_TOKEN, CAROUSEL_RENDER_SECRET. J'ai neuf projets. Le décompte réaliste se situe entre 25 et 40 variables une fois qu'on prend en compte les apps en aval qui cachent ces tokens.
Et mes secrets ne vivent pas que sur Vercel.
Trente minutes plus tard je termine le premier projet. Méthodique, content de moi. Au lieu de m'arrêter, je décide d'auditer tout le reste.
C'est là que ça devient intéressant.
La Commande Qui A Fuité Plus Que La Faille
Pour mapper les secrets sur mon instance Convex auto-hébergée, je tape la commande la plus naturelle au monde.
bunx convex env list
Je m'attends à des noms. Comme aws iam list-users, gh secret list, vercel env ls. Ceux-là retournent tous des noms. Juste des noms. Les valeurs restent cachées derrière une seconde commande explicite.
Mon terminal se remplit. PAT GitHub avec scope d'écriture prod. JWT admin Beehiiv. Clé Fal.ai. Clé OpenRouter. Clé YouTube Data API. Clé RapidAPI. Douze autres en dessous. Toutes les valeurs. Toutes en clair. Aucun flag à passer. Aucun bandeau d'avertissement. Juste le dump.
Quatre secondes de regard fixe. Puis je ferme les yeux.
Triple persistance. Historique bash. Scrollback terminal. La transcription Claude Code que j'avais ouverte pendant l'audit, parce que bien sûr j'en avais une ouverte, je travaillais.
Dix-huit secrets, sans rapport avec Vercel, maintenant dans trois endroits que je ne contrôle pas complètement. Ma machine n'est pas compromise. La probabilité d'usage malveillant est faible. Mais faible probabilité, gros rayon d'impact c'est le calcul qui nous a mis dans ce pétrin.
Quatre heures.
Vos Secrets Vivent Dans Plus D'Endroits Que Vous Ne Le Pensez

Comptez-les. Vraiment comptez-les.
Un seul secret dans une stack moderne s'étale sur quatre stores, pas un. Vous ne vous en rendez compte que le jour où vous devez faire la rotation, et là c'est trop tard.
Le runtime d'hébergement arrive en premier. Vercel, Netlify, Railway, Render, Fly.io. C'est le store que la faille a touché.
Si vous avez construit quelque chose de non-trivial, vous avez aussi un backend auto-hébergé. Convex auto-hébergé, un VPS custom, une machine Fly qui fait tourner la couche app. Deuxième copie, déployée la semaine dernière, probablement légèrement désynchronisée avec la première.
Puis le coffre-fort externe. Infisical, 1Password, Doppler, HashiCorp Vault. Si vous êtes assez discipliné pour en utiliser un. Supposément la source de vérité. Ça ne l'est jamais vraiment.
Et .env.local sur la machine de dev. L'équivalent numérique d'un Post-It sous votre clavier. Toujours là. Même quand vous juriez l'avoir nettoyé vendredi dernier.
Vous vous dites que le coffre-fort est la source de vérité. En réalité, Vercel a sa propre copie synchronisée le mois dernier, votre instance Convex a une autre copie de la semaine dernière, et votre .env.local est un snapshot d'il y a trois mois avec deux valeurs rotées encore en clair en bas.
Une faille chez un fournisseur expose un store. D'accord. C'est le rayon d'impact pour lequel vous avez signé.
Les commandes d'audit que vous lancez dans les trente minutes suivantes, paniqué et caféiné, exposent les trois autres. C'est le rayon d'impact dont personne ne vous a prévenu.
Règle : traitez l'audit avec le même soin que la rotation. Chaque commande de listing est un release candidate. Chaque commande de listing a un mode d'échec. Chaque commande de listing mérite la vérification de flag que vous sautez.
L'audit, c'est l'incident.
Commandes Qui Dumpent Tout (Et Quoi Lancer À La Place)
bunx convex env list n'est pas un cas isolé. C'est une famille.
Toute CLI qui livre une commande env list ou secrets list doit être présumée coupable jusqu'à ce que la page de manuel prouve le contraire. Lisez le comportement des flags avant de taper. Une fois que les valeurs touchent votre écran, elles sont dans votre historique bash, votre scrollback terminal, et l'outil IA qui lit votre shell en ce moment.
Les coupables que je connais. gh secret list --show-value affiche chaque secret de repo GitHub en clair. aws ssm get-parameter --with-decryption fait l'équivalent AWS. vercel env pull dumpe chaque variable Vercel dans un fichier .env local qui reste sur disque jusqu'à ce que vous pensiez à le supprimer. Et bien sûr bunx convex env list et npx convex env list, tous deux équivalents.
Quoi lancer à la place.
Pour Convex auto-hébergé, puisque env list est un piège :
bunx convex env get STRIPE_SECRET_KEY 2>/dev/null | wc -c
Retourne la longueur en octets, pas la valeur. Existe si > 0, manquant si 0.
Pour Infisical, n'utilisez pas l'UI au-delà de vingt secrets, utilisez l'API :
curl -s "https://app.infisical.com/api/v3/secrets/raw/STRIPE_SECRET_KEY" \
-H "Authorization: Bearer $INFISICAL_TOKEN" \
| jq '.secret.updatedAt'
Retourne un timestamp. N'affiche jamais la valeur.
Pour Vercel, l'UI a le nouveau tableau de bord d'aperçu. Pour la CLI, vercel env ls affiche les noms, timestamps, environnements. Pas de valeurs. Évitez vercel env pull jusqu'à ce que la phase d'audit soit terminée.
Pour .env.local, grepez le nom, ne catez pas le fichier :
grep -l "STRIPE_SECRET_KEY" .env.local
Pour GitHub, gh secret list seul (sans --show-value) retourne les noms. Suffisant pour un audit.
Construisez une petite matrice texte au fur et à mesure. Pour chaque secret, notez quels stores le détiennent. Quelque chose comme STRIPE_SECRET_KEY = Vercel + Convex + Infisical + .env.local. C'est votre plan de rotation. Vous savez maintenant exactement combien d'endroits doivent être re-synchronisés, dans quel ordre, et quelles apps en aval ont besoin de tokens frais.
La page de manuel coûte moins cher que la rotation.
Le Playbook
L'incident a produit trois règles, pas trois ensembles de règles. Elles sont agnostiques de stack. Elles marchent que vous soyez sur Vercel, Netlify, Railway, ou une machine Fly que personne d'autre dans l'entreprise ne se rappelle exister.
Mapper. Réduire. Faire la rotation.
Mapper
La première règle c'est la cartographie. Chaque secret dans chaque projet a une source de vérité et N caches, et la carte de où vivent ces caches appartient au repo, pas dans votre tête.
Une seule source de vérité par secret. Infisical, 1Password, Doppler, peu importe ce que vous avez choisi. Tous les autres stores (Vercel, Convex, .env.local) sont un cache de cette source. Synchronisation unidirectionnelle où votre plan le supporte, documentez le chemin de sync où ça ne marche pas.
Un CLAUDE.md ou SECURITY.md dans chaque repo, avec quatre choses : les secrets que le repo utilise, où chacun vit à travers les quatre stores, les tableaux de bord fournisseur pour les faire tourner, et les commandes que personne n'a le droit de lancer contre ce repo. Écrit noir sur blanc. Pour que la version de vous à 23h paniquant sur une nouvelle faille ne réinvente pas la carte dans le noir.
Le flag Sensitive au moment de la création, jamais après. Vercel a livré ça dans une réponse de panique et j'ai vu la moitié d'internet mal comprendre. Basculer Sensitive sur une variable déjà compromise ne rembobine rien. La valeur est sortie. Ça ne marche que prospectivement, ce qui signifie que la discipline est à la création, pas après coup.
Zéro .env committé. .gitignore systématique. .env.example avec des placeholders vides seulement. Ancienne règle, encore violée chaque semaine quelque part dans votre org, comptez dessus.
Réduire
La deuxième règle c'est la réduction de scope. La clé qui peut faire le moins est la clé qui vaut le moins la peine d'être volée, et la clé qui peut faire le plus est la clé que l'attaquant veut vraiment.
Clés scopées partout où le fournisseur les supporte. Clés Stripe restreintes scopées par fonctionnalité, pas la clé secrète générique qui peut rembourser, supprimer des clients, et émettre des payouts. Personal Access Tokens GitHub fine-grained, un par cas d'usage, un repo, un scope. Clerk avec des clés distinctes par environnement, pour qu'une clé dev fuité reste en dev. Supabase avec des politiques row-level-security et la clé anon pour tout côté client, service_role restreinte à un job côté serveur étroit. Convex avec une clé de déploiement read-only pour les builds, clé admin pour les scripts admin explicites seulement.
Plafonds de dépense durs sur chaque fournisseur facturé à la consommation. OpenAI, Anthropic, OpenRouter, Fal, Replicate. Une clé compromise qui génère à 500$ l'heure c'est la différence entre un mauvais après-midi et un mauvais trimestre. Ou entre un mauvais trimestre et un post Hacker News avec votre nom dans le titre.
MFA matériel sur les comptes pivot. Google Workspace, registrar de domaine, le runtime d'hébergement, l'hébergeur de code. YubiKey ou passkey. Pas SMS. Ces quatre comptes sont ceux qui déverrouillent tout le reste s'ils sont compromis, et ce sont exactement ceux pour lesquels les outils style Context.ai demandent des accès OAuth larges.
Faire la rotation
La troisième règle c'est la discipline de rotation. Si vous ne faites la rotation qu'après les failles, vous ne la faites que pendant la panique. Et la panique c'est quand les commandes d'audit fuient des choses.
Une cadence. Tous les 90 jours pour les clés critiques : auth, paiements, LLM haut volume. Annuel pour le reste. Proactif bat réactif à chaque fois.
Une procédure. Générez la nouvelle valeur chez le fournisseur. Gardez l'ancienne active si dual-key est supporté, sinon gardez la fenêtre de swap courte. Mettez à jour les quatre stores dans l'ordre, de la source de vérité vers l'extérieur. Test de fumée end-to-end, pas juste le build. Révoquez l'ancienne valeur seulement après validation. Marquez Sensitive au moment de la rotation, même écran, même workflow, jamais dans un passage séparé.
Une règle d'hygiène. Une session shell par projet pendant la rotation. Terminal frais, conversation Claude Code fraîche, pas de pollution cross-projet. Si l'historique d'une session est compromis plus tard, le rayon d'impact s'arrête à un projet.
Un balayage OAuth trimestriel. GitHub, Workspace, le runtime d'hébergement, Linear, Notion. Tout ce qui n'est pas utilisé en 90 jours est révoqué. Pas de sentimentalisme. J'ai reconstruit ma stack from scratch avant, quand une plateforme m'a tiré le tapis sous les pieds. Ça prend un weekend, pas une carrière. Le coût de garder un accès OAuth périmé est toujours plus élevé que le coût de reconstruire l'intégration le jour où vous en avez vraiment besoin à nouveau.
Mapper, réduire, faire la rotation. Tout le reste c'est de l'optimisme.
Puis Le Deuxième Email Est Arrivé
Lundi matin. Vingt-quatre heures après le mail Vercel. Notification Gmail. L'application Claude sur votre compte GitHub demande des permissions supplémentaires.
Je cligne des yeux. Même famille de vecteur. Un outil lié à l'IA déjà installé sur mon compte, qui demande des scopes étendus, avec un bouton Allow All et une capture d'écran produit rassurante en dessous. Je clique Review, pas Allow. Les nouveaux scopes incluent l'accès en écriture à tous mes repositories, les privés inclus. Je ferme l'onglet et laisse ça pour plus tard.
Deux emails, 24 heures d'écart, tous deux de la même surface d'attaque.
Le CEO de Vercel Guillermo Rauch a déclaré publiquement que l'attaque avait été significativement accélérée par l'IA. Ce qui colle. Quand votre menace amont est un outil IA qui détient des accès dans Workspace, Supabase, Datadog, et Authkit en même temps, un credential poppé devient un mouvement latéral dans une douzaine d'apps en aval.
Selon le SaaS Security Report 2026 d'AppOmni, cité par le post Medium du 21 avril de Before The Curve, 76% des employés utilisent des apps SaaS non-approuvées, en moyenne 25 apps par personne, et 31% des failles SaaS exploitent maintenant les connexions OAuth ou API. C'est la surface d'attaque principale de 2026.
Le Allow All en un clic est la plus grande surface d'attaque dans le logiciel d'entreprise aujourd'hui, et les outils IA sont les plus gros générateurs de ces prompts en un clic. Chaque agent IA qui s'intègre avec votre stack demande des scopes larges parce que demander des étroits signifie plus de friction de setup, moins de stickiness produit, de pires métriques d'onboarding. Donc ils demandent tout. Vous cliquez oui. Un an et demi plus tard l'outil se fait popper et emmène votre Workspace avec lui.
Quelque part là-bas, Karen de la Compta a vu le même email que moi et a cliqué Allow All sans scroller. Elle n'est pas le problème. Le bouton l'est. Aucune formation sécurité ne réparera une UX qui fait que review prend quatre clics et allow en prend un.
Ce n'est pas un argument bannissez les outils IA. J'utilise Claude Code huit heures par jour. C'est un argument l'hygiène OAuth est maintenant table stakes. Une tactique spécifique qui aide : préférez les outils CLI à scope étroit plutôt que les serveurs MCP OAuth larges quand vous avez le choix. Une CLI avec un token de courte durée sur votre machine a un rayon d'impact beaucoup plus petit qu'un serveur MCP avec un accès OAuth persistant au niveau fournisseur. Pas toujours possible. Quand ça l'est, prenez-le.
La facture arrive à échéance dix-huit mois plus tard.
Ce Que La Faille M'A Vraiment Appris
La faille m'a coûté trente minutes sur ces six badges. L'audit m'a coûté quatre heures et dix-huit rotations de plus. La rédaction que vous venez de lire en a coûté deux autres.
La leçon durable n'est pas la procédure. C'est le pattern.
Il y a vingt-deux mois quelqu'un a cliqué sur un lien de cheat Roblox. Ce clic, routé à travers un accès OAuth avec des scopes expansifs, a fini par devenir mon email d'alerte de dimanche matin et un weekend de rotations. Pas parce que l'attaque était sophistiquée (elle ne l'était pas). Parce que la stack entre ce laptop et mes variables d'environnement était une longue chaîne de clics Allow All, chacun un piège différé, chacun attendant un attaquant patient avec rien de mieux à faire pendant vingt-deux mois.
Chaque commande de listing par défaut dans votre stack est un amplificateur pour ces pièges. Chaque clé non-scopée, chaque secret partagé, chaque .env committé, chaque MFA basé SMS est un jour de plus que le prochain attaquant passe à l'intérieur avant que quelqu'un remarque.
Une faille expose un secret. Votre réponse expose le reste. Sauf si vous avez mappé, scopé, et fait la rotation avant que ça devienne la seule option.
Le prochain Vercel arrive. Si vous serez touché n'est pas la question.
C'est combien de secrets de plus votre audit va fuiter par-dessus 😬
Sources
Bulletin d'incident de sécurité Vercel d'avril 2026 : https://vercel.com/kb/bulletin/vercel-april-2026-security-incident
BleepingComputer sur le listing BreachForums et la citation Rauch : https://www.bleepingcomputer.com/news/security/vercel-confirms-breach-as-hackers-claim-to-be-selling-stolen-data/
Recherche Trend Micro sur l'angle supply chain OAuth et le temps de séjour de 22 mois : https://www.trendmicro.com/en_us/research/26/d/vercel-breach-oauth-supply-chain.html
CyberScoop sur le vecteur d'entrée Lumma Stealer : https://www.cyberscoop.com/vercel-security-breach-third-party-attack-context-ai-lumma-stealer/
Stats du SaaS Security Report 2026 d'AppOmni, citées par l'article Medium du 21 avril de Before The Curve : https://medium.com/@beforethecurve/how-a-roblox-cheat-script-led-to-a-2m-ransom-against-vercel-079707c21f0b
Rédigé avec Claude Code ouvert dans l'onglet suivant (oui, le même onglet qui vient de fuiter 18 secrets dans sa transcription, ce qui est exactement le sujet de cet article). Réécrit, fact-checké, et édité par moi.