Aller au contenu
LUINLancer un projet

Playbook

Durcir une appli vibe-codée sans perdre la vitesse

La démo fonctionnait. C’est le problème. Comment le code généré par IA casse face aux utilisateurs, attaquants et factures, et dans quel ordre le durcir.

Par LUINIngénierie IA · Sécurité

La liste des factures s’est affichée du premier coup. Vous avez envoyé le lien. Jeudi, un client avait les factures de toutes les autres organisations, parce que la route prenait orgId dans la query string et personne n’avait demandé qui avait le droit d’en envoyer une. Voilà la forme du problème. Le produit fonctionne. « Fonctionne » a été mesuré contre une démo : un utilisateur, une organisation, un happy path, pas d’attaquant, pas de facture, pas de trois heures du matin. On appelle cela le vibe coding. Vous vous asseyez avec un modèle, vous décrivez l’écran suivant, vous acceptez le fichier et vous passez à la suite. La boucle est une conversation, pas une revue. C’est une bonne façon de savoir si le produit devrait exister. C’est une mauvaise façon de décider s’il mérite qu’on lui fasse confiance. Le code n’est pas l’erreur. C’est de le livrer comme si quelqu’un l’avait lu.

Les pannes sont précises

Le code généré par IA ne casse pas au hasard. Il casse là où la démo n’est jamais allée.

Des secrets qui voyagent

Le modèle a besoin que le programme tourne. Le chemin le plus court est une clé dans le fichier, ou une variable d’environnement qui n’est pas secrète parce qu’elle est préfixée pour le navigateur.

const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY!);

NEXT_PUBLIC_ n’est pas un choix de style. Tout ce qui porte ce préfixe part dans le bundle client. Le modèle a vu « variable d’environnement » et s’est arrêté. Renouvelez la clé. Puis cherchez dans l’historique, parce que supprimer la ligne ne supprime pas le commit. La même forme apparaît avec un .env commité, un JSON de compte de service laissé dans /secrets « juste pour le local », et une clé d’API collée dans un prompt qui s’est ensuite imprimée toute seule dans un toast d’erreur.

L’autorisation sur le happy path

export async function GET(req: Request) {
  const session = await getSession(req);
  if (!session) {
    return new Response("Unauthorized", { status: 401 });
  }
  const orgId = new URL(req.url).searchParams.get("orgId");
  const invoices = await db.invoice.findMany({ where: { orgId } });
  return Response.json(invoices);
}

C’est correct si vous faites confiance à la query string. Vous ne devriez pas. Le contrôle de session est passé. La ligne est tout de même venue de l’orgId envoyé par l’appelant. L’authentification répond qui vous êtes. L’autorisation répond ce que vous avez le droit de toucher. L’IA est remarquablement bonne à l’authentification et médiocre à l’autorisation. Parcourez chaque route qui lit ou écrit une ligne. Si le seul contrôle est « une session existe », cette route est publique pour tout utilisateur connecté. Une référence directe à un objet sans contrôle de propriété n’est pas une attaque élégante. C’est le comportement par défaut.

Des erreurs qui ressemblent à un succès

try {
  await chargeCustomer(order);
} catch (err) {
  console.log(err);
  return { ok: true };
}

L’interface affiche une confirmation. La finance, non. Le modèle a appris qu’une exception non gérée fait paraître la démo cassée, alors il a appris à tout attraper et à continuer. La production demande l’inverse : échouer à découvert, garder l’identifiant, refuser de mentir.

Des migrations qui ne tiennent que sur un laptop

ALTER TABLE invoices ADD COLUMN amount_cents integer;
UPDATE invoices SET amount_cents = amount * 100;
ALTER TABLE invoices DROP COLUMN amount;

Sur un laptop, c’est instantané. Sur une grande table de production, le UPDATE prend un verrou, le DROP efface la seule colonne sur laquelle un rollback était encore possible, et le déploiement est déjà passé. Élargissez d’abord, recréez les valeurs par lots, basculez les lectures, supprimez plus tard. Une instruction qui fait les trois est une démonstration de courage, pas une migration. Le reste de la liste est plus discret et tout aussi coûteux. Une requête N+1 acceptable à dix lignes et hostile à dix mille ; une boucle sans borne parce que le jeu de données en avait trois ; des tests générés pour affirmer que la fonction renvoie ce que la fonction renvoie. Des plages de dépendances sans pin et sans revue, c’est ainsi qu’un paquet transitif devient l’incident. Pas d’identifiant de requête dans les logs, un déploiement qui se résume à « merge to main », et une question de licence que personne n’a posée à un paquet désormais assis sur le chemin de paiement. Rien de tout cela n’est exotique. C’est ce que l’on obtient quand le test d’acceptation est « ça s’affiche ».

Le durcissement est un ordre, pas une question d’humeur

Une réécriture donne une impression de propreté parce qu’elle cache chaque décision non prise. Elle cache aussi le seul chemin qui encaisse déjà de l’argent. Gardez le produit. Changez le contrat qu’il a avec le monde. L’ordre compte parce que chaque étape produit l’information dont la suivante a besoin. L’observabilité avant un refactor, sinon vous polissez la mauvaise fonction. Les tests après avoir vu à quoi ressemble un incident, sinon vous achetez de la couverture et vous ratez le défaut. La frontière de sécurité avant le ménage visuel, sinon vous livrez un trou plus soigné.

Sept étapes numérotées sur une ligne ; une flèche de chacune vers la suivante.
L’ordre du durcissement. Chaque étape produit ce dont la suivante a besoin.
  1. Inventoriez ce qui existe

    Listez les surfaces qui tournent : routes, jobs, files d’attente, webhooks, pages d’administration, scripts qui ont encore des identifiants de production, cette Cloud Function dont plus personne ne se souvient. Notez où vit l’état et qui peut l’atteindre. Une recherche dans le dépôt sur process.env, sk_live, BEGIN PRIVATE KEY et TODO est un début, pas une carte. On ne durcit pas un système que l’on ne sait pas nommer.

  2. Trouvez les trois choses qui peuvent vous coûter

    Pas trente constats. Trois. Ce qui fait perdre des données, de l’argent ou de la confiance s’il est faux cette semaine : le chemin d’encaissement, l’export, l’endpoint qui prend un identifiant côté client. Le reste attend. Une longue liste, c’est le durcissement qui redevient une réécriture sous un autre nom.

  3. Corrigez d’abord la frontière de sécurité

    Placez l’autorisation à côté des données, pas dans le composant qui se trouve les afficher. Déduisez le tenant de la session, pas de la query string. Sortez les secrets du bundle et de git. Renouvelez tout ce qui a déjà été commité. Fermez la route d’administration que le modèle a laissée sur la même origine sans contrôle supplémentaire. Faites cela avant de renommer des dossiers. Une base de code rangée avec une liste de factures ouverte reste une liste de factures ouverte.

  4. Ajoutez l’observabilité avant d’améliorer quoi que ce soit

    Il vous faut un identifiant de requête, des logs structurés et une erreur qui arrive à une personne. Il vous faut savoir si l’encaissement a tourné, pas si la page s’est chargée. Ajoutez la sonde qui vous aurait dit la fuite de jeudi : qui a demandé quel orgId, et s’il correspondait à la session. Si vous ne voyez pas une panne, vous passerez la semaine suivante à « améliorer » une fonction qui n’était pas le problème.

  5. Écrivez les tests qui auraient vu l’incident

    La couverture est une métrique de vanité quand la suite affirme que le code fait ce que le code fait. Écrivez le cas qui aurait échoué jeudi : l’utilisateur A demande les factures de l’utilisateur B et se voit refuser. Écrivez le cas où chargeCustomer lève une exception et où la réponse n’est pas { ok: true }. Un test qui nomme une vraie panne vaut mieux qu’un fichier généré qui n’en nomme aucune.

  6. Rendez les déploiements réversibles

    Une migration au démarrage qui ne sait qu’avancer n’est pas un processus de livraison. Il vous faut un déploiement que vous pouvez annuler, une migration que vous pouvez étendre ou annuler, et un flag qui coupe un nouveau chemin sans reconstruction. Si le seul rollback est « git revert et espérons que le schéma soit d’accord », vous n’avez pas de rollback. Vous avez un vœu.

  7. Ensuite, et seulement ensuite, refactorez

    Alors seulement vous pouvez renommer le module, couper le fichier que le modèle a laissé à neuf cents lignes et supprimer le client inutilisé créé par le scaffold. Faites-le derrière les tests et les logs que vous venez d’ajouter. Refactorer d’abord, c’est la façon dont une équipe passe un mois à rendre la fuite plus élégante.

Traitez chaque patch du modèle comme la modification d’un prestataire que vous n’avez pas rencontré : lisez le diff, exécutez le chemin qui peut faire perdre de l’argent, et refusez tout ce dont l’histoire d’autorisation est « l’interface ne montre pas ce bouton ».

Ce qu’il faut garder

La vitesse était l’enjeu. Ne punissez pas la boucle qui vous a donné un produit. Changez ce que « terminé » veut dire. Terminé, ce n’est pas « la page s’est affichée pour moi ». Terminé, c’est : la route refuse le mauvais appelant, la panne est visible, le déploiement revient, et le test qui aurait vu jeudi existe avant la fonctionnalité de vendredi. Le modèle peut encore écrire le premier jet de la route. Une personne décide encore si cette route a le droit d’exister. Une équipe qui fait cela bien ne paraît pas plus lente l’après-midi. Elle paraît ennuyeuse. Quelqu’un colle un prompt. Quelqu’un lit le diff. Les secrets restent dans un gestionnaire, pas dans le bundle client. L’autorisation est une barrière dans le handler, pas un commentaire dans le ticket. Les logs portent un identifiant de requête. Le déploiement a un bouton qui l’annule. Quand le modèle invente une dépendance, une personne ouvre la page du registre avant que le lockfile ne change. Ils livrent encore un mardi. Ils n’apprennent pas le jeudi que orgId était une suggestion. L’endpoint des factures peut rester. Il ne croit plus la query string.

Pratique associée

Ingénierie IA

Cela vous a été utile ?

Demander un briefingObtenir une estimation