Module B03

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);
});
⚠️ La validation côté client (HTML5 required, pattern, JS form validation) ne protège pas le serveur — elle peut être complètement contournée avec curl ou Postman.

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() });
}
AspectJoiZod
TypeScriptSupport manuel (.d.ts)Natif — inférence automatique
SyntaxeDSL fluide, maturePlus concis, TypeScript idiomatique
EcosystèmeTrès étendu (hapi, Express)Croissant, excellent avec React/Next
Taille bundle~30kb min~14kb min
Usage recommandéProjets JS ou Express legacyProjets 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])
⚠️ Sanitiser APRÈS avoir validé. D'abord rejeter les données qui n'ont pas la bonne forme, puis transformer celles acceptées.

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),
});