Back

Gestion sécurisée des entrées utilisateur dans Node.js

Gestion sécurisée des entrées utilisateur dans Node.js

Chaque API Node.js reçoit des données auxquelles elle ne peut pas faire entièrement confiance. Les corps JSON, les chaînes de requête, les champs de formulaire et les téléchargements de fichiers proviennent tous du monde extérieur, et chacun d’entre eux peut transporter des charges malveillantes. L’injection SQL, l’injection NoSQL, l’injection de commandes, la pollution de prototype — ces attaques partagent un point d’entrée commun : des entrées utilisateur non validées qui atteignent la logique métier, une base de données ou une commande shell. Cet article présente les modèles pratiques qui les empêchent.

Points clés à retenir

  • Validez toujours les données entrantes par rapport à un schéma strict avant qu’elles n’atteignent votre logique applicative — définissez ce qui est autorisé et rejetez tout le reste.
  • Utilisez des requêtes paramétrées pour SQL et imposez des types de données stricts pour NoSQL afin de prévenir les attaques par injection.
  • Évitez child_process.exec() pour les commandes système ; utilisez plutôt execFile() ou spawn() avec des tableaux d’arguments explicites.
  • Ne propagez jamais les corps de requête bruts sur vos modèles ; sélectionnez uniquement les champs attendus pour prévenir l’assignation de masse et la pollution de prototype.
  • Définissez des limites de taille de corps et validez les téléchargements de fichiers au niveau de l’analyseur pour vous protéger contre les attaques par déni de service.

Valider d’abord, traiter ensuite

La règle la plus efficace dans la gestion sécurisée des requêtes dans les API Node.js est : ne laissez jamais les données de requête brutes toucher votre logique applicative. Validez la forme et le type de chaque valeur entrante avant de l’utiliser.

La validation basée sur un schéma est l’approche la plus claire. Des bibliothèques comme Zod et Joi vous permettent de déclarer exactement à quoi une requête devrait ressembler et de rejeter tout ce qui ne correspond pas.

import { z } from "zod"

const RegisterSchema = z.object({
  username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  password: z.string().min(10),
})

app.post("/register", (req, res) => {
  const result = RegisterSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() })
  }
  // result.data est maintenant sûr à utiliser
})

Il s’agit d’une approche par liste blanche : vous définissez ce qui est autorisé, et tout le reste est rejeté. C’est bien plus fiable que d’essayer de créer une liste noire de caractères dangereux. Le même principe est souligné dans le guide OWASP sur la validation des entrées.


Prévenir les attaques par injection

Injection SQL et NoSQL

Ne concaténez jamais les entrées utilisateur dans les requêtes. Utilisez des instructions paramétrées pour SQL :

// ❌ Vulnérable
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`

// ✅ Sécurisé
const result = await db.query("SELECT * FROM users WHERE email = $1", [email])

Les bases de données NoSQL ont leur propre risque d’injection. Les opérateurs MongoDB comme $gt peuvent être injectés via des corps JSON si vous passez des objets directement :

// ❌ Vulnérable — req.body.username pourrait être { $gt: "" }
User.find({ username: req.body.username })

// ✅ Sécurisé — imposez d'abord le type attendu
User.find({ username: String(req.body.username) })

En pratique, la validation de schéma devrait garantir que le champ est une chaîne de caractères (par exemple avec z.string()) avant qu’il n’atteigne la couche de requête.

Injection de commandes

Si votre API appelle des commandes système, évitez complètement child_process.exec() — elle passe les arguments à travers un shell et est trivialement exploitable. Utilisez plutôt execFile() ou spawn() avec des tableaux d’arguments explicites :

import { execFile } from "child_process"

// ✅ Les arguments sont passés directement, non interprétés par un shell
execFile("convert", [userSuppliedFilename, outputPath], (err, stdout) => {
  // traiter le résultat
})

Validez quand même et créez une liste blanche des valeurs d’arguments. Si l’opération présente un risque élevé, exécutez-la dans un conteneur isolé (sandbox).


Éviter l’assignation de masse et la pollution de prototype

L’assignation de masse se produit lorsque vous propagez un corps de requête directement sur un modèle :

// ❌ Un utilisateur pourrait envoyer { role: "admin" }
const user = await User.create({ ...req.body })

// ✅ Sélectionnez uniquement les champs attendus
const user = await User.create({
  username: result.data.username,
  email: result.data.email,
})

La pollution de prototype est plus subtile. La fusion d’objets non fiables peut permettre à un attaquant d’injecter des propriétés comme __proto__ ou constructor.prototype qui affectent chaque objet dans votre processus. Évitez les utilitaires de fusion profonde sur les données de requête brutes. Si vous devez fusionner, validez d’abord l’entrée et créez des objets intermédiaires avec Object.create(null) afin qu’ils n’héritent pas de la chaîne de prototypes par défaut. Le guide OWASP sur la prévention de la pollution de prototype fournit des conseils supplémentaires.


Limiter la taille et la structure des requêtes

Les charges utiles volumineuses ou profondément imbriquées peuvent causer des problèmes de déni de service. Définissez des limites de taille de corps au niveau de l’analyseur :

app.use(express.json({ limit: "50kb" }))

Pour les téléchargements de fichiers, utilisez multer avec des limites explicites de fileSize et files, et validez les types MIME — ne faites jamais confiance à l’extension de fichier seule.


Conclusion

La gestion sécurisée des entrées utilisateur dans Node.js se résume à un principe : définissez à quoi ressemble une entrée valide, appliquez-la à la frontière, et ne laissez jamais rien d’autre passer. La validation de schéma, les requêtes paramétrées, le passage d’arguments explicites et la sélection stricte de champs couvrent la majorité des surfaces d’attaque réelles. Appliquez ces modèles de manière cohérente au niveau de la couche de requête, avant que les données n’atteignent toute logique en aval, et la plupart des vulnérabilités de type injection n’auront tout simplement nulle part où se loger.

FAQ

La validation de schéma constitue une première ligne de défense solide car elle rejette les entrées mal formées avant qu'elles n'atteignent votre logique. Cependant, elle doit être combinée avec des requêtes paramétrées et une exécution sécurisée des commandes. La validation garantit que les données ont la bonne forme et le bon type, mais le code en aval doit également traiter ces données de manière sécurisée pour couvrir les cas limites que la validation seule pourrait manquer.

Les deux bibliothèques sont matures et largement utilisées. Zod offre un support TypeScript de premier ordre avec une inférence de types solide, ce qui en fait un choix naturel pour les projets TypeScript. Joi a un historique plus long dans l'écosystème Express et offre une API riche et chaînable. Choisissez en fonction des préférences linguistiques de votre projet et des dépendances existantes.

Évitez d'exécuter des fonctions de fusion profonde sur des données de requête brutes. Validez d'abord l'entrée avec un schéma strict afin que seuls les champs connus passent. Lorsque la fusion est nécessaire, créez des objets intermédiaires avec Object.create(null) pour produire des objets sans chaîne de prototypes. Des bibliothèques comme lodash.merge ont eu des vulnérabilités de pollution de prototype par le passé, alors maintenez toujours vos dépendances à jour.

Il n'y a pas de réponse universelle, mais 100 ko à 1 Mo couvre la plupart des cas d'usage d'API JSON. Commencez avec une limite conservatrice comme 100 ko et augmentez-la uniquement si des requêtes légitimes nécessitent davantage. Pour les téléchargements de fichiers, configurez des limites séparées via votre analyseur multipart. L'objectif est de rejeter les charges utiles anormalement volumineuses tôt, avant qu'elles ne consomment de la mémoire ou du temps de traitement.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay