23 - Sécurité : les attaques que tu vas subir (et comment t'en protéger)
Ce que tu vas apprendre
- L'input sanitization et pourquoi la validation ne suffit pas
- Les injections SQL et comment les éviter définitivement
- Le mass assignment et le danger des ORM trop permissifs
- Les limites de taille de requête
- Les security headers HTTP
- Le OWASP API Security Top 10
Prerequisites
Avoir lu l'article sur la performance. Avoir une API REST qui accepte des donnees utilisateur (donc toutes les APIs).
Ma première API publique a ete scannee par des bots moins de 24 heures apres sa mise en ligne. Des tentatives d'injection SQL dans les query params, des payloads JSON de 50 Mo pour faire crasher le serveur, des paths comme /../../../etc/passwd. Je n'avais prevu aucune de ces attaques. Quand tu mets une API sur internet, tu invites le monde entier a essayer de la casser. Autant s'y preparer.
Input sanitization
La regle numero un : ne fais jamais confiance aux donnees entrantes. Tout ce qui vient du client est suspect. Meme les headers. Meme le Content-Type.
La validation vérifié que les donnees ont le bon format. La sanitization nettoie les donnees dangereuses. Tu as besoin des deux.
typescriptimport { z } from "zod";
import xss from "xss";
const CreateProductSchema = z.object({
name: z.string()
.min(1)
.max(200)
.transform(val => xss(val)), // Supprime le HTML/JS
description: z.string()
.max(5000)
.transform(val => xss(val)),
price: z.number()
.int()
.positive()
.max(1_000_000_00) // 1M euros max en centimes
});
La validation avec Zod rejette les donnees mal formees. Le transform avec xss nettoie le HTML potentiel. Un <script>alert('xss')</script> dans le nom d'un produit devient du texte inoffensif.
SQL injection
La SQL injection, c'est quand un attaquant inséré du SQL dans tes paramètres :
GET /users?name='; DROP TABLE users; --
Si tu construis ta requête avec de la concatenation de strings, c'est game over :
typescript// DANGER : injection SQL possible
const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;
La solution définitive : les requêtes parametrees. Tous les ORM et tous les query builders les utilisent par défaut :
typescript// Avec un query builder (Drizzle)
const users = await db.select().from(usersTable)
.where(eq(usersTable.name, req.query.name));
// Avec une requete parametree brute
const result = await pool.query(
"SELECT * FROM users WHERE name = $1",
[req.query.name]
);
Le $1 est un placeholder. La base de donnees traite la valeur comme une donnee, jamais comme du SQL. Fin de l'injection. Si tu utilises un ORM comme Drizzle ou Prisma, tu es protégé par défaut. Mais si tu ecris du SQL brut quelque part, utilise toujours des paramètres.
Mass assignment
Le mass assignment, c'est quand tu prends le body JSON du client et tu le passes directement a ta base de donnees :
typescript// DANGER : mass assignment
app.put("/users/:id", async (req, res) => {
await db.update(usersTable).set(req.body).where(eq(usersTable.id, req.params.id));
});
Un attaquant envoie :
json{
"name": "Alice",
"role": "admin",
"email_verified": true
}
Et se retrouve admin avec un email vérifié. La solution : un schema explicite qui n'accepte que les champs autorises.
typescriptconst UpdateUserSchema = z.object({
name: z.string().min(1).max(100).optional(),
email: z.string().email().optional(),
avatar_url: z.string().url().optional()
// Pas de "role", pas de "email_verified"
});
app.put("/users/:id", async (req, res) => {
const data = UpdateUserSchema.parse(req.body);
await db.update(usersTable).set(data).where(eq(usersTable.id, req.params.id));
});
Sur paltemps.fr, chaque endpoint a son schema Zod qui liste explicitement les champs acceptes. Pas d'exception.
Limites de taille
Un body JSON de 100 Mo va consommer toute la mémoire de ton serveur. Configure des limites partout :
typescript// Express
app.use(express.json({ limit: "1mb" }));
// Nginx
// client_max_body_size 5m;
Pour les query strings aussi. Un ?ids=1,2,3,...,100000 peut faire exploser un IN SQL. Limite le nombre d'éléments :
typescriptconst ids = req.query.ids?.split(",");
if (ids && ids.length > 100) {
return res.status(400).json({ detail: "Maximum 100 IDs." });
}
Et les headers. Un header Cookie de 1 Mo, ca existe dans les attaques. Nginx limite ca par défaut (large_client_header_buffers), mais vérifié ta configuration.
HTTPS obligatoire
En 2026, une API sans HTTPS, ca ne devrait plus exister. HTTPS chiffre tout le trafic entre le client et le serveur. Sans HTTPS :
- Les tokens d'authentification transitent en clair
- Un attaquant sur le meme réseau wifi peut lire toutes les requêtes
- Les intermediaires (FAI, proxys) peuvent modifier les réponses
Avec Let's Encrypt, un certificat SSL est gratuit et s'installe en une commande. Pas d'excuse.
Ajoute aussi le header HSTS pour forcer HTTPS :
Strict-Transport-Security: max-age=31536000; includeSubDomains
Security headers
Des headers a ajouter a toutes tes réponses :
typescriptapp.use((req, res, next) => {
res.set("X-Content-Type-Options", "nosniff");
res.set("X-Frame-Options", "DENY");
res.set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
res.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
res.set("X-XSS-Protection", "0"); // Desactive le filtre XSS des vieux navigateurs (cause plus de problemes qu'il n'en resout)
next();
});
En Express, le package helmet fait tout ca en une ligne :
typescriptimport helmet from "helmet";
app.use(helmet());
OWASP API Security Top 10
L'OWASP publie un top 10 des vulnérabilités spécifiques aux APIs. Les plus courantes :
Broken Object Level Authorization : un user accede aux donnees d'un autre user en changeant l'ID dans l'URL. Verifie toujours que le user a le droit d'acceder a la ressource demandee.
Broken Authentication : tokens qui n'expirent pas, pas de rate limit sur le login, tokens stockes en clair.
Broken Object Property Level Authorization : le mass assignment dont on a parle plus haut.
Unrestricted Resource Consumption : pas de rate limiting, pas de limites de taille, pas de pagination.
Broken Function Level Authorization : un user normal qui accede a un endpoint admin parce que tu as oublie le middleware d'autorisation.
typescript// Verifie TOUJOURS l'autorisation
app.delete("/users/:id", requireAuth, requireRole("admin"), async (req, res) => {
// ...
});
La checklist sécurité minimum
- HTTPS partout, avec HSTS
- Requetes parametrees (jamais de concatenation SQL)
- Schemas de validation sur chaque endpoint (Zod, Typebox)
- Rate limiting sur tous les endpoints, renforce sur login/register
- Limites de taille sur body, query string, headers
- Security headers (helmet ou équivalent)
- Verification d'autorisation sur chaque ressource (pas juste l'authentification)
- Tokens avec expiration et rotation
- Logs des tentatives d'acces echouees
Résumé
- Sanitize et valide toutes les entrees avec des schemas explicites (Zod)
- Les requêtes parametrees eliminent les injections SQL définitivement
- Le mass assignment se resout avec des schemas qui listent les champs autorises
- Limite la taille des requêtes a tous les niveaux (body, query, headers)
- HTTPS et security headers sont le minimum absolu
- Consulte le OWASP API Top 10 et vérifié chaque point
Article précédent : Performance Article suivant : Glossaire