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
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 devCette 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
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
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
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
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
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
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
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.jsavecallowedOrigins. - 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.