Mentalité de sécurité pour le vibe coding avec Cursor

Le vibe coding avec Cursor est rapide et amusant : vous décrivez ce que vous voulez, l'agent écrit le code, et vous gardez le rythme sans vous arrêter pour lire chaque ligne. Le problème est qu'un modèle entraîné à satisfaire des prompts produira volontiers du code qui fonctionne et semble propre tout en portant encore une injection SQL, des clés d'API en dur ou une vérification d'autorisation qui fait confiance au client. Ce guide vous donne une mentalité de sécurité reproductible pour le workflow de vibe coding dans Cursor, de la détection des défaillances que le code généré par IA produit le plus souvent à l'audit de la sortie, l'application de règles, la gestion des secrets et l'exécution d'une liste de contrôle avant déploiement avant que quoi que ce soit ne soit livré.
Qu'est-ce que le vibe coding et pourquoi la sécurité compte
Le vibe coding signifie que vous pilotez avec intention et laissez le modèle remplir les détails d'implémentation. Vous demandez « un point de terminaison de connexion avec un cookie de session » et Cursor écrit la route, l'appel à la base de données et le middleware. La sortie est généralement fonctionnelle, mais fonctionnel n'est pas la même chose que sécurisé. Le modèle optimise pour faire tourner le code, pas pour se défendre contre un attaquant qui lit le même code.
La sécurité compte plus dans ce workflow, pas moins, pour trois raisons :
- Vous révisez moins. Quand le modèle écrit des dizaines de lignes par prompt, l'étape de révision humaine devient le goulot d'étranglement et est sautée.
- Le modèle a appris à partir de code non sécurisé. Les dépôts publics regorgent de SQL concaténé et de stockage de tokens côté client, donc ces motifs apparaissent dans les complétions.
- Le rayon d'impact est large. Le mode Agent peut toucher de nombreux fichiers à la fois, donc un seul motif non sécurisé est dupliqué dans toute la base de code.
La sécurité n'est pas une phase séparée que vous ajoutez à la fin. C'est une habitude constante que vous intégrez à votre façon de prompt, de réviser et de déployer avec Cursor.
L'agent de Cursor peut écrire du code qui compile, passe vos tests et livre quand même une vulnérabilité. Le modèle ne peut pas valider votre risque. Traitez chaque bloc généré comme un brouillon d'un développeur junior rapide mais négligent.
Les vulnérabilités les plus courantes du code généré par IA
Ce sont les modes de défaillance qui apparaissent encore et encore dans le code généré par IA. Apprenez à reconnaître la forme de chacun afin de pouvoir le repérer en survolant plutôt qu'en lisant chaque ligne.
| Vulnérabilité | À quoi cela ressemble dans le code généré | Pourquoi le modèle le produit |
|---|---|---|
| Injection SQL | Requête construite par concaténation de chaînes au lieu de paramètres | La concaténation est plus simple à écrire et se lit « naturellement » |
| XSS | Entrée utilisateur non assainie rendue avec innerHTML ou dangerouslySetInnerHTML | Rendre le HTML directement est le chemin le plus court vers le résultat visuel |
| Secrets / clés d'API en dur | const API_KEY = "sk-live-..." laissé dans le fichier source | Une valeur littérale rend l'extrait autonome et exécutable |
| Authentification / gestion de session défaillante | Tokens stockés dans localStorage, sans expiration, sans validation côté serveur | Le stockage côté client est le plus facile à démontrer dans un navigateur |
| CORS permissif | Access-Control-Allow-Origin: * avec credentials | Le joker est la correction d'une ligne qui arrête l'erreur immédiate |
| Désérialisation non sécurisée | pickle.loads, eval, ou faire confiance à du JSON directement dans des objets | Tout désérialiser est pratique et suppose une entrée de confiance |
| Points de terminaison d'admin exposés | Des routes comme /admin/users sans vérification de rôle | Le modèle implémente la fonctionnalité mais oublie la garde |
| Validation d'entrée manquante | Corps utilisé directement dans la logique sans vérification de schéma | La validation semble redondante quand le chemin heureux fonctionne déjà |
Injection SQL en JavaScript
Voici la paire classique : une requête qui concatène l'entrée utilisateur, et la même requête écrite avec des paramètres positionnels.
// VULNERABLE: user input is concatenated straight into the query
app.get('/user', async (req, res) => {
const sql = "SELECT * FROM users WHERE email = '" + req.query.email + "'";
const rows = await db.query(sql);
res.json(rows);
});
// FIXED: parameterized query, input never becomes part of the SQL string
app.get('/user', async (req, res) => {
const sql = 'SELECT * FROM users WHERE email = $1';
const rows = await db.query(sql, [req.query.email]);
res.json(rows);
});
Vérifications d'autorisation en Python
La version vulnérable fait confiance à un indicateur envoyé par le client. La version corrigée vérifie la session authentifiée sur le serveur.
# VULNERABLE: the client tells the server whether it is an admin
@app.get("/admin/users")
def admin_users(request):
if request.args.get("is_admin") == "true":
return db.list_users()
return {"error": "forbidden"}
# FIXED: the server decides based on the authenticated session
@app.get("/admin/users")
def admin_users(request):
user = request.session.get("user")
if not user or user["role"] != "admin":
abort(403)
return db.list_users()
Stockage de token : localStorage vs cookie httpOnly
Stocker un token de session dans localStorage le rend lisible par n'importe quel script de la page, ce qui transforme un simple bug XSS en prise de contrôle complète du compte.
// VULNERABLE: token readable by any script, no protection against XSS
localStorage.setItem('token', response.token);
// FIXED: server sets an httpOnly cookie that scripts cannot read
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60,
});
Auditer le code généré par IA avec Cursor
Auditer ne signifie pas lire chaque ligne. Cela signifie demander une seconde passe au modèle pour attaquer ce que la première passe a écrit. Cursor est bon à cela lorsque vous lui donnez la bonne personnalité et une cible claire.
Un prompt de persona de révision de sécurité
Réutilisez ce prompt en mode Agent après que l'agent a terminé une fonctionnalité. Il force le modèle à passer de constructeur à attaquant.
You are a security reviewer auditing code a previous agent just wrote.
For every file changed in this feature:
1. List each place that handles user input, authentication, or secrets.
2. For each, describe the concrete attack an unauthenticated user could run.
3. Show the vulnerable line and the fixed line side by side.
4. Rate severity as low, medium, high, or critical with one sentence of reasoning.
Do not rewrite the whole feature. Report findings only.
Demander une modélisation des menaces en mode Agent
Au-delà de la révision du code terminé, vous pouvez demander à Cursor de modéliser les menaces avant d'écrire. Cela fait ressortir des défauts de conception que la révision au niveau des lignes manque.
Before implementing this billing endpoint, do a short threat model:
- Who can call this route and with what privileges?
- What happens if the amount, currency, or user ID is tampered with?
- Which trust boundary does this input cross?
Then implement the endpoint with those mitigations in place.
Utiliser .cursor/rules pour imposer des vérifications
Vous pouvez encoder l'audit lui-même comme une règle afin que l'agent l'exécute automatiquement à chaque changement. Les règles dans .cursor/rules/ sont chargées dans le contexte, ce qui signifie que la liste de contrôle de sécurité est toujours présente lorsque l'agent travaille.
Faites de « lancer une révision de sécurité » un réflexe après toute session d'agent qui touche à l'authentification, au traitement des entrées ou aux secrets. Cela coûte un prompt et attrape les modes de défaillance du tableau ci-dessus avant qu'ils n'atteignent un commit.
Imposer la sécurité avec les règles Cursor
Un fichier de règles de projet garde vos attentes de sécurité devant le modèle sans vous répéter. Créez .cursor/rules/security.mdc et committez-le pour que chaque collaborateur bénéficie des mêmes garde-fous.
---
description: Security requirements for all code in this project
globs: ["**/*.ts", "**/*.tsx", "**/*.py", "**/*.js"]
alwaysApply: true
---
# Security requirements
- Never write SQL by concatenating user input. Use parameterized queries or an ORM.
- Never render user input with innerHTML or dangerouslySetInnerHTML. Escape it or use a safe renderer.
- Never hardcode secrets, API keys, or tokens. Read them from environment variables.
- Never grant administrative access based on a value supplied by the client.
- Always validate request bodies against a schema before use.
- Always set httpOnly, secure, and sameSite on session cookies.
- Never use eval, exec, or pickle.loads on untrusted input.
- When adding a new endpoint, state explicitly which role can call it.
- When touching auth or input handling, add a security-review note in the response.
Avec alwaysApply: true, la règle est injectée dans chaque requête, donc l'agent se corrige lui-même pendant qu'il écrit au lieu que vous ne rattrapiez les problèmes plus tard. Vous pouvez aussi délimiter les règles à des dossiers spécifiques avec globs afin que les règles de sécurité du backend n'encombrent pas le travail frontend.
Gérer les secrets et les variables d'environnement
Les secrets en dur sont la vulnérabilité la plus facile à livrer par accident, car une valeur littérale rend le code exécutable et le modèle aime le code exécutable. Gardez les secrets entièrement hors du code source.
Patrons de variables d'environnement
# .env.example -- commit this, it documents the shape without real values
DATABASE_URL=postgres://user:password@localhost:5432/app
SESSION_SECRET=replace-me
STRIPE_SECRET_KEY=replace-me
# Load locally, never commit the real .env
cp .env.example .env
# .env -- gitignored, contains the real values and stays on your machine
DATABASE_URL=postgres://realuser:realpass@prod-host:5432/app
SESSION_SECRET=a-long-random-value-from-a-secret-manager
STRIPE_SECRET_KEY=sk_live_the_actual_key
Ajoutez toujours .env et les fichiers associés au .gitignore avant votre premier commit, pas après.
# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key
Analyse des secrets avant commit avec gitleaks
Un hook de pré-commit arrête un secret avant qu'il n'atteigne jamais l'historique du dépôt, ce qui est bien moins coûteux que de faire tourner une clé divulguée.
# Install gitleaks (Homebrew example)
brew install gitleaks
# Scan the whole repo once
gitleaks detect --source . --verbose
# Or install it as a pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
pre-commit install
pre-commit run --all-files
Si un vrai secret atterrit un jour dans un commit, faites tourner l'identifiant immédiatement. Supprimer la ligne ou réécrire l'historique ne « dé-divulgue » pas une clé déjà poussée vers un dépôt distant.
Valider manuellement l'authentification et l'autorisation
Les analyses automatisées manquent les bugs logiques, donc vous devez encore vérifier le flux à la main. Parcourez ces vérifications contre une instance en cours d'exécution.
Vérifications de session et d'authentification
- Déconnectez-vous, puis demandez une route protégée avec l'ancien token. Elle doit échouer avec 401, pas renvoyer de données.
- Changez votre propre identifiant utilisateur dans une URL ou un corps par l'identifiant d'un autre utilisateur. Le serveur doit le rejeter, pas faire confiance au paramètre.
- Appelez une route d'admin en tant que non-admin. Elle doit renvoyer 403 quel que soit tout indicateur fourni par le client.
- Confirmez que les cookies sont définis avec
httpOnly,secureet une expiration. Ouvrez la console du navigateur et essayez de lire le cookie avecdocument.cookie; il ne devrait pas apparaître. - Réinitialisez un mot de passe et vérifiez que l'ancienne session est invalidée.
Vérifications de token et d'en-tête
# A protected route without credentials must not return data
curl -i https://api.example.com/me
# A protected route with a forged or expired token must fail
curl -i https://api.example.com/me -H "Authorization: Bearer forged.token.value"
# An admin route as a normal user must return 403
curl -i https://api.example.com/admin/users -H "Authorization: Bearer $USER_TOKEN"
Exécutez ces vérifications contre un environnement de recette qui reflète la production, pas seulement contre localhost où les données sont jetables.
Analyse des dépendances et de la chaîne d'approvisionnement
Votre propre code n'est qu'une partie de la surface. Le code généré tire des dépendances, et un paquet vulnérable est une vulnérabilité que vous avez livrée. Analysez l'arbre à chaque changement.
# Node / npm -- built in, no extra install
npm audit
npm audit --production
# Python
pip-audit
pip-audit -r requirements.txt
# Snyk for a cross-language view
npm install -g snyk
snyk test
snyk monitor
# Or run the Snyk CLI against a Python project
pip install snyk
snyk test --file=requirements.txt
Branchez un scanner dans la CI pour qu'il s'exécute à chaque pull request plutôt que de compter sur la mémoire.
# .github/workflows/security.yml (excerpt)
- name: Scan dependencies
run: npm audit --production --audit-level=high
Avant d'accepter un paquet suggéré par un modèle, vérifiez son nombre de téléchargements, sa date de dernière publication et son mainteneur. Les paquets typosquattés sont un vecteur d'attaque réel, et un modèle peut suggérer un nom proche de la vraie bibliothèque sans l'être.
Une liste de contrôle de sécurité avant déploiement
Exécutez cette liste avant chaque déploiement. Elle est délibérément courte pour que vous l'utilisiez réellement.
- Aucun secret, clé d'API ou token en dur dans un fichier committé
-
.envet les fichiers de clés sont dans.gitignoreet n'ont jamais été committés - Tout le SQL utilise des requêtes paramétrées ou un ORM, sans concaténation de chaînes
- Toute entrée utilisateur est validée par rapport à un schéma avant utilisation
- Les tokens de session sont stockés dans des cookies httpOnly, pas
localStorage - Chaque route protégée renvoie 401/403 lorsqu'elle n'est pas authentifiée ou autorisée
- Les routes d'admin vérifient le rôle côté serveur, jamais à partir d'une valeur fournie par le client
- CORS n'autorise que vos origines connues, pas
*avec credentials -
npm audit/pip-auditne signale aucun résultat élevé ou critique - Les réponses d'erreur ne divulguent pas de traces de pile, de SQL ou de chemins internes
- La journalisation n'enregistre pas les tokens, mots de passe ou corps de requête complets
- Une passe de révision de sécurité a été exécutée sur chaque fichier que l'agent a touché ce sprint
Signaler et corriger les résultats
Les résultats ne sont utiles que s'ils sont corrigés et suivis. Donnez à chacun juste assez de structure pour agir.
- Enregistrez le résultat. Une ligne avec le fichier, la ligne, le type de vulnérabilité et la gravité.
- Écrivez la correction comme un diff. Faites produire par Cursor le changement de la ligne vulnérable vers la ligne corrigée, puis révisez-le.
- Ajoutez un test qui prouve la correction. Un test de régression pour l'injection ou le contournement d'authentification l'empêche de revenir.
- Mettez à jour la règle. Si l'agent a produit le bug, ajoutez une ligne à
.cursor/rules/security.mdcpour qu'il ne le produise plus. - Retestez et clôturez. Relancez les vérifications manuelles et le scanner contre la version corrigée.
Finding: CWE-89 SQL injection, src/api/users.ts:42, severity high
Vulnerable: db.query("SELECT * FROM users WHERE id = " + id)
Fixed: db.query("SELECT * FROM users WHERE id = $1", [id])
Regression test: request /user?id=1' OR '1'='1 returns no rows
Rule update: added "never concatenate user input into SQL" to security.mdc
Status: fixed, verified in staging
Traitez le fichier de règles comme un registre croissant de chaque erreur que l'agent a commise. Avec le temps, il devient l'actif de sécurité le plus précieux du dépôt, car il encode exactement les façons dont votre base de code a eu tort auparavant.