Validation des entrées
Joi, Zod, sanitisation — la règle d'or : ne jamais faire confiance aux données entrantes.
01 Validate everything, trust nothing
Toute donnée provenant de l'extérieur doit être considérée comme hostile : req.body, req.query, req.params, headers, cookies. Une seule donnée non validée peut ouvrir une faille d'injection SQL, XSS, path traversal, ou buffer overflow.
// ❌ Vulnérable — confiance aveugle en req.body app.post('/users', async (req, res) => { const user = await db.create(req.body); // n'importe quel champ peut passer res.json(user); }); // ✅ Sécurisé — validation explicite app.post('/users', async (req, res) => { const { error, value } = userSchema.validate(req.body, { abortEarly: false }); if (error) return res.status(400).json({ errors: error.details.map(d => d.message) }); const user = await db.create(value); // 'value' = données filtrées et validées res.json(user); });
02 Joi — schémas de validation
Joi est la bibliothèque de validation la plus populaire pour Node.js. Elle permet de décrire la forme attendue des données avec un DSL fluide.
const Joi = require('joi'); // Schéma d'inscription const registerSchema = Joi.object({ username: Joi.string().alphanum().min(3).max(30).required(), email: Joi.string().email().required(), password: Joi.string().min(8).max(128) .pattern(/[A-Z]/, 'uppercase') .pattern(/[0-9]/, 'number') .required(), age: Joi.number().integer().min(13).max(120), role: Joi.string().valid('user', 'editor').default('user'), // pas 'admin' ! }); // Middleware de validation Joi const validate = (schema) => (req, res, next) => { const { error, value } = schema.validate(req.body, { abortEarly: false, stripUnknown: true }); if (error) return res.status(400).json({ errors: error.details.map(d => d.message) }); req.body = value; // Remplace par les données filtrées next(); }; app.post('/register', validate(registerSchema), registerHandler);
03 Zod — validation TypeScript-first
Zod permet de définir des schémas de validation ET d'inférer automatiquement les types TypeScript depuis ces schémas — une seule source de vérité.
import { z } from 'zod'; const RegisterSchema = z.object({ username: z.string().min(3).max(30).regex(/^[a-z0-9]+$/i), email: z.string().email(), password: z.string().min(8).refine(p => /[A-Z]/.test(p), 'Majuscule requise'), age: z.number().int().min(13).optional(), }); // Type inféré automatiquement : type RegisterInput = z.infer<typeof RegisterSchema>; // { username: string; email: string; password: string; age?: number } // Parsing (lance une exception si invalide) const data = RegisterSchema.parse(req.body); // Parsing sécurisé (retourne { success, data, error }) const result = RegisterSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ errors: result.error.flatten() }); }
| Aspect | Joi | Zod |
|---|---|---|
| TypeScript | Support manuel (.d.ts) | Natif — inférence automatique |
| Syntaxe | DSL fluide, mature | Plus concis, TypeScript idiomatique |
| Ecosystème | Très étendu (hapi, Express) | Croissant, excellent avec React/Next |
| Taille bundle | ~30kb min | ~14kb min |
| Usage recommandé | Projets JS ou Express legacy | Projets TS, frameworks modernes |
04 Sanitisation vs Validation
Validation : vérifier si les données respectent les règles (format, type, plage). Rejette ou accepte.
Sanitisation : transformer les données pour supprimer/neutraliser les éléments dangereux.
// Sanitisation HTML — empêche XSS stocké const DOMPurify = require('isomorphic-dompurify'); function sanitizeHTML(str) { return DOMPurify.sanitize(str, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }); } // Sanitisation SQL — en complément de paramètres préparés function sanitizeString(str) { return String(str) .replace(/[<>"'`]/g, '') // caractères HTML .replace(/[;--]/g, '') // injection SQL de base .trim() .slice(0, 255); // longueur max } // ⚠️ La sanitisation SQL ne remplace pas les requêtes préparées ! // Toujours : db.query('SELECT * FROM users WHERE id = ?', [id])
05 Validation côté client vs côté serveur
La validation côté client (HTML5, JavaScript) améliore l'UX mais n'offre aucune garantie de sécurité. Tout utilisateur peut modifier le DOM, désactiver JavaScript, ou envoyer des requêtes directement avec curl.
// Côté client (UX seulement — contournable) <input type="email" required maxlength="100" /> // Côté serveur (sécurité réelle — obligatoire) const emailSchema = Joi.string().email().max(100).required(); // Validation de params URL (injection via /users/../../etc/passwd) const idSchema = Joi.string().uuid().required(); // ou const idSchema = Joi.number().integer().positive().required(); // Validation des query params const searchSchema = Joi.object({ q: Joi.string().max(200).pattern(/^[a-zA-Z0-9\s\-_]+$/).optional(), page: Joi.number().integer().min(1).default(1), limit: Joi.number().integer().min(1).max(100).default(20), });