Skip to Content
Wagoo SaaS 1.0.25 is released 🎉
02 ArchitectureSécuritéSécurité

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.db

Protection 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 site

Validation 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.local dans .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