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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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 :

  1. En-tetes HTTP — Protection contre le clickjacking, le MIME sniffing et les injections
  2. Content Security Policy — Controle strict des ressources chargees par le navigateur
  3. Authentification — Connexion securisee avec sessions protegees
  4. Validation des donnees — Schema de validation cote serveur pour chaque entree utilisateur
  5. Rate limiting — Protection contre le brute force et le DDoS au niveau applicatif
  6. Protection CSRF — Jetons anti-falsification pour les mutations
  7. 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-teteProtection
Strict-Transport-SecurityForce HTTPS pendant 2 ans, meme si l'utilisateur tape http://
X-Frame-OptionsEmpeche l'integration de votre site dans une iframe (anti-clickjacking)
X-Content-Type-OptionsEmpeche le navigateur de deviner le type MIME
Referrer-PolicyLimite les informations envoyees dans l'en-tete Referer
Permissions-PolicyDesactive 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.com

Vous 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

DirectiveRoleValeur recommandee
default-srcPolitique par defaut pour toutes les ressources'self'
script-srcSources autorisees pour JavaScript'self' 'nonce-xxx' 'strict-dynamic'
style-srcSources autorisees pour CSS'self' 'nonce-xxx'
img-srcSources autorisees pour les images'self' blob: data: https:
object-srcBloque les plugins (Flash, Java)'none'
frame-ancestorsQui peut integrer votre page en iframe'none'
form-actionOu les formulaires peuvent soumettre'self'
upgrade-insecure-requestsForce 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@beta

Configuration 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 zod

Creer 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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#x27;");
}

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

VariablePrefix NEXT_PUBLIC_ ?Raison
Cle API secreteNONVisible dans le bundle client
URL de base de donneesNONAcces direct a la BDD
Secret JWT/AuthNONPermet de falsifier des tokens
Cle Stripe secreteNONTransactions frauduleuses
URL de l'appOUIInformation publique
Cle Stripe publiqueOUIConcue 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:3000

Etape 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 OWASPProtection Next.jsEtape
A01Broken Access ControlMiddleware auth + verification dans Server Actions3, 8
A02Cryptographic Failuresbcrypt, HTTPS (HSTS), secrets en env serveur3, 7
A03Injection (SQL, XSS)Zod + ORM parametrise + CSP2, 4, 9
A04Insecure DesignValidation a chaque couche, principe du moindre privilege4, 8
A05Security MisconfigurationEn-tetes HTTP, CSP, env validation1, 2, 7
A06Vulnerable Componentsnpm audit, Dependabot, mises a jour regulieresCi-dessous
A07Auth & Session FailuresNextAuth.js v5, JWT securise, rate limiting3, 5
A08Data Integrity FailuresValidation Zod cote serveur, CSRF protection4, 6
A09Logging & MonitoringLogs structures, alertes sur erreurs 4xx/5xxCi-dessous
A10Server-Side Request ForgeryValidation des URLs, liste blanche de domainesCi-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 automatiques

A09 : 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

OutilTypeUsage
OWASP ZAPScanner gratuitTests automatises de vulnerabilites
Burp Suite CommunityProxy d'interceptionTests manuels approfondis
securityheaders.comEn ligneVerification des en-tetes HTTP
Mozilla ObservatoryEn ligneAudit de securite complet
npm auditCLIVulnerabilites des dependances
SnykSaaSSurveillance 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 audit dans 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


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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Créez votre propre interpréteur de code avec génération dynamique d'outils.

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.

30 min read·