Skip to content
Learni
Voir tous les tutoriels
Sécurité Web

Comment sécuriser une API contre OWASP Top 10 en 2026

Read in English

Introduction

L'OWASP API Security Top 10 (2023) recense les risques de sécurité les plus critiques pour les APIs REST et GraphQL, responsables de 94% des breaches API. En 2026, avec l'explosion des microservices et des apps serverless, ignorer ces vulnérabilités expose votre backend à des attaques comme le vol de données ou les DoS. Ce tutoriel intermédiaire vous guide pas à pas pour sécuriser une API Next.js App Router contre les top risques : Broken Object Level Authorization (BOLA), Broken Authentication, Excessive Data Exposure, Lack of Rate Limiting, Broken Function Level Authorization, et un aperçu des autres.

Nous utilisons un store in-memory simulant une DB pour des exemples 100% fonctionnels et copier-collables. Chaque étape inclut du code vulnérable puis sécurisé, avec explications précises sur les mécanismes (RBAC, JWT, middleware). À la fin, votre API résiste aux scans OWASP ZAP. Idéal pour les devs backend qui veulent passer pro en sécurité API. Temps estimé : 30min pour implémenter.

Prérequis

  • Node.js 20+ installé
  • Connaissances en TypeScript et Next.js 15 (App Router)
  • Éditeur comme VS Code avec extension TypeScript
  • Outils de test : Postman ou curl
  • npm ou yarn pour les dépendances

Initialisation du projet Next.js

terminal
npx create-next-app@latest mon-api-securisee --typescript --app --tailwind --eslint --src-dir --import-alias "@/*"
cd mon-api-securisee
npm install jsonwebtoken @types/jsonwebtoken zod @types/node
npm install -D @types/bun
next dev

Cette commande crée un projet Next.js 15 avec TypeScript et App Router. Nous ajoutons jsonwebtoken pour l'auth JWT, zod pour la validation, et types Node. Lancez next dev pour démarrer sur http://localhost:3000. Piège : Oubliez pas --app pour l'App Router, sinon les API routes legacy ne marchent pas.

Store de données in-memory (simulation DB)

Avant les routes, créons un module DB simulé avec un Map pour users. Chaque user a un id, ownerId (pour BOLA), email, role (admin/user pour function-level auth). Cela mime Prisma sans setup DB externe, parfait pour tester localement.

Création du DB in-memory

src/lib/db.ts
import { NextApiRequest, NextApiResponse } from 'next';

type UserRole = 'admin' | 'user';

export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  ownerId: string;
}

export const users = new Map<string, User>();

// Données de test
users.set('1', { id: '1', name: 'Alice Admin', email: 'alice@ex.com', role: 'admin', ownerId: 'alice' });
users.set('2', { id: '2', name: 'Bob User', email: 'bob@ex.com', role: 'user', ownerId: 'bob' });
users.set('3', { id: '3', name: 'Charlie User', email: 'charlie@ex.com', role: 'user', ownerId: 'eve' });

export function getUserById(id: string): User | undefined {
  return users.get(id);
}

export function getUsersByOwner(ownerId: string): User[] {
  return Array.from(users.values()).filter(u => u.ownerId === ownerId);
}

Ce module définit l'interface User avec ownerId clé pour BOLA. Fonctions getUserById et getUsersByOwner simulent des queries DB. Ajoutez-le dans src/lib/. Piège : Map est global (state partagé), idéal pour démo mais utilisez Redis/Prisma en prod pour persistance.

API1:2023 - Broken Object Level Authorization (BOLA)

BOLA (ex-TOP1) permet d'accéder à des objets non autorisés via ID manipulation (e.g. /users/123 au lieu de ses propres users). 44% des breaches API. Analogie : Comme lire le courrier d'un voisin en changeant l'adresse. Solution : Vérifier ownership par user ID dans chaque GET/PUT/DELETE.

Route vulnérable à BOLA

src/app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getUserById } from '@/lib/db';

export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
  const id = params.id;
  const user = getUserById(id);
  if (!user) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 });
  }
  return NextResponse.json(user);
}

export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
  const id = params.id;
  const user = getUserById(id);
  if (!user) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 });
  }
  // TODO: Supprimer en prod
  return NextResponse.json({ message: 'User deleted' });
}

Cette route vulnérable renvoie/supprime n'importe quel user via ID sans vérifier l'owner. Testez avec curl http://localhost:3000/api/users/2 : tout le monde accède à Bob. Piège : Pas de 405 pour méthodes non autorisées ; ajoutez-les explicitement.

Route sécurisée contre BOLA

src/app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getUserById } from '@/lib/db';
import { getUserIdFromToken } from '@/lib/auth';

export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  const userId = token ? getUserIdFromToken(token) : null;
  const id = params.id;
  const user = getUserById(id);
  if (!user) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 });
  }
  if (user.ownerId !== userId && userId !== 'admin') {
    return NextResponse.json({ error: 'Access denied' }, { status: 403 });
  }
  return NextResponse.json({ ...user, sensitive: '[redacted]' });
}

export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  const userId = token ? getUserIdFromToken(token) : null;
  const id = params.id;
  const user = getUserById(id);
  if (!user || (user.ownerId !== userId && userId !== 'admin')) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }
  // users.delete(id); // En prod
  return NextResponse.json({ message: 'User deleted' });
}

Maintenant, on extrait userId du JWT et vérifie user.ownerId === userId ou admin. Redact sensitive data. Test : JWT bob accède seulement à ses users. Piège : Toujours valider token avant DB query pour éviter timing attacks.

API2:2023 - Broken User Authentication

Auth brisée inclut mots de passe faibles, sessions infinies, ou JWT sans expiry. 94% des apps vulnérables. Solution : JWT avec expiry courte, refresh tokens, et validation stricte. Nous implémentons un login simple.

Middleware auth JWT et login route

src/lib/auth.ts
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET || 'supersecret2026';

export interface JWTPayload {
  userId: string;
  role: string;
  exp: number;
}

export function createToken(userId: string, role: string): string {
  return jwt.sign({ userId, role }, JWT_SECRET, { expiresIn: '15m' });
}

export function getUserIdFromToken(token: string): string | null {
  try {
    const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
    return decoded.userId;
  } catch {
    return null;
  }
}

Fonctions pour signer/vérifier JWT avec expiry 15min. JWT_SECRET en env var. Ajoutez JWT_SECRET=supersecret2026 dans .env.local. Piège : Jamais hardcoded en prod ; utilisez Vault ou AWS Secrets.

Route de login sécurisée

src/app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createToken } from '@/lib/auth';
import { users } from '@/lib/db';
import { z } from 'zod';

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { email, password } = loginSchema.parse(body);
  // Simule check password (en prod: bcrypt compare)
  const user = Array.from(users.values()).find(u => u.email === email);
  if (!user || password !== 'password123') { // Hardcoded pour démo
    return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
  }
  const token = createToken(user.ownerId, user.role);
  return NextResponse.json({ token, user: { id: user.id, role: user.role } });
}

Validation Zod empêche injections. Check password simulé (utilisez bcrypt en prod). Renvoie JWT. Test : POST {email:'bob@ex.com', password:'password123'} → token. Piège : Rate limit login pour brute-force.

API3:2023 - Excessive Data Exposure & API4: Lack of Resources

Excessive Data : APIs renvoient trop (e.g. passwords). Rate Limiting : Pas de quotas → DoS. Solutions : Schemas de réponse minimes + Upstash Redis ou in-memory limiter.

Liste users avec rate limit et schema min

src/app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getUserIdFromToken } from '@/lib/auth';
import { getUsersByOwner } from '@/lib/db';
import { z } from 'zod';

const requests = new Map<string, number>();
const LIMIT = 10;
const WINDOW = 60 * 1000; // 1min

function rateLimit(ip: string): boolean {
  const now = Date.now();
  const key = ip;
  const count = requests.get(key) || 0;
  if (now - (requests.get(key + '_time') || 0) > WINDOW) {
    requests.set(key, 1);
    requests.set(key + '_time', now);
    return true;
  }
  if (count >= LIMIT) return false;
  requests.set(key, count + 1);
  return true;
}

export async function GET(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') || 'anonymous';
  if (!rateLimit(ip)) {
    return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 });
  }
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  const userId = token ? getUserIdFromToken(token) : null;
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  const userList = getUsersByOwner(userId);
  const safeList = userList.map(({ id, name, role }) => ({ id, name, role }));
  return NextResponse.json(safeList);
}

Rate limit in-memory par IP (10 req/min). Schema safe exclut email/ownerId. Seulement users ownés. Test : 11 req rapides → 429. Piège : In-memory pas scalable ; utilisez Upstash Redis en prod.

API5:2023 - Broken Function Level Authorization

Users normaux accèdent à admin features (e.g. /admin/users). Vérifiez RBAC (Role-Based Access Control) à chaque endpoint sensible.

Route admin-only avec RBAC

src/app/api/admin/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getUserIdFromToken } from '@/lib/auth';
import { users } from '@/lib/db';

export async function GET(request: NextRequest) {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  const decoded = token ? getUserIdFromToken(token) : null;
  if (!decoded || decoded !== 'alice') { // Simule admin check via userId ou role
    return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
  }
  const allUsers = Array.from(users.values()).map(({ id, email, role }) => ({ id, email, role }));
  return NextResponse.json(allUsers);
}

Check userId === 'alice' (admin). En prod, query role DB. Piège : RBAC centralisé dans middleware pour éviter duplication.

Bonnes pratiques

  • Toujours valider JWT avant DB : Évite leaks sur erreurs auth.
  • Rate limit global avec Upstash Redis ou Cloudflare.
  • Zod/Valibot pour tous inputs/outputs : Stoppe injections SQL/NoSQL.
  • CORS strict : next.config.js avec allowedOrigins.
  • Logs structurés : Winston + Sentry pour monitorer 403/429.

Erreurs courantes à éviter

  • Oublier expiry JWT : Sessions infinies → token theft.
  • ID séquentiels : Prédictibles pour BOLA ; utilisez UUIDv4.
  • Pas de HTTPS : JWT sniffés en transit.
  • Rate limit par user pas IP : VPN bypass.

Pour aller plus loin

Maîtrisez l'OWASP complet avec nos formations sécurité API Learni. Ressources : OWASP API Top 10, Next.js Auth.js, OWASP ZAP pour scans auto.