Sécurité
Fichiers Sensibles ⚠️
Certains fichiers ne doivent JAMAIS être commités ou exposés :
.env.local ou .env.production.local
# ❌ JAMAIS COMMITER - Contient tous les secrets
DATABASE_URL="postgresql://..."
STRIPE_SECRET_KEY="sk_..."
GITHUB_CLIENT_SECRET="ghp_..."
BETTER_AUTH_SECRET="..."auth.ts ou configuration Auth
// ❌ Ne JAMAIS exposer les clés secrètes ici
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET, // OK (env var)
// ❌ secret: "hard-coded-secret" // MAUVAIS
});Fichiers à ignorer dans .gitignore
# Environment variables
.env.local
.env.production.local
.env.*.local
# Secrets
*.key
*.pem
secrets/
# Dependencies (trop gros)
node_modules/
.pnpm-store/
# Build outputs
.next/
dist/
build/
# IDE
.vscode/
.idea/
*.swp
# OS
.DS_Store
Thumbs.dbProtection des Secrets
1. Variables d’environnement
# ✅ BON : Utiliser les env vars
DATABASE_URL=${DATABASE_URL}
# ❌ MAUVAIS : Hard-coder
const dbUrl = "postgresql://user:password@host/db";2. Stripe Webhook Secrets
JAMAIS exposer votre STRIPE_WEBHOOK_SECRET :
// ✅ BON
export async function POST(request: Request) {
const sig = request.headers.get("stripe-signature");
const body = await request.text();
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET // Env var
);
}
// ❌ MAUVAIS
const secret = "whsec_live_..."; // Hard-coded!3. API Keys
// ✅ BON
const githubSecret = process.env.GITHUB_CLIENT_SECRET;
// ❌ MAUVAIS
const githubSecret = "ghp_xxxxxxxxxxxx"; // Visible en Git!Authentification Sécurisée
Hachage des mots de passe
Wagoo utilise bcrypt automatiquement :
// ✅ better-auth gère le hachage
await auth.signUp({ email, password });
// ❌ NE PAS faire ça
const hashedPassword = password; // Pas de hachage!Sessions sécurisées
// ✅ better-auth crée des sessions sécurisées
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 jours
updateAge: 60 * 60 * 24, // Rafraîchir chaque jour
cookieCache: { maxAge: 5 * 60, maxSize: 10 }
}
});Cookies HttpOnly
// ✅ Automatique avec better-auth
// Cookies HttpOnly (non-accessibles en JavaScript)
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Strict;Protection CSRF
Intégrée dans Next.js
// ✅ Next.js protège automatiquement les formulaires
<form action="/api/projects" method="POST">
{/* CSRF token ajouté automatiquement */}
</form>
// ✅ API routes protégées
export async function POST(request: Request) {
// Next.js vérifie le CSRF token automatiquement
}SameSite Cookies
// ✅ Défini dans better-auth
Set-Cookie: session=...; SameSite=Strict;
// Cookies envoyés UNIQUEMENT pour le même siteValidation des entrées
Côté client
// ✅ Valider avant d'envoyer
const validateEmail = (email: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
if (!validateEmail(email)) {
setError("Email invalide");
return;
}Côté serveur (IMPORTANT!)
// ✅ TOUJOURS valider côté serveur
import { z } from "zod";
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export async function POST(request: Request) {
const body = await request.json();
try {
const { email, password } = userSchema.parse(body);
// Traiter les données validées
} catch (e) {
return Response.json(
{ error: "Validation failed" },
{ status: 400 }
);
}
}Protection SQL Injection
Avec Prisma
// ✅ Prisma échappe automatiquement les requêtes
const users = await prisma.user.findMany({
where: { email: userInput } // Sûr!
});
// ❌ Ne PAS faire ça
const result = await db.raw(`SELECT * FROM users WHERE email = '${userInput}'`);Protection XSS
Avec React
// ✅ React échappe le contenu par défaut
<div>{userContent}</div>
// L'utilisateur ne peut pas injecter du HTML
// ❌ Utiliser dangerouslySetInnerHTML uniquement si nécessaire
// Et TOUJOURS nettoyer le contenu
import DOMPurify from "dompurify";
const cleanHtml = DOMPurify.sanitize(userContent);
<div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;TipTap (Rich Text Editor)
// ✅ TipTap échappe automatiquement
// Le contenu de l'éditeur est sûr par défaut
<EditorContent editor={editor} />
// ✅ Si vous parsez du HTML utilisateur
import { sanitizeHtml } from 'lib/sanitizer';
const clean = sanitizeHtml(userHtml);Rate Limiting
Implémenter un rate limiter
// Avec Upstash Redis
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, "10 m"),
});
export async function POST(request: Request) {
const ip = request.headers.get("x-forwarded-for") || "anonymous";
const { success, pending } = await ratelimit.limit(ip);
if (!success) {
return Response.json(
{ error: "Too many requests" },
{ status: 429 }
);
}
// Traiter la requête
}Stripe Webhook Rate Limiting
// Stripe rate limite automatiquement les webhooks
// Implémenter une file d'attente pour la sécurité
import { Queue } from "bullmq";
const stripeQueue = new Queue("stripe-webhooks");
export async function POST(request: Request) {
const event = await constructStripeEvent(request);
// Ajouter à la file d'attente au lieu de traiter immédiatement
await stripeQueue.add("process", event);
return Response.json({ received: true });
}
// Worker qui traite les webhooks
stripeQueue.process(async (job) => {
const event = job.data;
await handleStripeEvent(event);
});CORS & Origins de confiance
Configuration
// next.config.ts
/** @type {import('next').NextConfig} */
const nextConfig = {
headers: async () => {
return [
{
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: process.env.NEXT_PUBLIC_APP_URL,
},
{
key: "Access-Control-Allow-Methods",
value: "GET,POST,PUT,DELETE",
},
{
key: "Access-Control-Allow-Credentials",
value: "true",
},
],
},
];
},
};Mode Maintenance
API pour activer le mode maintenance
// app/api/cli/maintenance/route.ts
export async function POST(request: Request) {
const { enabled } = await request.json();
// Vérifier que l'admin est authentifié
const session = await auth.getSession(request);
if (!session?.user.isAdmin) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Activer/désactiver le mode maintenance
// (Utiliser Redis ou une variable d'env)
return Response.json({ maintenance: enabled });
}Middleware de maintenance
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const MAINTENANCE_MODE = process.env.MAINTENANCE_MODE === "true";
export function middleware(request: NextRequest) {
if (MAINTENANCE_MODE && !request.nextUrl.pathname.startsWith("/admin")) {
return NextResponse.rewrite(new URL("/maintenance", request.url));
}
return NextResponse.next();
}Checkliste de sécurité
- ✅
.env.localdans.gitignore - ✅ Variables d’env pour tous les secrets
- ✅ HTTPS en local (pour Stripe, cookies)
- ✅ Validation côté serveur obligatoire
- ✅ Hachage des mots de passe (better-auth)
- ✅ Cookies HttpOnly
- ✅ Rate limiting sur les endpoints publics
- ✅ Webhook Stripe avec signature verification
- ✅ Admin check pour les opérations sensibles
- ✅ Logs des actions sensibles
- ✅ Expiration des sessions
- ✅ 2FA pour les comptes admins
Audit de sécurité
Périodiquement :
# Vérifier les vulnérabilités connues
pnpm audit
# Mettre à jour les dépendances
pnpm update --latest
# Vérifier l'utilisation des env vars
grep -r "sk_\|whsec_\|secret\|password" app/ --include="*.ts" --include="*.tsx"
# Ne doit rien retourner!Ressources
Last updated on