La validation des entrées est la première ligne de défense applicative et la contre-mesure la plus citée dans l'OWASP Input Validation Cheat Sheet. Elle consiste à vérifier, avant tout traitement, que chaque donnée reçue respecte un schéma strict défini par l'application : type, longueur, format, jeu de caractères, plage de valeurs. Les bonnes pratiques 2026 tiennent en cinq principes : allowlist plutôt que denylist, validation exclusivement serveur, parsing strict via schémas (Zod, Pydantic, Joi, JSON Schema), normalisation Unicode NFKC systématique, et défense en profondeur (validation + échappement selon contexte). Bien appliquées, ces règles ferment la porte à la plupart des injections (A03 OWASP), aux désérialisations dangereuses (A08), à certaines escalades d'accès (A01), aux SSRF (A10) et aux uploads malveillants. Cet article détaille les principes, les librairies par langage, les patterns par type de donnée, et les pièges historiques à éviter.
Pourquoi la validation des entrées est centrale
La majorité des catégories du OWASP Top 10 trouvent leur racine dans une validation absente, partielle ou contournée. Un tableau de correspondance rapide :
| Catégorie OWASP Top 10 | Faille type validation |
|---|---|
| A01 Broken Access Control | IDs objet non validés contre propriété |
| A03 Injection | SQL, NoSQL, command, LDAP, template : entrée non contrôlée |
| A04 Insecure Design | Workflow contourné via paramètres non vérifiés |
| A05 Security Misconfiguration | Paramètres de config exposés et modifiables |
| A08 Data Integrity Failures | Désérialisation d'objets attaquant |
| A10 SSRF | URLs utilisateur non restreintes |
La validation n'est donc pas un contrôle mineur : c'est un contrôle transverse qui réduit la surface d'attaque de 40 à 60 % selon les études AppSec 2024 de Snyk et Veracode.
Les cinq principes fondamentaux
Principe 1 - Allowlist par défaut
Définir ce qui est autorisé et rejeter tout le reste. Jamais l'inverse.
Denylist (mauvais)
Bannir ["<script>", "alert(", "javascript:", "../", "DROP TABLE"]
→ un attaquant trouvera toujours une forme non listée
(encodage, Unicode, variante syntaxique, obfuscation)
Allowlist (bon)
Username : [a-z0-9_]{3,32}
→ toute forme non conforme est rejetée, quelle qu'en soit la causePrincipe 2 - Validation exclusivement serveur
La validation client améliore l'UX (feedback instantané, pas d'aller-retour réseau) mais ne constitue jamais une mesure de sécurité. Désactiver JavaScript, intercepter via Burp, ou appeler l'API directement avec curl contourne toute validation front en quelques secondes.
Principe 3 - Parsing strict via schéma
Utiliser une librairie de validation de schéma plutôt qu'un assemblage de vérifications ad hoc. Bénéfices : un point unique de spécification, typage automatique (TypeScript/Python), message d'erreur consistant, facilité d'audit.
Principe 4 - Normalisation systématique
Avant toute comparaison, stockage ou échappement : normaliser l'encodage (UTF-8), appliquer Unicode NFKC, trimmer les espaces, décoder une seule fois les séquences URL-encoded. Les attaques les plus subtiles exploitent les décalages entre étapes.
Principe 5 - Défense en profondeur
Validation en entrée + échappement contextuel en sortie + requêtes paramétrées en DB. Jamais un seul contrôle. Exemple : une validation stricte ne dispense pas d'utiliser des prepared statements SQL.
Librairies de validation 2026 par langage
TypeScript / JavaScript
| Librairie | Type | Points forts | Quand choisir |
|---|---|---|---|
| Zod | TypeScript-first | Inférence de types, API moderne, léger | Projet TypeScript, API REST moderne |
| Joi | Plain JS | Très riche, mature, écosystème | Node.js enterprise, payloads complexes |
| Yup | Plain JS + TS | Léger, partage Formik / React Hook Form | App React avec forms frontaux |
| class-validator | Décorateurs | Intégration NestJS | Projet NestJS |
| ajv | JSON Schema | Standard JSON Schema, performant | API avec spec JSON Schema |
| valibot | TypeScript | Ultra-léger (alternative Zod) | Bundle-size critique (edge, browser) |
// Exemple Zod - schéma API de création d'utilisateur
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email().max(254).toLowerCase(),
username: z.string().regex(/^[a-z0-9_]{3,32}$/),
password: z.string().min(12).max(128),
birth_year: z.number().int().min(1900).max(2026),
role: z.enum(['user', 'editor']).default('user'),
});
type CreateUserInput = z.infer<typeof CreateUserSchema>;
// Express handler
app.post('/api/users', async (req, res) => {
const parsed = CreateUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.issues });
}
const user = await createUser(parsed.data);
res.json(user);
});Python
| Librairie | Type | Points forts |
|---|---|---|
| Pydantic v2 | Modèles typés | Performance (Rust), TypeScript-like |
| marshmallow | Serialization | Mature, orienté API |
| attrs + cattrs | Dataclasses | Léger, alternative Pydantic |
| jsonschema | JSON Schema | Standard |
| Cerberus | Dict-based | Config files validation |
# Exemple Pydantic v2 - validation stricte
from pydantic import BaseModel, EmailStr, Field, field_validator
import unicodedata
from typing import Literal
class CreateUser(BaseModel):
email: EmailStr
username: str = Field(pattern=r'^[a-z0-9_]{3,32}$')
password: str = Field(min_length=12, max_length=128)
birth_year: int = Field(ge=1900, le=2026)
role: Literal['user', 'editor'] = 'user'
@field_validator('username', 'email')
@classmethod
def normalize_unicode(cls, v: str) -> str:
return unicodedata.normalize('NFKC', v)
# FastAPI
@app.post('/api/users')
def create_user(payload: CreateUser):
return user_service.create(payload)Java
| Librairie | Type | Points forts |
|---|---|---|
| Bean Validation (Jakarta) | Annotations | Standard Java EE, Spring-compatible |
| Hibernate Validator | Impl. Bean Validation | Référence |
| Spring Validator | Spring Framework | Intégration native |
// Bean Validation avec Spring Boot
public record CreateUserRequest(
@Email @Size(max = 254) String email,
@Pattern(regexp = "^[a-z0-9_]{3,32}$") String username,
@Size(min = 12, max = 128) String password,
@Min(1900) @Max(2026) int birthYear,
@NotNull Role role
) {}
@PostMapping("/api/users")
public User create(@Valid @RequestBody CreateUserRequest req) {
return userService.create(req);
}Go
Go n'a pas de standard unique. Les choix 2026 :
// validator/v10 - le plus utilisé
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email,max=254"`
Username string `json:"username" validate:"required,min=3,max=32,alphanum"`
Password string `json:"password" validate:"required,min=12,max=128"`
BirthYear int `json:"birth_year" validate:"required,gte=1900,lte=2026"`
Role string `json:"role" validate:"required,oneof=user editor"`
}Alternatives : ozzo-validation pour un style fluent, go-playground/validator pour les annotations struct.
Validation par type de donnée
Strings
Longueur : min et max toujours définis (éviter DoS par payload géant)
Jeu charset : whitelist regex ([a-zA-Z0-9_-] si possible)
Normaliser : NFKC avant comparaison ou stockage
Trimmer : espaces en début et fin
Refuser : null bytes \0, CRLF si hors texte libre, contrôles C0Emails
Règle : utiliser une lib dédiée (Zod .email(), Pydantic EmailStr,
Apache Commons EmailValidator Java)
Pas de regex maison - RFC 5322 fait 6+ pages, les regex naïves
échouent sur les cas valides ou acceptent des cas dangereux.
Limite max 254 caractères (RFC 5321).
Normaliser en lowercase pour comparaison.URLs
Utiliser le parseur natif (URL en JS, urllib en Python, java.net.URI)
Après parsing :
- Whitelister les schemes autorisés (http, https uniquement)
- Vérifier le host contre une allowlist si usage SSRF-sensible
- Refuser les IP privées et métadonnées cloud
(169.254.169.254, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8)
- Refuser les redirections ouvertes non contrôlées
Bibliothèques dédiées anti-SSRF :
- safe-url (Node)
- defusedxml + ipaddress (Python)
- Apache HttpClient RedirectStrategy custom (Java)Nombres et dates
Entiers :
- Type int explicite (pas de parseFloat)
- Plage [min, max] explicite
- Refuser NaN, Infinity, notation scientifique sauf besoin
Dates :
- Parser avec lib ISO 8601 stricte (pas de strptime permissif)
- Timezone explicite
- Plage raisonnable ([1900-01-01, 2100-12-31])JSON et objets imbriqués
Utiliser schéma imbriqué (Zod .object(), Pydantic modèles imbriqués)
Limite profondeur (éviter JSON bomb : 10 niveaux max typique)
Limite taille brute (Content-Length max 1 MB typique pour une API)
Rejeter propriétés inconnues (strict: true ou .strict() en Zod)Fichiers uploadés
Six contrôles cumulatifs obligatoires en 2026 :
- Taille maximum côté serveur (
client_max_body_sizenginx,MultipartResolver.maxUploadSizeSpring). - Extension whitelistée : jamais de denylist (
.exe,.phpetc. contournables). - Type MIME vérifié via magic bytes, pas via header
Content-Typefourni par le client. - Nom de fichier assaini : bannir
..,/,\, null bytes, caractères Unicode ambigus. Régénérer un UUID et garder l'original en base. - Stockage hors du webroot ou dans un bucket isolé sans exécution possible.
- Scan antivirus si le fichier est redistribué (ClamAV, Cloudmersive, VirusTotal API).
# Python - validation magic bytes + UUID storage
import magic
from pathlib import Path
from uuid import uuid4
ALLOWED_MIMES = {'image/png', 'image/jpeg', 'application/pdf'}
MAX_SIZE = 10 * 1024 * 1024 # 10 MB
def store_upload(file_bytes: bytes, original_name: str) -> str:
if len(file_bytes) > MAX_SIZE:
raise ValueError("fichier trop volumineux")
mime = magic.from_buffer(file_bytes, mime=True)
if mime not in ALLOWED_MIMES:
raise ValueError(f"type {mime} non autorisé")
ext = Path(original_name).suffix.lower()
if ext not in {'.png', '.jpg', '.jpeg', '.pdf'}:
raise ValueError("extension non autorisée")
stored_name = f"{uuid4().hex}{ext}"
(Path("/srv/uploads") / stored_name).write_bytes(file_bytes)
return stored_namePièges historiques à éviter
Double décodage
Décoder deux fois une entrée URL-encoded contourne une validation qui tourne après le premier décodage seulement.
Entrée brute : %252e%252e%252f
1er décodage : %2e%2e%2f
2nd décodage : ../
Si validation rejetait "../" après 1er décodage mais que l'app
consomme après 2nd décodage, le payload passe.
Règle : décoder une seule fois, rejeter tout payload qui contient
encore des séquences encodées après décodage.Normalisation Unicode et homoglyphes
admin : a d m i n (ASCII standard)
ADMIN : majuscule, peut être normalisé lower
admın : ı turc (U+0131) visuellement proche
𝐚𝐝𝐦𝐢𝐧 : math bold Unicode (U+1D41A...)
ⓐⓓⓜⓘⓝ : cercles (U+24D0...)
Sans NFKC + lower strict, un attaquant peut créer un username
visuellement identique à un existant.Path traversal et null bytes
Attaques :
filename=../../../etc/passwd
filename=..%2f..%2fetc%2fpasswd
filename=legit.jpg\0.php (null byte truncation en C)
filename=legit.jpg::$DATA (Windows ADS)
Règles :
Régénérer un UUID pour le nom stocké.
Bannir les noms contenant ".." après décodage.
Utiliser os.path.realpath / Path.resolve() pour vérifier
que le chemin final est dans le répertoire autorisé.JSON type confusion
{"user_id": "42"} vs {"user_id": 42}
Une validation laxiste accepte les deux puis compare faiblement
côté code. En PHP, "0e1234" est parfois égal à 0 en comparaison
lâche. Toujours valider le type précisément et comparer en strict.TOCTOU (Time-of-Check to Time-of-Use)
1. Le code vérifie que le fichier n'existe pas.
2. L'attaquant crée un symlink avant que le code l'écrive.
3. Le code écrit dans la destination du symlink.
Règles :
Utiliser les syscalls atomiques (O_CREAT | O_EXCL).
Ne pas se baser sur un path string après check.Gestion des erreurs de validation
Règles 2026 :
Ne jamais exposer en réponse :
- La trace d'exception Python/Java brute
- Le nom des champs internes (ex. mot-clé SQL)
- Le chemin système du serveur
Exposer de manière contrôlée :
- Code HTTP 400 Bad Request (jamais 500 pour validation)
- Structure stable { "errors": [ { "field": "email", "code": "invalid_format" } ] }
- i18n possible côté client via codes
Logger côté serveur :
- L'entrée brute anonymisée (pas de mot de passe ni PII sensible)
- L'user agent, l'IP source, la timestamp
- Le code d'erreur retournéChecklist développeur avant merge
Pour chaque PR qui ajoute un endpoint ou une fonction consommant des entrées utilisateur :
- Schéma de validation défini via librairie (Zod, Pydantic, Joi, Bean Validation) ?
- Longueur max définie pour chaque string ?
- Allowlist regex ou enum pour tous les champs avec format contraint ?
- Nombres bornés (min, max) et typés explicitement ?
- Propriétés inconnues rejetées (strict mode) ?
- Normalisation Unicode NFKC avant stockage ou comparaison ?
- Si fichier uploadé : taille, MIME via magic bytes, extension whitelistée, UUID, stockage hors webroot ?
- Si URL : scheme whitelisté, host contrôlé si usage SSRF-sensible ?
- Erreurs de validation retournent 400 avec structure stable, pas de stacktrace ?
- Tests unitaires : cas valides + invalides + cas limites ?
Points clés à retenir
- Allowlist par défaut : définir ce qui est autorisé plutôt que de bannir ce qui est dangereux. OWASP Input Validation Cheat Sheet est catégorique.
- Validation exclusivement côté serveur. La validation client améliore l'UX mais ne sécurise rien.
- Utiliser une librairie de schéma : Zod (TS), Pydantic v2 (Python), Joi (Node), Bean Validation (Java), validator/v10 (Go). Plus robuste et auditable qu'un assemblage ad hoc.
- Normaliser systématiquement : NFKC Unicode, lowercase pour comparaison, trim, décodage une seule fois.
- Défense en profondeur : validation en entrée + échappement contextuel en sortie + requêtes paramétrées. Ne jamais compter sur un seul contrôle.
- Fichiers uploadés : 6 contrôles cumulatifs (taille, extension whitelistée, MIME via magic bytes, nom assaini, UUID, stockage isolé, scan AV si redistribué).
- Erreurs de validation : HTTP 400 avec structure stable, jamais de stacktrace exposée, logs anonymisés et échappés.
Pour resituer la validation dans l'écosystème OWASP complet, voir introduction au OWASP Top 10 et l'analyse spécifique Broken Access Control : explication, exemples et prévention où la validation des IDs d'objet est centrale. Pour comprendre l'enjeu carrière et business d'une discipline de validation rigoureuse, importance du OWASP Top 10 pour les développeurs détaille le ROI mesuré.





