TP8 : API Express sécurisée et testée (Zod, Helmet, rate limiting, Vitest)
Une API exposée sur un réseau reçoit n’importe quoi : champs manquants, types inattendus, JSON cassé, scripts, tentatives d’injection SQL, rafales de requêtes. Ce TP part d’une petite API de formulaire de contact volontairement fragile et la rend robuste, puis prouve cette robustesse par des tests automatisés que la CI pourra exécuter.
Fichiers de départ :
api-starter.zip
1. Objectifs pédagogiques
À l’issue du TP, vous serez capable de :
- situer les risques d’une API à l’aide de l’OWASP API Security Top 10 ;
- valider strictement les entrées avec Zod et uniformiser les erreurs avec un middleware d’erreur centralisé ;
- ajouter les headers de sécurité (Helmet), limiter la taille des requêtes et le débit (rate limiting) ;
- écrire des tests de routes avec Vitest + Supertest, y compris des cas négatifs (injection, entrées malformées) ;
- écrire un test d’intégration contre un vrai PostgreSQL lancé avec Docker Compose, et appliquer des migrations.
Les images de cette page sont des schémas explicatifs, pas des captures d’écran.
2. Environnement du TP
-
VM de lab Ubuntu Server 26.04 avec Node.js 24, npm, Docker et Compose (TP7).
-
Copier
api-starter/sur la VM puis :cd api-starter npm install npm test # 2 tests passent, les autres sont marqués "todo"
| Fichier fourni | Rôle |
|---|---|
src/app.js |
Fabrique createApp({ contactRepository }) (injection de dépendance) |
src/routes/contact.js |
POST /api/contact sans validation |
src/schemas/contact.js |
Schéma Zod à compléter |
src/middlewares/validate.js, errorHandler.js |
Middlewares à compléter (l’actuel divulgue la stack trace !) |
src/repositories/contactRepository.js |
Repository PostgreSQL (requêtes paramétrées) et repository en mémoire |
migrations/, src/migrate.js |
Migrations SQL et script qui les applique |
tests/ |
Un test modèle + des it.todo à transformer en tests |
3. Partie 1 : OWASP API Security Top 10
- Lire l’OWASP API Security Top 10 (2023) et compléter le tableau pour cette API :
| Risque OWASP | Concerne notre API ? | Mesure dans ce TP |
|---|---|---|
| API1 Broken Object Level Authorization | ||
| API2 Broken Authentication | ||
| API3 Broken Object Property Level Authorization | .strict() refuse les champs en trop (mass assignment) |
|
| API4 Unrestricted Resource Consumption | rate limiting, taille max du corps | |
| API8 Security Misconfiguration | Helmet, pas de stack trace, pas de x-powered-by |
|
| … |
- Démarrer l’API (partie 5 pour la base) ou la lire, puis lister trois attaques que la version actuelle laisse passer.
4. Partie 2 : Validation et gestion des erreurs
-
Compléter
src/schemas/contact.js:name(trim, 2–100),email(format email, ≤ 254),message(10–2000),.strict(). -
Compléter
src/middlewares/validate.js: en cas d’échec, répondre 400 avec un corps du type :{ "error": "VALIDATION_ERROR", "details": [{ "field": "email", "message": "Invalid email" }] } -
Brancher
validate(contactSchema)danssrc/routes/contact.js. -
Réécrire
src/middlewares/errorHandler.js:INVALID_JSON(400),PAYLOAD_TOO_LARGE(413),INTERNAL_ERROR(500), jamais de stack trace côté client (on la journalise côté serveur). -
Ajouter une route 404 JSON (
{ "error": "NOT_FOUND" }) avant le middleware d’erreur.
5. Partie 3 : Durcissement HTTP
-
app.disable("x-powered-by")puisapp.use(helmet()). Comparer les headers avant/après :curl -I http://localhost:4000/api/health -
Limiter le corps JSON :
express.json({ limit: "10kb" }). -
Ajouter
express-rate-limituniquement sur/api/contact(par exemple 5 requêtes par minute et par IP, headersRateLimit-*standard).for i in $(seq 1 7); do curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:4000/api/contact \ -H 'Content-Type: application/json' \ -d '{"name":"Ada","email":"[email protected]","message":"Bonjour le monde"}' doneDerrière un reverse proxy (Nginx, Ingress), pensez à
app.set("trust proxy", 1), sinon toutes les requêtes semblent venir de la même IP.
6. Partie 4 : Tests de routes et cas négatifs
| Outil | Rôle |
|---|---|
| Vitest | Lanceur de tests et assertions (describe, it, expect), compatible avec l’API de Jest. |
| Supertest | Envoie des requêtes HTTP à l’application Express sans ouvrir de port. |
| Repository en mémoire | Remplace PostgreSQL dans les tests de routes : rapides et sans dépendance. |
-
Transformer chaque
it.tododetests/contact.test.jsen test réel. Exemple de requête mal formée :const res = await request(app) .post("/api/contact") .set("Content-Type", "application/json") .send('{"name": "Ada",'); // JSON tronqué expect(res.status).toBe(400); expect(res.body.error).toBe("INVALID_JSON"); -
Pour le test « erreur 500 », injecter un repository dont
create()lève une exception, et vérifier que la réponse ne contient nistackni le message d’origine. -
Mesurer la couverture :
npm run test:coverage
7. Partie 5 : Intégration avec PostgreSQL et migrations
-
Démarrer la base et appliquer les migrations :
cp .env.example .env # puis changer le mot de passe set -a; . ./.env; set +a docker compose up -d --wait npm run migrate # applique migrations/001_create_contacts.sql npm run migrate # 2e exécution : rien à faire (idempotent) -
Ajouter une migration
002_add_contacts_email_index.sql(index suremail) et l’appliquer. -
Compléter
tests/integration/contact.int.test.jspuis lancer :npm run test:integration -
Démarrer l’API (
npm start) et tester à la main aveccurl: une requête valide, une invalide, une injection.
Défi
- Écrire un workflow GitHub Actions qui lance
npm test, puisnpm run test:integrationavec un service containerpostgres:17-alpine.- Ajouter une route
GET /api/contact(liste paginée) avec validation des paramètres?page=&limit=par Zod (z.coerce.number()), limitée à 50 éléments par page (API4).
8. Livrables
Captures à déposer dans captures/ :
| Fichier | Contenu attendu |
|---|---|
validation-400.png |
Requête invalide et réponse VALIDATION_ERROR détaillée |
helmet-headers.png |
curl -I avant / après Helmet |
rate-limit-429.png |
Boucle curl se terminant par des 429 |
tests-ok.png |
npm test : tous les tests passent, plus aucun todo |
coverage.png |
Rapport de couverture |
integration-ok.png |
npm run test:integration contre PostgreSQL |
ci-green.png |
(défi) workflow GitHub Actions réussi |
Ainsi que le code complété et le tableau OWASP rempli.