12.3 — Sécurité backend
🎯 Objectif : protéger ton backend contre les attaques qui n’ont rien à voir avec le navigateur — injections, manipulation d’IDs, fuite de secrets, brute-force.
À l'issue de cet axe, tu sauras :
- Prévenir SQL/NoSQL/Command injection
- Implémenter une vérification d'autorisation contre IDOR
- Hasher un mot de passe avec argon2id
- Stocker des secrets dans un secret manager
- Rate-limiter les endpoints sensibles
- Bloquer les SSRF en filtrant les URLs
Confirmé
Injections — la triple menace
Section intitulée « Injections — la triple menace »SQL Injection (déjà vu axe 12.1)
Section intitulée « SQL Injection (déjà vu axe 12.1) »// ❌db.query(`SELECT * FROM users WHERE email = '${email}'`);
// ✅db.query('SELECT * FROM users WHERE email = $1', [email]);ORMs (Prisma, Drizzle, SQLAlchemy, Eloquent, Doctrine) paramétrisent automatiquement — c’est une raison majeure de les utiliser.
NoSQL Injection (MongoDB, Firestore)
Section intitulée « NoSQL Injection (MongoDB, Firestore) »// ❌db.users.find({ email: req.body.email, password: req.body.password });// Si body.password = { $ne: null } → MongoDB matche tout password différent de null// ✅db.users.find({ email: String(req.body.email), password: String(req.body.password) });// Plus mieux : valider avec Zodconst { email, password } = LoginSchema.parse(req.body);Command Injection
Section intitulée « Command Injection »// ❌import { exec } from 'node:child_process';exec(`convert ${userFilename} output.png`);// Si filename = "input.jpg; rm -rf /"// ✅ execFile + arguments séparés (jamais concaténés en string)import { execFile } from 'node:child_process';execFile('convert', [userFilename, 'output.png']);Règle absolue : NEVER construire un shell command avec interpolation de string. Toujours execFile / spawn avec array d’arguments.
IDOR — Insecure Direct Object Reference
Section intitulée « IDOR — Insecure Direct Object Reference »L’attaque la plus simple à tester : changer un ID dans l’URL et voir si on accède aux données d’un autre.
// ❌app.get('/orders/:id', async (req, res) => { const order = await db.order.findById(req.params.id); res.json(order);});Tu peux tester GET /orders/1, /orders/2, /orders/3 et tout récupérer.
Prévention
Section intitulée « Prévention »Niveau 1 — Vérif applicative
Section intitulée « Niveau 1 — Vérif applicative »// ✅app.get('/orders/:id', requireAuth, async (req, res) => { const order = await db.order.findOne({ where: { id: req.params.id, userId: req.user.id }, }); if (!order) return res.status(404).json({ error: 'Not found' }); res.json(order);});Pourquoi 404 et pas 403 : 403 confirme que la ressource existe → l’attaquant peut énumérer.
Niveau 2 — Defense in depth avec RLS
Section intitulée « Niveau 2 — Defense in depth avec RLS »Même si tu oublies le check applicatif, PostgreSQL RLS empêche l’accès :
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;CREATE POLICY user_sees_own_orders ON orders FOR SELECT USING (user_id = auth.uid());Niveau 3 — IDs non séquentiels
Section intitulée « Niveau 3 — IDs non séquentiels »Utiliser des UUIDs v4 ou slugs random plutôt que des entiers séquentiels rend l’énumération beaucoup plus dure :
/orders/8f3a9c2e-4b1d-... au lieu de /orders/42Pas une protection (juste de l’obscurité), mais défense en profondeur.
Hash de mots de passe — argon2id
Section intitulée « Hash de mots de passe — argon2id »Un hash de mot de passe doit être lent et adapté aux GPU.
Les bons algos en 2026
Section intitulée « Les bons algos en 2026 »| Algo | Avis |
|---|---|
| argon2id | ✅ Recommandation OWASP 2026 |
| bcrypt | ✅ Toujours acceptable (legacy mais éprouvé) |
| scrypt | ✅ Alternative valide |
| PBKDF2 | 🟡 Acceptable mais moins moderne |
Les mauvais (à ne JAMAIS utiliser)
Section intitulée « Les mauvais (à ne JAMAIS utiliser) »| Algo | Pourquoi |
|---|---|
| MD5 | Cassé depuis 2005, hashable à 100 GH/s sur GPU |
| SHA-1 | Cassé en 2017 |
| SHA-256 / SHA-512 | Trop rapide → brute-force massif |
| Plain text | (sans commentaire) |
Implémentation
Section intitulée « Implémentation »import { hash, verify } from '@node-rs/argon2';
const hashed = await hash('myPassword');const valid = await verify(hashed, 'myPassword');from passlib.context import CryptContext
pwd = CryptContext(schemes=['argon2'], deprecated='auto')hashed = pwd.hash('myPassword')valid = pwd.verify('myPassword', hashed)$hashed = password_hash('myPassword', PASSWORD_ARGON2ID);$valid = password_verify('myPassword', $hashed);Laravel / Symfony
Section intitulée « Laravel / Symfony »Configuration du hasher dans config/hashing.php (Laravel) ou security.yaml (Symfony) → automatique partout.
Secrets — gestion sérieuse
Section intitulée « Secrets — gestion sérieuse »Hiérarchie de gravité
Section intitulée « Hiérarchie de gravité »| Secret | Si fuit |
|---|---|
| Service role DB / SUPABASE_SERVICE_ROLE_KEY | Compromission totale de la DB |
| STRIPE_SECRET_KEY | Charges frauduleuses |
| JWT_SECRET | Tous les tokens forgés par l’attaquant |
| Tokens API tiers (OpenAI, Resend, …) | Vol de quota / spam |
| Cookies session | Impersonation utilisateur |
Storage en prod
Section intitulée « Storage en prod »| Plateforme | Outil |
|---|---|
| Vercel / Netlify | Variables d’env du dashboard |
| AWS | Secrets Manager (rotation automatique possible) |
| GCP | Secret Manager |
| Kubernetes | Sealed Secrets, External Secrets Operator |
| Hashicorp Vault | Self-host enterprise |
| Doppler | SaaS, sync vers tous les envs |
En dev local
Section intitulée « En dev local ».env (PAS dans Git).env.example (DANS Git, sans les vraies valeurs).gitignore : .env, .env.*.localSi tu commit un secret par erreur
Section intitulée « Si tu commit un secret par erreur »- RÉVOQUER la clé chez le provider (Stripe, AWS, etc.) — pas négociable.
- Rotation : générer une nouvelle clé.
- Nettoyer l’historique avec
git filter-repo. - Force-push (avec accord équipe).
Outils pour détecter avant : gitleaks, trufflehog, GitHub Secret Scanning (auto sur les repos publics).
Rate limiting — bloquer le brute-force
Section intitulée « Rate limiting — bloquer le brute-force »Sans rate-limit, un attaquant peut tester 1 million de mots de passe en quelques minutes.
Endpoints à protéger
Section intitulée « Endpoints à protéger »/login,/register,/reset-password— par IP- API publique — par token
- Endpoints coûteux (recherche, IA) — par user
Implémentation Node/Hono
Section intitulée « Implémentation Node/Hono »import { rateLimiter } from 'hono-rate-limiter';
app.use('/auth/login', rateLimiter({ windowMs: 15 * 60 * 1000, // 15 min limit: 5, // 5 tentatives par IP message: 'Too many attempts, try again later',}));Implémentation Python/FastAPI
Section intitulée « Implémentation Python/FastAPI »uv add slowapifrom slowapi import Limiterfrom slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.post('/login')@limiter.limit('5/15minutes')async def login(...): ...Implémentation Laravel
Section intitulée « Implémentation Laravel »Route::post('/login', LoginController::class)->middleware('throttle:5,1');// 5 tentatives par minuteAvec Redis pour distributed rate-limit
Section intitulée « Avec Redis pour distributed rate-limit »Pour une app multi-instances, le compteur doit être partagé :
import { Redis } from 'ioredis';const redis = new Redis();
async function checkRateLimit(ip: string): Promise<boolean> { const count = await redis.incr(`rate:login:${ip}`); if (count === 1) await redis.expire(`rate:login:${ip}`, 900); return count <= 5;}Captcha en cas d’abus
Section intitulée « Captcha en cas d’abus »Quand un IP dépasse N tentatives, exige un CAPTCHA (hCaptcha, Cloudflare Turnstile) avant la prochaine.
SSRF — Server-Side Request Forgery
Section intitulée « SSRF — Server-Side Request Forgery »Cas typique : ton serveur fetch une URL fournie par l’utilisateur (génération PDF, preview de lien…).
L’attaque
Section intitulée « L’attaque »// ❌const html = await fetch(req.body.url).then(r => r.text());L’attaquant envoie :
http://169.254.169.254/latest/meta-data→ credentials AWS instance.http://localhost:5432→ exfiltration DB locale.http://internal-api/admin→ accès services internes.
Prévention
Section intitulée « Prévention »import dns from 'node:dns/promises';import net from 'node:net';
async function fetchSafe(url: string) { const parsed = new URL(url);
// 1. Whitelist de protocoles if (!['https:', 'http:'].includes(parsed.protocol)) { throw new Error('Unsupported protocol'); }
// 2. Résoudre le hostname → IP const { address } = await dns.lookup(parsed.hostname);
// 3. Bloquer les IPs privées et localhost if (isPrivateIp(address)) { throw new Error('Internal IPs not allowed'); }
// 4. Fetch avec timeout return fetch(url, { signal: AbortSignal.timeout(5000) });}
function isPrivateIp(ip: string): boolean { if (ip.startsWith('127.') || ip.startsWith('10.') || ip.startsWith('192.168.')) return true; if (ip === '::1' || ip.startsWith('fe80:')) return true; // 172.16.0.0/12 const parts = ip.split('.').map(Number); if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; return false;}Bibliothèques : ssrf-req-filter (Node), équivalents par langage.
Désérialisation non sûre
Section intitulée « Désérialisation non sûre »Particulièrement risqué en PHP (unserialize), Java (ObjectInputStream), Python (pickle).
// ❌$data = unserialize($_POST['data']);// L'attaquant peut envoyer un objet malicieux qui exécute du code à la désérialisation# ❌ pickle est dangereux avec des inputs externesimport pickledata = pickle.loads(request.data)Règle : pour échanger des données, utilise JSON uniquement avec validation de schéma.
Audit log — savoir qui a fait quoi
Section intitulée « Audit log — savoir qui a fait quoi »// Logger les actions sensibleslogger.info({ event: 'user_role_change', actor: req.user.id, target: targetUserId, oldRole, newRole, ip: req.ip, timestamp: new Date(),});Stocker dans une table audit_log ou un système dédié (Datadog, Loki). Ne jamais y mettre password ou token.
Auto-évaluation
Section intitulée « Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- OWASP Cheat Sheet — Authentication — cheatsheetseries.owasp.org
- OWASP Password Storage — cheatsheetseries.owasp.org
- PortSwigger Web Security Academy — labs gratuits
- HackTricks — wiki offensif
- gitleaks — github.com/gitleaks/gitleaks
Suite : 12.4 — Pratiques continues — Dependabot, SAST/DAST, RGPD.