Vibe coding et sécurité : j'ai démonté une app générée par IA sans écrire une
ligne d'exploit
Vibe coding : le front React généré par IA était blindé, le backend grand ouvert. Clé d'API exposée dans le bundle, aucune Row Level Security, fonctions RPC sans autorisation. Récit d'une compromission totale par l'API, et pourquoi l'IA code toujours pareil.
own2pwn··13 min de lecture
Un samedi soir, une idée, un prompt. Trois quarts d'heure plus tard, une plateforme complète tourne en ligne : comptes utilisateurs, connexion, classement, tableau de bord. Zéro ligne de code écrite à la main. C'est ça, le vibe coding : vous décrivez, l'IA construit, ça marche. Le lundi, je l'ai attaquée. En quatre minutes, sans écrire un seul exploit, j'avais tous les secrets de la base, mon score à deux milliards et le pouvoir de bannir n'importe qui. Voici comment, et pourquoi ça arrive à presque toutes les applications générées de cette façon.
Cadre : mes propres applications
Le front tient. On applaudit (trop vite)
Réflexe de pentester : on commence par là où tout le monde tape. Un champ de pseudo, un champ « pays », une bio. On y glisse les classiques du Top 10 des risques web, en visant le XSS stocké :
<script>alert(document.domain)</script>
<img src=x onerror=alert(1337)>
"><svg/onload=alert(document.cookie)>
javascript:alert(1)Rien. Chaque charge s'affiche telle quelle, en texte, sur le profil. On teste le DOM (fragments d'URL, location.hash), on cherche un dangerouslySetInnerHTML avec de la donnée utilisateur, une open redirect, un eval traînant : rien n'accroche. La raison est structurelle et plutôt rassurante : le front est en React, et React échappe automatiquement toute expression rendue dans du JSX. Votre <script> devient <script> avant même d'atteindre le DOM. Pour sortir du contexte HTML, il faudrait que le développeur ait explicitement désactivé cette protection. L'IA ne l'a pas fait. Bon point.
Ce n'est pas un hasard
{username} dans une balise ». Ce qui est visible, testable au clic et omniprésent dans les tutoriels, l'IA le maîtrise. Retenez cette phrase : elle explique aussi la suite.À ce stade, un audit superficiel signerait « RAS côté client » et passerait à autre chose. C'est exactement le piège. La surface qui compte n'est pas celle qu'on regarde.
Puis on ouvre l'onglet Réseau
L'application est un single-page React qui parle à un backend hébergé (le fameux backend-as-a-service : base de données, authentification et API REST auto-générée, le tout clé en main). Le navigateur doit bien s'authentifier auprès de cette API. Donc la clé d'accès est forcément dans le bundle JavaScript. On l'y trouve en dix secondes :
# la clé « anon » et l'URL du projet, en clair dans le JS livré au navigateur
$ curl -s https://cible.exemple.app/assets/index-*.js | grep -oE 'https://[a-z0-9]+\.backend\.co|eyJ[A-Za-z0-9_-]{20,}'
https://xxxxxxxx.backend.co
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...REDACTED...role":"anon"...Ici, une confusion fréquente : cette clé est censée être publique. Elle n'est pas un secret, et l'exposer n'est pas la faille. La clé publique n'est qu'un ticket d'entrée vers l'API : ce qu'elle vous autorise à faire une fois entré dépend entièrement des règles côté serveur. Et c'est là que tout s'effondre.
Pas de Row Level Security : la base répond à tout le monde
Sur ce type de backend, une table n'est protégée que si l'on active la Row Level Security (RLS) et qu'on écrit des policies : des règles SQL qui décident, ligne par ligne, qui a le droit de lire ou d'écrire quoi. Par défaut, sur beaucoup de projets, la RLS est désactivée. Sans elle, la clé publique donne un accès en lecture (et souvent en écriture) à toutes les lignes de toutes les tables exposées. Y compris la table des défis, avec leurs réponses :
# lire la table des challenges, colonne "flag" comprise, avec la clé publique
$ curl -s 'https://xxxxxxxx.backend.co/rest/v1/challenges?select=title,points,flag' \
-H "apikey: eyJ...REDACTED"
[
{"title":"Warmup", "points":50, "flag":"CTF{r3d4ct3d_0001}"},
{"title":"SQLi 101", "points":200, "flag":"CTF{r3d4ct3d_0002}"},
{"title":"Crypto boss","points":500, "flag":"CTF{r3d4ct3d_0003}"}
...
]Les douze réponses de la plateforme, extraites en une requête, sans en résoudre une seule. Ce n'est ni du XSS, ni de l'injection SQL : c'est du contrôle d'accès cassé (broken access control), la première catégorie de l'OWASP Top 10. La donnée n'était protégée par aucune règle. L'IA a créé les tables, les a exposées via l'API, et n'a jamais écrit la moindre policy.
Les fonctions RPC sans videur
On monte d'un cran. L'IA avait généré des fonctions serveur (RPC) pour la logique métier : ajouter des points, bannir un tricheur. Le front les appelle. Mais rien ne vérifie qui les appelle ni pour qui. La signature est en clair dans le JavaScript ; il suffit de la rejouer :
# s'attribuer le score maximal d'un entier 32 bits signé (2 147 483 647)
$ curl -s 'https://xxxxxxxx.backend.co/rest/v1/rpc/add_points_to_user' \
-X POST -H "apikey: eyJ...REDACTED" -H 'content-type: application/json' \
-d '{"p_user_id":"<mon-uuid>","p_points":2147483647}'
# bannir n'importe quel compte : la fonction admin n'exige aucun rôle admin
$ curl -s 'https://xxxxxxxx.backend.co/rest/v1/rpc/admin_ban_user' \
-X POST -H "apikey: eyJ...REDACTED" -H 'content-type: application/json' \
-d '{"p_user_id":"<uuid-du-1er-au-classement>","p_reason":"nope"}'À partir de là, la plateforme m'appartient. Je mets mon score à INT_MAX, je passe tous les concurrents en négatif, je bannis le haut du classement, je réécris les pseudos. Le préfixe admin_ sur la fonction était le seul « contrôle » : un nom, pas une barrière. Une fonction qui s'appelle admin mais que tout anonyme peut exécuter, c'est le résumé parfait du problème.
admin_ban_user exécutable par n'importe qui. Le nom fait office de sécurité.Deuxième cas : un proxy qui n'a jamais demandé la clé
Même famille de faille, autre décor. Un petit service généré pour relayer des appels vers une API de modèle de langage (un LLM). Le genre d'outil qu'on assemble en vingt minutes pour « partager un accès ». Sauf qu'il relaie les requêtes vers l'API coûteuse sans jamais vérifier d'authentification : une clé bidon est acceptée sans broncher.
# aucune clé valide requise : "Bearer peu-importe" passe
$ curl -s https://proxy.exemple.app/v1/messages \
-H 'authorization: Bearer nimportequoi' \
-H 'content-type: application/json' \
-d '{"model":"...","messages":[{"role":"user","content":"coucou"}]}'
{"id":"msg_...","role":"assistant","content":[{"type":"text","text":"Bonjour !"}]}C'est de l'authentification cassée à l'état pur (OWASP A07) : un endpoint qui coûte de l'argent à chaque appel, exposé au monde entier, sans porte. N'importe qui trouvant l'URL consomme le quota (et la facture) du propriétaire. Là encore, l'IA avait bien construit le relais, la fonctionnalité demandée. Personne ne lui avait demandé le videur, alors il n'y en a pas.
Pourquoi l'IA code toujours comme ça
Le fil rouge des deux cas est le même, et il n'a rien d'un accident :
- L'IA optimise ce qui est prouvable au clic. « Fais un classement » produit un classement qui s'affiche. Que n'importe qui puisse le réécrire par l'API ne se voit pas dans le navigateur : rien, dans le prompt ni dans le rendu, ne pousse le modèle à s'en soucier.
- Le défaut non sécurisé passe inaperçu. Activer la RLS, écrire des policies, vérifier un rôle dans une fonction : ce sont des étapes invisibles, souvent désactivées par défaut. L'IA suit le chemin qui marche le plus vite, et le chemin par défaut mène à une base ouverte.
- L'autorisation ne s'apprend pas dans les tutoriels. Les millions d'exemples d'entraînement montrent « comment afficher des données », rarement « comment décider qui a le droit de les modifier ». Le modèle reproduit ce qu'il a le plus vu.
- Le vibe coding retire le seul garde-fou : la relecture. Quand on ne lit pas le code généré (c'est tout le principe), personne ne remarque la policy manquante. La faille naît silencieuse et part en prod telle quelle.
La morale tient en une phrase : l'IA sécurise ce qui se voit, et laisse béant ce qui compte. Le front, visible et testable, est propre. L'autorisation, les secrets, la logique d'accès, tout l'invisible, reste ouvert. Ce n'est pas une question de modèle plus ou moins bon : c'est la structure même de la démarche qui produit ce biais.
Ce qu'un audit aurait vu en une passe
Aucune de ces failles n'est subtile. Un test d'accès basique les lève toutes : rejouer les appels d'API sans session, tenter une écriture sur la ligne d'un autre utilisateur, appeler une fonction admin_* en anonyme. C'est le pain quotidien d'un contrôle d'accès sérieux, et c'est précisément le genre de vérification qu'on automatise mal quand on se fie au seul rendu. Sur la frontière entre ce qu'une machine détecte et ce qu'un humain doit trancher, voyez pentest automatisé vs pentest humain, et pour la manière dont l'IA change le tri du bruit en analyse de code, le comparatif SAST, DAST, IAST à l'heure de l'IA.
Le vibe coding n'est pas le problème ; l'absence de relecture de sécurité, si. Générer vite est une force, à condition qu'une passe de contrôle systématique cherche la policy manquante, la fonction sans rôle, la clé qui autorise trop. C'est exactement ce que fait notre plateforme AppSec pilotée par IA : repérer, dans le code généré, l'endroit précis où l'IA a oublié le videur.
À retenir
- Une app générée par IA a souvent un front solide (React échappe le XSS) et un backend grand ouvert : ne jugez jamais sa sécurité au navigateur.
- La clé publique d'un backend-as-a-service n'est pas un secret ; ce qui protège vos données, c'est la Row Level Security et ses policies, trop souvent désactivées par défaut.
- Une fonction nommée
admin_*mais exécutable par un anonyme n'a de contrôle d'accès que le nom : le broken access control reste la faille n°1. - Un relais d'API sans authentification, c'est votre facture ouverte au monde : l'authentification cassée guette tout service assemblé à la va-vite.
- L'IA optimise le visible ; l'autorisation est invisible. Sans relecture de sécurité, le vibe coding envoie ce biais directement en production.
Vous avez du code généré par IA quelque part en production ? Il y a de bonnes chances que le videur manque à un endroit précis. On sait où regarder : parlez-en avec un humain via la page contact, ou découvrez notre approche de l'AppSec native IA.
Articles liés
appsec
SAST, DAST, IAST : comparatif et ce que l'IA change en 2026
SAST, DAST, IAST : trois familles d'outils, trois manières de chasser les failles applicatives. Définitions, forces, limites, faux positifs, et ce que le SAST IA et le DAST IA changent vraiment en 2026 (sans le bullshit marketing).
appsec
Pentest automatisé vs pentest humain : ce que l'IA change (et ses limites)
Pentest IA, pentest autonome, agent IA de pentest : entre la hype et la réalité 2026. Ce que les agents savent vraiment faire, où ils butent encore, et pourquoi « pentest IA » cache souvent un simple scan avancé.
appsec
Intégrer SAST et DAST dans sa CI/CD : le guide DevSecOps
Où placer le SAST (sur la PR) et le DAST (en staging) dans votre pipeline, comment brancher SARIF sur GitHub Code Scanning, et comment écrire un security gate qui bloque sans noyer les devs sous le bruit. Exemples GitHub Actions et GitLab CI inclus.