// docs / security guides / hardening
Comment sécuriser une app construite avec des outils de codage IA
Un guide de renforcement étape par étape pour les applications que vous avez créées avec Cursor, Claude Code, Lovable, Bolt, v0, Replit ou Windsurf. Quatre phases : comprendre pourquoi AI-les applications générées échouent différemment, exécuter un audit immédiat de la base de code, renforcer au moment du déploiement, puis continuer la surveillance. Opinion, narrative, avec de vrais extraits que vous pouvez copier.
Pourquoi AI-les applications générées échouent différemment
Les applications codées par Vibe peuvent être sécurisées. Ils ont besoin d’un audit supplémentaire car les modes de défaillance sont structurels et non imprudents :
- Cursor inlines hardcoded keys. Vous demandez à Cursor de « corriger l'erreur d'authentification » et il colle un exemple Supabase qui suppose un client de rôle de service. La clé se retrouve en haut d'un composant de page. Le client anonyme et le client de service coexistent ; les deux navires.
- Claude Code defaults to permissive CORS. Les gestionnaires Express / Fastify générés sont livrés avec
cors({ origin: '*' })car c'est le moyen le plus rapide d'obtenir un aperçu fonctionnel. Le middleware ne reçoit jamais de deuxième passage. - Lovable and v0 skip the rules file. Les projets soutenus par Firestore génèrent le modèle de données mais touchent rarement
firestore.rules. Les règles du mode test expirent silencieusement et verrouillent la base de données sans avertissement pour l'utilisateur. - Bolt skips RLS migrations. Bolt génère un schéma Supabase et une surface CRUD qui utilise la clé anon.
ENABLE ROW LEVEL SECURITYn'entre jamais dans la migration. Les utilisateurs anonymes peuvent lire ou écrire n'importe quelle ligne. - Windsurf trusts unsigned IDs. Généré
GET /api/items/[id]lit le paramètre et interroge Postgres sans vérifier la propriété. Le motif est suffisamment courant pour que la sonde active active.idor-walking le fasse surface en un seul balayage.
L'audit immédiat : recherchez votre base de code pour les modèles de risque
Avant de durcir quoi que ce soit, trouvez ce qui est déjà cassé. Ces greps prennent chacun moins d'une minute :
Secrets et clés du fournisseur
grep -RIn 'NEXT_PUBLIC_SUPABASE_SERVICE' src/
grep -RIn 'sk_live_\|pk_live_\|STRIPE_SECRET' src/
grep -RIn 'sk-ant-\|^sk-' src/ # Anthropic / OpenAI
grep -RIn 'AIza\|AKIA' src/ # Google / AWS
grep -RIn 'eyJh[A-Za-z0-9_-]\{20,\}' src/ # JWT-shaped stringsTout accès nécessite une suppression et une rotation des clés. ProTableaux de bord Vider : Supabase → Paramètres → API, Stripe → Développeurs → API touches, console Anthropic / OpenAI.
Contrôles d'accès à la base de données
# Supabase migrations
grep -RIn 'CREATE TABLE public\.' supabase/migrations/
grep -RIn 'ENABLE ROW LEVEL SECURITY\|FORCE ROW LEVEL SECURITY' supabase/migrations/
# Firebase / Firestore
cat firestore.rules # confirm no `if true;` matchesChaque CREATE TABLE public.* nécessite un ENABLE ROW LEVEL SECURITY correspondant et au moins une stratégie. Les règles Firestore doivent étendre les lectures à request.auth.uid.
Gestion de l'authentification et de la session
grep -RIn 'getSession()' src/ # should be getUser() server-side
grep -RIn 'localStorage\.\(set\|get\)Item.*token' src/
grep -RIn 'jwt.verify.*\(noVerify\|skipVerify\)' src/Les routes rendues par le serveur doivent utiliser supabase.auth.getUser() — elles sont vérifiées auprès du backend. getSession() lit un cookie non vérifié. Les jetons dans localStorage sont accessibles à tout script exécuté sur la page.
En-têtes et middleware
# Confirm middleware location for src/ layouts
ls src/middleware.ts middleware.ts 2>&1
# Look for CSP and security headers
grep -RIn 'Content-Security-Policy\|Strict-Transport-Security' src/Avec la disposition src/, seul src/middleware.ts est récupéré. Si votre fichier middleware se trouve à la racine du projet, Next.js l'ignore silencieusement et votre logique CSP / auth-refresh ne s'exécute jamais.
Durcissement au moment du déploiement
Une fois la source propre, verrouillez la manière dont l’application atteint la production.
Étape 1 : Séparer les environnements
Vercel : trois environnements — Production (votre domaine de production), Aperçu (PR / déploiements intermédiaires), Développement (local). Chacun obtient son propre ensemble de variables d'environnement. Les touches Live Stripe / Anthropic / Supabase n'atteignent jamais l'aperçu ; Les clés d'aperçu n'atteignent jamais Production. Les branches sont automatiquement poussées vers l'aperçu ; fusionner vers main se déploie vers Production.
Étape 2 : CSP strict via un middleware
Générez un nom occasionnel par requête, puis injectez-le dans Content-Security-Policy. Next.js applique automatiquement le nom occasionnel à ses propres balises de script lorsque vous définissez l'en-tête de requête x-nonce.
// src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = crypto.randomUUID().replace(/-/g, '');
const csp = [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://*.supabase.co`,
`object-src 'none'`,
`base-uri 'self'`,
`frame-ancestors 'none'`,
].join('; ');
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set('Content-Security-Policy', csp);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Étape 3 : Forcer RLS sur chaque table publique
RLS n'est pas activé par défaut et n'est pas appliqué pour les propriétaires de table, sauf si vous le FORCE. Associez chaque table à des politiques explicites par rôle.
-- supabase/migrations/XXXX_rls.sql
alter table public.profiles enable row level security;
alter table public.profiles force row level security;
create policy "profiles: read own"
on public.profiles for select
using (auth.uid() = id);
create policy "profiles: update own"
on public.profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);Étape 4 : Vérification d'authentification sur le serveur uniquement sur chaque route API
Chaque route API qui change d'état vérifie le côté serveur de l'appelant avec supabase.auth.getUser(). L'objet utilisateur devient la source de vérité pour user_id — ne faites jamais confiance à un corps de requête pour le définir.
// src/app/api/items/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(request: NextRequest) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
const body = await request.json();
const { data, error } = await supabase
.from('items')
.insert({ ...body, user_id: user.id }) // server-supplied, not from body
.select()
.single();
if (error) return NextResponse.json({ error: error.message }, { status: 400 });
return NextResponse.json(data);
}Étape 5 : Effectuez un proxy inverse pour vos analyses
Proxying analytique via votre propre domaine évite les bloqueurs de publicités et permet à votre CSP connect-src 'self' de rester étroit. Le même modèle fonctionne pour PostHog, Plausible, Umami et les récepteurs d'événements personnalisés.
// src/app/api/posthog/[...path]/route.ts
import { type NextRequest } from 'next/server';
const UPSTREAM = 'https://us.i.posthog.com';
export async function POST(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params;
const url = `${UPSTREAM}/${path.join('/')}`;
return fetch(url, {
method: 'POST',
headers: { 'content-type': req.headers.get('content-type') ?? 'application/json' },
body: await req.text(),
});
}Étape 6 : Garde de redirection ouverte sur le rebond post-authentification
Les flux de connexion/inscription acceptent généralement un paramètre de requête next. Rejetez tout ce qui n'est pas un chemin sur le même site : commencez par / et jamais // (relatif au protocole, envoie les utilisateurs hors site).
function safeNext(raw: string | null): string {
if (!raw) return '/dashboard';
if (!raw.startsWith('/') || raw.startsWith('//')) return '/dashboard';
return raw;
}En cours : surveillance et nouvelle analyse
La dérive se produit à chaque déploiement. Traitez la sécurité comme une boucle et non comme une liste de contrôle à terminer.
Vérifiez votre domaine de production
Dashboard → Domains → ajoutez votre domaine de production → DNS TXT ou HTTP-vérification du fichier (en une seule étape). Une fois vérifiées, les analyses actives deviennent disponibles et les nouvelles analyses planifiées peuvent être activées.
Planifier de nouvelles analyses passives
Tous les jours sur Hobby, toutes les 3 heures sur Pro, toutes les heures sur Unlimited. Chaque exécution vous envoie un e-mail si un nouveau résultat apparaît et déclenche un webhook scan.completed si vous êtes abonné.
# Or from CI, via the REST API:
curl -X POST https://fixvibe.app/api/v1/scans \
-H "authorization: Bearer $FIXVIBE_TOKEN" \
-H "content-type: application/json" \
-d '{"target":"https://your-app.com"}'Activer API-analyses actives (facultatif)
Si vous souhaitez une détection active automatisée (SQLi / XSS / IDOR marche / etc.), activez-la par domaine dans Tableau de bord → Domaines → API actif. L'autorisation est durable, expire 90 jours, instantanément révocable. Associez-le au webhook scan.active_api.first_used pour que la première analyse active automatisée après l'activation atteigne votre alerte.
Intégrez les résultats à votre flux de travail AI
Mint an API token at Account → API tokens, then configure the MCP server (/docs/mcp) in Claude Desktop / Cursor / Continue. Ask your agent: "Run a scan on staging and show me the highest-severity findings." The agent calls FixVibe, fetches the report, and renders categorized remediation guidance so code/config fixes become prompts and DNS/provider/manual fixes become operator steps.
Détection des menaces en direct (Unlimited)
Les différences dans les journaux de transparence des certificats font apparaître les nouveaux certificats TLS émis pour votre domaine. DNS-Les différences d'enregistrement détectent les modifications non autorisées. JS-La surveillance secrète du bundle se déclenche dès qu'une nouvelle clé atteint un bundle expédié. Les flux d'informations sur les menaces (Spamhaus, URLhaus) signalent votre domaine s'il est répertorié.
Modèles de défaillance réels et leurs correctifs
Cinq modèles issus d'analyses de production sur des milliers d'applications générées AI-, chacun avec le correctif réel :
- Clé de rôle de service dans un composant client
Symptom:
baas.supabase-service-keyconstatation sur la production URL. Cause: une saisie semi-automatique Cursor colléecreateClient(URL, SERVICE_ROLE_KEY)dans un composant React. Fix: déplacez le client de service verssrc/lib/supabase/service.tsavecimport 'server-only'en haut ; créez unsrc/lib/supabase/client.tsparallèle en utilisant la clé anon pour une utilisation côté client ; faites pivoter la clé de rôle de service via Supabase Studio. - Règles Firestore laissées en mode test
Symptom:
baas.firebase-rulesconstatation de gravité élevée. Cause: les règles générées sont luesallow read, write: if request.time < timestamp.date(2026, 6, 1);— un « tout autoriser » limité dans le temps. Fix: étend chaque règle à l'utilisateur authentifié —match /users/{userId}/posts/{postId} { allow read, write: if request.auth.uid == userId; }— et redéployezfirebase deploy --only firestore:rules. - Permissif CORS survivant en production
Symptom:
active.corshaute gravité. Cause: Middleware Express généré :app.use(cors({ origin: '*' })). Fix: ajoutez votre origine frontend à la liste blanche :app.use(cors({ origin: ['https://your-app.com'], credentials: true })). Pour les routes Next.js API, définissezAccess-Control-Allow-Originexplicitement dans la réponse. - RLS activé mais pas forcé
Symptom: actif
baas.supabase-rlssignale que le rôle anon peut écrire dans une table publique même si RLS est activé dans le tableau de bord. Cause:ENABLEsansFORCElaisse le propriétaire de la table exempté — et les migrations s'exécutent en tant que propriétaire. Fix: ajoutezalter table public.items force row level security;à la migration. Redéployer. - IDOR-ID accessibles à pied non signés
Symptom:
active.idor-walkingrapporte que l'utilisateur anonyme peut lire/api/items/1,/api/items/2, ... entre les locataires. Cause: le gestionnaire API fait confiance au paramètre de chemin et interroge Postgres sans prédicat de propriété. Fix: ajoutez.eq('user_id', user.id)à chaque requête de lecture, ou passez aux URL/UUID signés limités à/api/users/[uid]/items/[id].
La boucle de sécurité du vibe-code
L'objectif n'est pas une sécurité parfaite ; cela élimine les fruits à portée de main AI que les outils manquent constamment afin que vous puissiez continuer à expédier rapidement.
- Generate fast — utilisez Cursor, Claude Code, Lovable, Bolt. C'est le point.
- Audit immediately — exécutez l'ensemble grep ci-dessus, vérifiez RLS, vérifiez CSP, vérifiez la limite d'authentification.
- Harden at deploy — middleware, séparation de l'environnement, CSP nonce, HSTS, vérification d'authentification sur le serveur uniquement.
- Monitor — FixVibe passif quotidiennement, actif chaque semaine sur un domaine vérifié, webhooks vers Slack, détection des menaces sur Unlimited.
- Fix fast — use FixVibe coding-agent prompts for code/config findings and operator steps for DNS, provider, secret-rotation, or manual-review findings. Re-deploy, re-scan, close the loop.
Prochaines étapes
Pour obtenir le contexte conceptuel sur DAST vs SAST et pourquoi AI-les applications générées ont besoin de leur propre analyse, lisez AI-generated code security scanning. Pour un audit de référence rapide avant expédition, consultez le vibe coding security checklist.
