Securiser une Application Next.js : Le Guide Complet de Cybersecurite pour 2026

La cybersecurite n'est plus une option — c'est une necesssite. Avec les offres d'emploi en cybersecurite qui ont double entre 2024 et 2025, et une croissance projetee de 29 % d'ici 2034, savoir securiser une application web est devenu une competence incontournable pour tout developpeur. Ce guide vous montre comment blinder votre application Next.js, etape par etape.
Objectifs d'apprentissage
A la fin de ce guide, vous serez capable de :
- Configurer les en-tetes de securite HTTP essentiels (CSP, HSTS, X-Frame-Options)
- Implementer une authentification robuste avec NextAuth.js v5
- Proteger vos formulaires contre les attaques CSRF et XSS
- Valider et assainir les donnees utilisateur avec Zod
- Mettre en place un rate limiting pour prevenir les attaques par force brute
- Securiser vos Routes API et Server Actions dans Next.js 15
- Appliquer les recommandations du OWASP Top 10 a votre application
Prerequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installe (
node --version) - Une application Next.js 14 ou 15 existante (App Router recommande)
- Des connaissances de base en TypeScript et React
- Un editeur de code — VS Code ou Cursor recommande
- Une comprehension basique des concepts de HTTP et des cookies
Ce que vous allez securiser
Nous allons construire une couche de securite complete autour d'une application Next.js typique :
- En-tetes HTTP — Protection contre le clickjacking, le MIME sniffing et les injections
- Content Security Policy — Controle strict des ressources chargees par le navigateur
- Authentification — Connexion securisee avec sessions protegees
- Validation des donnees — Schema de validation cote serveur pour chaque entree utilisateur
- Rate limiting — Protection contre le brute force et le DDoS au niveau applicatif
- Protection CSRF — Jetons anti-falsification pour les mutations
- Securite des API — Middleware d'autorisation pour les endpoints
Etape 1 : Configurer les en-tetes de securite HTTP
Les en-tetes de securite HTTP sont votre premiere ligne de defense. Ils indiquent au navigateur comment se comporter face aux menaces courantes.
Pourquoi c'est important
Sans en-tetes de securite, votre application est vulnerable a :
- Clickjacking — Un attaquant integre votre page dans une iframe invisible
- MIME type sniffing — Le navigateur interprete mal le type de fichier
- Donnees sensibles dans le referrer — Des informations privees fuient via l'en-tete Referer
- Absence de HTTPS — Les donnees circulent en clair sur le reseau
Implementation dans next.config.ts
// next.config.ts
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
];
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};
export default nextConfig;Voici ce que fait chaque en-tete :
| En-tete | Protection |
|---|---|
Strict-Transport-Security | Force HTTPS pendant 2 ans, meme si l'utilisateur tape http:// |
X-Frame-Options | Empeche l'integration de votre site dans une iframe (anti-clickjacking) |
X-Content-Type-Options | Empeche le navigateur de deviner le type MIME |
Referrer-Policy | Limite les informations envoyees dans l'en-tete Referer |
Permissions-Policy | Desactive les APIs sensibles (camera, micro, geolocalisation) |
Verifier vos en-tetes
Apres le deploiement, testez vos en-tetes avec securityheaders.com ou via la ligne de commande :
curl -I https://votre-domaine.comVous devriez viser un score A+ sur securityheaders.com. La plupart des applications Next.js par defaut obtiennent un F — cette seule etape vous fait passer directement a A.
Etape 2 : Mettre en place une Content Security Policy (CSP)
La CSP est l'en-tete de securite le plus puissant — et le plus complexe. Elle controle exactement quelles ressources votre page peut charger.
Pourquoi la CSP est cruciale
Sans CSP, une faille XSS permet a un attaquant d'injecter n'importe quel script dans votre page. Avec une CSP stricte, meme si un attaquant trouve une faille XSS, le navigateur bloquera le script malveillant.
Implementation avec nonce dans le middleware
L'approche recommandee par Next.js utilise un nonce (number used once) genere a chaque requete :
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, " ").trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
response.headers.set("Content-Security-Policy", cspHeader);
return response;
}
export const config = {
matcher: [
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};Utiliser le nonce dans vos composants
Pour que vos scripts inline fonctionnent avec la CSP, passez le nonce via les headers :
// app/layout.tsx
import { headers } from "next/headers";
import Script from "next/script";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headersList = await headers();
const nonce = headersList.get("x-nonce") ?? "";
return (
<html lang="fr">
<body>
{children}
<Script
nonce={nonce}
strategy="afterInteractive"
src="/scripts/analytics.js"
/>
</body>
</html>
);
}Ne utilisez jamais 'unsafe-inline' ou 'unsafe-eval' dans votre CSP en production. Ces directives annulent presque entierement la protection CSP. Utilisez toujours des nonces ou des hashes.
Directives CSP expliquees
| Directive | Role | Valeur recommandee |
|---|---|---|
default-src | Politique par defaut pour toutes les ressources | 'self' |
script-src | Sources autorisees pour JavaScript | 'self' 'nonce-xxx' 'strict-dynamic' |
style-src | Sources autorisees pour CSS | 'self' 'nonce-xxx' |
img-src | Sources autorisees pour les images | 'self' blob: data: https: |
object-src | Bloque les plugins (Flash, Java) | 'none' |
frame-ancestors | Qui peut integrer votre page en iframe | 'none' |
form-action | Ou les formulaires peuvent soumettre | 'self' |
upgrade-insecure-requests | Force HTTPS pour toutes les sous-requetes | (pas de valeur) |
Etape 3 : Securiser l'authentification avec NextAuth.js v5
L'authentification est souvent le maillon faible d'une application. NextAuth.js v5 (Auth.js) offre une solution robuste et securisee.
Installation
npm install next-auth@betaConfiguration de base
// auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import bcrypt from "bcryptjs";
const loginSchema = z.object({
email: z.string().email("Adresse email invalide"),
password: z
.string()
.min(8, "Le mot de passe doit contenir au moins 8 caracteres")
.max(128, "Le mot de passe est trop long"),
});
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Mot de passe", type: "password" },
},
async authorize(credentials) {
// Valider les donnees d'entree
const parsed = loginSchema.safeParse(credentials);
if (!parsed.success) return null;
const { email, password } = parsed.data;
// Rechercher l'utilisateur en base de donnees
const user = await getUserByEmail(email);
if (!user || !user.hashedPassword) return null;
// Verifier le mot de passe
const isValid = await bcrypt.compare(password, user.hashedPassword);
if (!isValid) return null;
return {
id: user.id,
email: user.email,
name: user.name,
};
},
}),
],
session: {
strategy: "jwt",
maxAge: 24 * 60 * 60, // 24 heures
},
pages: {
signIn: "/connexion",
error: "/connexion?error=true",
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (token.id) {
session.user.id = token.id as string;
}
return session;
},
},
});Bonnes pratiques d'authentification
1. Hashage des mots de passe avec bcrypt
// lib/auth-utils.ts
import bcrypt from "bcryptjs";
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}Utilisez un minimum de 12 rounds de sel pour bcrypt en 2026. Le standard de 10 rounds est de plus en plus considere comme insuffisant face aux GPU modernes.
2. Protection des routes avec middleware
// middleware.ts (section authentification)
import { auth } from "@/auth";
const protectedRoutes = ["/dashboard", "/profil", "/parametres"];
const authRoutes = ["/connexion", "/inscription"];
export default auth((req) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
const isProtected = protectedRoutes.some((route) =>
nextUrl.pathname.startsWith(route)
);
const isAuthRoute = authRoutes.some((route) =>
nextUrl.pathname.startsWith(route)
);
// Rediriger les utilisateurs non connectes
if (isProtected && !isLoggedIn) {
const callbackUrl = encodeURIComponent(nextUrl.pathname);
return Response.redirect(
new URL(`/connexion?callbackUrl=${callbackUrl}`, nextUrl)
);
}
// Rediriger les utilisateurs connectes loin des pages d'auth
if (isAuthRoute && isLoggedIn) {
return Response.redirect(new URL("/dashboard", nextUrl));
}
});3. Ne jamais exposer d'informations sensibles
// MAUVAIS — revele si l'email existe dans la base
if (!user) return { error: "Aucun compte avec cet email" };
if (!isValid) return { error: "Mot de passe incorrect" };
// BON — message generique qui ne revele rien
if (!user || !isValid) {
return { error: "Identifiants invalides" };
}Etape 4 : Valider les donnees avec Zod
La validation cote client ne suffit pas — un attaquant peut la contourner facilement. La validation cote serveur est obligatoire.
Installation
npm install zodCreer des schemas de validation reutilisables
// lib/validations/user.ts
import { z } from "zod";
export const createUserSchema = z.object({
name: z
.string()
.min(2, "Le nom doit contenir au moins 2 caracteres")
.max(50, "Le nom est trop long")
.regex(
/^[a-zA-ZÀ-ÿ\s'-]+$/,
"Le nom ne peut contenir que des lettres, espaces, apostrophes et tirets"
),
email: z
.string()
.email("Adresse email invalide")
.toLowerCase()
.trim(),
password: z
.string()
.min(8, "Le mot de passe doit contenir au moins 8 caracteres")
.max(128, "Le mot de passe est trop long")
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Le mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre"
),
});
export const updateProfileSchema = z.object({
name: z
.string()
.min(2)
.max(50)
.regex(/^[a-zA-ZÀ-ÿ\s'-]+$/)
.optional(),
bio: z
.string()
.max(500, "La bio ne peut pas depasser 500 caracteres")
.optional(),
website: z
.string()
.url("URL invalide")
.optional()
.or(z.literal("")),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;Utiliser la validation dans les Server Actions
// app/actions/user.ts
"use server";
import { createUserSchema } from "@/lib/validations/user";
import { hashPassword } from "@/lib/auth-utils";
export async function createUser(formData: FormData) {
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
password: formData.get("password"),
};
// Valider les donnees cote serveur
const result = createUserSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
};
}
const { name, email, password } = result.data;
// Hasher le mot de passe avant stockage
const hashedPassword = await hashPassword(password);
// Creer l'utilisateur en base de donnees
try {
await db.user.create({
data: { name, email, hashedPassword },
});
return { success: true };
} catch (error) {
// Ne jamais exposer les erreurs de base de donnees
return {
success: false,
errors: { _form: ["Une erreur est survenue. Veuillez reessayer."] },
};
}
}Assainir les donnees contre les XSS
// lib/sanitize.ts
import DOMPurify from "isomorphic-dompurify";
export function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
ALLOWED_ATTR: ["href", "target", "rel"],
});
}
export function escapeForDisplay(input: string): string {
return input
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}Regle d'or : ne faites jamais confiance aux donnees provenant du client. Meme si vous avez une validation cote client avec React Hook Form ou Formik, validez toujours a nouveau cote serveur avec Zod.
Etape 5 : Implementer le Rate Limiting
Le rate limiting protege votre application contre les attaques par force brute, le scraping abusif et le DDoS au niveau applicatif.
Approche avec Upstash Redis
npm install @upstash/ratelimit @upstash/redis// lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// Rate limiter pour les endpoints generaux : 60 requetes par minute
export const generalLimiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(60, "1 m"),
analytics: true,
prefix: "ratelimit:general",
});
// Rate limiter strict pour l'authentification : 5 tentatives par 15 minutes
export const authLimiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, "15 m"),
analytics: true,
prefix: "ratelimit:auth",
});
// Rate limiter pour les API : 100 requetes par minute
export const apiLimiter = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, "1 m"),
analytics: true,
prefix: "ratelimit:api",
});Appliquer le rate limiting dans le middleware
// middleware.ts (section rate limiting)
import { generalLimiter, authLimiter } from "@/lib/rate-limit";
import { NextRequest, NextResponse } from "next/server";
function getClientIp(request: NextRequest): string {
const forwarded = request.headers.get("x-forwarded-for");
const ip = forwarded?.split(",")[0]?.trim() ?? request.ip ?? "unknown";
return ip;
}
async function handleRateLimit(request: NextRequest) {
const ip = getClientIp(request);
const path = request.nextUrl.pathname;
// Rate limiting strict pour les routes d'authentification
if (path.startsWith("/api/auth") || path === "/connexion") {
const { success, remaining, reset } = await authLimiter.limit(ip);
if (!success) {
return NextResponse.json(
{
error: "Trop de tentatives. Veuillez reessayer plus tard.",
retryAfter: Math.ceil((reset - Date.now()) / 1000),
},
{
status: 429,
headers: {
"Retry-After": String(Math.ceil((reset - Date.now()) / 1000)),
"X-RateLimit-Remaining": String(remaining),
},
}
);
}
}
// Rate limiting general pour les API
if (path.startsWith("/api/")) {
const { success, remaining, reset } = await generalLimiter.limit(ip);
if (!success) {
return NextResponse.json(
{ error: "Limite de requetes atteinte." },
{
status: 429,
headers: {
"Retry-After": String(Math.ceil((reset - Date.now()) / 1000)),
"X-RateLimit-Remaining": String(remaining),
},
}
);
}
}
return null; // Pas de rate limit atteint
}Alternative sans Redis : rate limiting en memoire
Si vous n'avez pas Redis, voici une solution simple pour les petits projets :
// lib/rate-limit-memory.ts
const requests = new Map<string, { count: number; resetTime: number }>();
export function rateLimit(
ip: string,
maxRequests: number = 60,
windowMs: number = 60_000
): { success: boolean; remaining: number } {
const now = Date.now();
const record = requests.get(ip);
if (!record || now > record.resetTime) {
requests.set(ip, { count: 1, resetTime: now + windowMs });
return { success: true, remaining: maxRequests - 1 };
}
if (record.count >= maxRequests) {
return { success: false, remaining: 0 };
}
record.count++;
return { success: true, remaining: maxRequests - record.count };
}
// Nettoyer les anciennes entrees toutes les 5 minutes
setInterval(() => {
const now = Date.now();
for (const [key, value] of requests) {
if (now > value.resetTime) {
requests.delete(key);
}
}
}, 5 * 60 * 1000);La solution en memoire fonctionne uniquement pour un seul serveur. Si vous avez plusieurs instances (load balancing), utilisez Redis avec Upstash — c'est gratuit jusqu'a 10 000 requetes par jour.
Etape 6 : Proteger contre les attaques CSRF
Next.js protege automatiquement les Server Actions contre le CSRF en verifiant l'en-tete Origin. Mais pour vos API Routes classiques, vous devez ajouter une protection supplementaire.
Verifier l'en-tete Origin dans les API Routes
// lib/csrf.ts
export function validateOrigin(request: Request): boolean {
const origin = request.headers.get("origin");
const host = request.headers.get("host");
if (!origin || !host) return false;
const allowedOrigins = [
`https://${host}`,
process.env.NEXT_PUBLIC_APP_URL,
].filter(Boolean);
return allowedOrigins.includes(origin);
}Middleware de protection CSRF pour les API
// lib/api-middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { validateOrigin } from "@/lib/csrf";
export function withCsrfProtection(
handler: (req: NextRequest) => Promise<NextResponse>
) {
return async (request: NextRequest) => {
// Les requetes GET sont sures (pas de mutation)
if (request.method === "GET" || request.method === "HEAD") {
return handler(request);
}
// Verifier l'origine pour les requetes mutantes
if (!validateOrigin(request)) {
return NextResponse.json(
{ error: "Requete non autorisee" },
{ status: 403 }
);
}
return handler(request);
};
}Utilisation dans une Route API
// app/api/articles/route.ts
import { NextRequest, NextResponse } from "next/server";
import { withCsrfProtection } from "@/lib/api-middleware";
import { auth } from "@/auth";
async function handler(request: NextRequest) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Non authentifie" }, { status: 401 });
}
const body = await request.json();
// ... traitement de la requete
return NextResponse.json({ success: true });
}
export const POST = withCsrfProtection(handler);Etape 7 : Securiser les variables d'environnement
Les variables d'environnement mal gerees sont une source frequente de fuites de donnees.
Regles strictes
// lib/env.ts
import { z } from "zod";
const envSchema = z.object({
// Variables serveur uniquement — ne jamais prefixer par NEXT_PUBLIC_
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
UPSTASH_REDIS_REST_URL: z.string().url(),
UPSTASH_REDIS_REST_TOKEN: z.string().min(1),
// Variables publiques — disponibles cote client
NEXT_PUBLIC_APP_URL: z.string().url(),
});
// Valider au demarrage — crash immediat si une variable manque
export const env = envSchema.parse(process.env);Ce qui ne doit JAMAIS etre dans les variables publiques
| Variable | Prefix NEXT_PUBLIC_ ? | Raison |
|---|---|---|
| Cle API secrete | NON | Visible dans le bundle client |
| URL de base de donnees | NON | Acces direct a la BDD |
| Secret JWT/Auth | NON | Permet de falsifier des tokens |
| Cle Stripe secrete | NON | Transactions frauduleuses |
| URL de l'app | OUI | Information publique |
| Cle Stripe publique | OUI | Concue pour le client |
Ajoutez toujours .env, .env.local, .env.production a votre .gitignore. Une seule variable secrete committee par erreur peut compromettre toute votre application.
Fichier .env.example pour l'equipe
# .env.example — Copiez en .env.local et remplissez les valeurs
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
AUTH_SECRET=votre-secret-de-32-caracteres-minimum
UPSTASH_REDIS_REST_URL=https://votre-instance.upstash.io
UPSTASH_REDIS_REST_TOKEN=votre-token-upstash
NEXT_PUBLIC_APP_URL=http://localhost:3000Etape 8 : Securiser les Server Actions de Next.js
Les Server Actions sont puissantes mais necessitent des precautions specifiques.
Toujours verifier l'autorisation
// app/actions/article.ts
"use server";
import { auth } from "@/auth";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const articleSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10).max(50000),
published: z.boolean().default(false),
});
export async function createArticle(formData: FormData) {
// 1. Verifier l'authentification
const session = await auth();
if (!session?.user?.id) {
throw new Error("Non authentifie");
}
// 2. Valider les donnees
const result = articleSchema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
published: formData.get("published") === "true",
});
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}
// 3. Verifier les permissions (autorisation)
const userRole = await getUserRole(session.user.id);
if (!["admin", "editor"].includes(userRole)) {
throw new Error("Permissions insuffisantes");
}
// 4. Effectuer l'operation
await db.article.create({
data: {
...result.data,
authorId: session.user.id,
},
});
revalidatePath("/articles");
return { success: true };
}Pattern de securite pour les Server Actions
Suivez toujours cet ordre dans vos Server Actions :
1. Authentification — L'utilisateur est-il connecte ?
2. Validation — Les donnees sont-elles conformes au schema ?
3. Autorisation — L'utilisateur a-t-il le droit d'effectuer cette action ?
4. Execution — Effectuer l'operation
5. Revalidation — Mettre a jour le cache si necessaire
Etape 9 : Proteger contre les injections SQL
Si vous utilisez un ORM comme Prisma ou Drizzle, vous etes largement protege. Mais attention aux requetes brutes.
Avec Prisma — Utiliser les parametres lies
// BON — Prisma echappe automatiquement les parametres
const user = await prisma.user.findUnique({
where: { email: userInput },
});
// BON — Requete brute avec parametres lies
const results = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${userInput}
`;
// MAUVAIS — Concatenation de chaines = injection SQL
const results = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${userInput}'` // VULNERABLE !
);Avec Drizzle ORM
import { eq } from "drizzle-orm";
import { users } from "@/db/schema";
// BON — Drizzle utilise des parametres lies
const user = await db
.select()
.from(users)
.where(eq(users.email, userInput));
// MAUVAIS — Ne jamais concatener l'input utilisateur dans du SQL
const user = await db.execute(
sql`SELECT * FROM users WHERE email = '${userInput}'` // VULNERABLE !
);
// BON — Utiliser sql.placeholder ou les parametres
const user = await db.execute(
sql`SELECT * FROM users WHERE email = ${userInput}`
);Etape 10 : Checklist de securite OWASP Top 10
Voici comment chaque vulnerabilite du OWASP Top 10 s'applique a Next.js et les mesures que nous avons couvertes :
| # | Vulnerabilite OWASP | Protection Next.js | Etape |
|---|---|---|---|
| A01 | Broken Access Control | Middleware auth + verification dans Server Actions | 3, 8 |
| A02 | Cryptographic Failures | bcrypt, HTTPS (HSTS), secrets en env serveur | 3, 7 |
| A03 | Injection (SQL, XSS) | Zod + ORM parametrise + CSP | 2, 4, 9 |
| A04 | Insecure Design | Validation a chaque couche, principe du moindre privilege | 4, 8 |
| A05 | Security Misconfiguration | En-tetes HTTP, CSP, env validation | 1, 2, 7 |
| A06 | Vulnerable Components | npm audit, Dependabot, mises a jour regulieres | Ci-dessous |
| A07 | Auth & Session Failures | NextAuth.js v5, JWT securise, rate limiting | 3, 5 |
| A08 | Data Integrity Failures | Validation Zod cote serveur, CSRF protection | 4, 6 |
| A09 | Logging & Monitoring | Logs structures, alertes sur erreurs 4xx/5xx | Ci-dessous |
| A10 | Server-Side Request Forgery | Validation des URLs, liste blanche de domaines | Ci-dessous |
A06 : Surveiller les dependances vulnerables
# Verifier les vulnerabilites connues
npm audit
# Corriger automatiquement les failles benignes
npm audit fix
# Installer Dependabot ou Renovate pour les mises a jour automatiquesA09 : Logging structure
// lib/logger.ts
type LogLevel = "info" | "warn" | "error" | "security";
interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
ip?: string;
userId?: string;
path?: string;
metadata?: Record<string, unknown>;
}
export function log(entry: Omit<LogEntry, "timestamp">) {
const logEntry: LogEntry = {
...entry,
timestamp: new Date().toISOString(),
};
// En production, envoyez vers un service de logging (Datadog, Sentry, etc.)
if (entry.level === "security" || entry.level === "error") {
console.error(JSON.stringify(logEntry));
} else {
console.log(JSON.stringify(logEntry));
}
}
// Exemples d'utilisation
log({
level: "security",
message: "Tentative de connexion echouee",
ip: "192.168.1.1",
path: "/api/auth/signin",
metadata: { email: "suspect@example.com", attempt: 5 },
});A10 : Prevention du SSRF
// lib/url-validator.ts
const ALLOWED_DOMAINS = [
"api.example.com",
"cdn.example.com",
"images.unsplash.com",
];
export function isAllowedUrl(url: string): boolean {
try {
const parsed = new URL(url);
// Bloquer les protocoles dangereux
if (!["http:", "https:"].includes(parsed.protocol)) {
return false;
}
// Bloquer les adresses IP locales
const hostname = parsed.hostname;
if (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname.startsWith("192.168.") ||
hostname.startsWith("10.") ||
hostname.startsWith("172.16.")
) {
return false;
}
// Verifier la liste blanche de domaines
return ALLOWED_DOMAINS.some(
(domain) => hostname === domain || hostname.endsWith(`.${domain}`)
);
} catch {
return false;
}
}Tester la securite de votre application
Outils recommandes
| Outil | Type | Usage |
|---|---|---|
| OWASP ZAP | Scanner gratuit | Tests automatises de vulnerabilites |
| Burp Suite Community | Proxy d'interception | Tests manuels approfondis |
| securityheaders.com | En ligne | Verification des en-tetes HTTP |
| Mozilla Observatory | En ligne | Audit de securite complet |
npm audit | CLI | Vulnerabilites des dependances |
| Snyk | SaaS | Surveillance continue des dependances |
Script de verification rapide
#!/bin/bash
# security-check.sh — Verification rapide de securite
echo "=== Verification de securite ==="
# 1. Verifier les dependances vulnerables
echo "\n1. Audit des dependances..."
npm audit --production
# 2. Verifier que .env n'est pas committe
echo "\n2. Verification des fichiers sensibles..."
if git ls-files --error-unmatch .env 2>/dev/null; then
echo "ALERTE : .env est suivi par git !"
else
echo "OK : .env n'est pas dans git"
fi
# 3. Rechercher des secrets potentiels dans le code
echo "\n3. Recherche de secrets dans le code..."
grep -rn "password\|secret\|api_key\|private_key" --include="*.ts" --include="*.tsx" \
--exclude-dir=node_modules --exclude-dir=.next \
| grep -v "process.env" \
| grep -v "schema" \
| grep -v "type\|interface\|placeholder"
echo "\n=== Verification terminee ==="Depannage
La CSP bloque mes scripts tiers
Si vous integrez des services tiers (analytics, chat, etc.), ajoutez leurs domaines a la directive appropriee :
// Exemple : autoriser Google Analytics et Intercom
const cspHeader = `
script-src 'self' 'nonce-${nonce}' https://www.googletagmanager.com https://widget.intercom.io;
connect-src 'self' https://www.google-analytics.com https://api-iam.intercom.io;
`;Le rate limiting bloque les utilisateurs legitimes
Ajustez les seuils selon votre trafic reel :
- Pages publiques : 120 requetes/minute
- API authentifiee : 100 requetes/minute
- Login : 5-10 tentatives par 15 minutes
- Inscription : 3 par heure par IP
Erreurs de validation Zod cote client
Utilisez useActionState de React 19 pour afficher les erreurs proprement :
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions/user";
export function SignupForm() {
const [state, formAction, isPending] = useActionState(createUser, null);
return (
<form action={formAction}>
<input name="name" type="text" required />
{state?.errors?.name && (
<p className="text-red-500 text-sm">{state.errors.name[0]}</p>
)}
<input name="email" type="email" required />
{state?.errors?.email && (
<p className="text-red-500 text-sm">{state.errors.email[0]}</p>
)}
<input name="password" type="password" required />
{state?.errors?.password && (
<p className="text-red-500 text-sm">{state.errors.password[0]}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Inscription en cours..." : "Creer un compte"}
</button>
</form>
);
}Prochaines etapes
Maintenant que votre application Next.js est securisee, voici comment aller plus loin :
- Surveillance continue — Configurez Sentry pour les alertes d'erreurs en temps reel
- Audit regulier — Lancez
npm auditdans votre pipeline CI/CD a chaque push - Tests de penetration — Utilisez OWASP ZAP pour des scans automatises reguliers
- Formation equipe — Partagez les pratiques OWASP avec votre equipe de developpement
- Bug bounty — Pour les projets critiques, envisagez un programme de bug bounty
Tutoriels complementaires sur noqta.tn
- Deployer Next.js avec Docker et CI/CD — Securisez aussi votre infrastructure de deploiement
- Construire votre premier serveur MCP — Appliquez ces pratiques de securite a vos serveurs MCP
Conclusion
La securite web n'est pas un etat final — c'est un processus continu. Ce guide vous a donne les fondations pour securiser une application Next.js moderne en couvrant les 10 principales vulnerabilites OWASP :
- Les en-tetes HTTP protegent contre les attaques cote navigateur
- La CSP avec nonce bloque les scripts malveillants meme en cas de faille XSS
- NextAuth.js v5 fournit une authentification securisee prete pour la production
- Zod assure que chaque donnee entrante est validee cote serveur
- Le rate limiting empeche les abus et le brute force
- La protection CSRF previent les requetes falsifiees
- Les variables d'environnement validees empechent les fuites de secrets
Le cout de la securite est toujours inferieur au cout d'une faille. Prenez le temps de mettre en place ces protections des le debut de votre projet — votre futur vous en remerciera.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.