Skip to main content

Éviter les vulnérabilités d’affectation de masse dans Node.js

feature buffer overflow

28 mars 2023

0 minutes de lecture

L’affectation de masse est une vulnérabilité qui permet aux attaquants d’exploiter des modèles d’enregistrements prévisibles et de déclencher des actions illégales. Elle se produit généralement lorsque les propriétés ne sont pas filtrées lors de la liaison des données fournies par le client aux modèles de données. Les vulnérabilités de ce type permettent à un attaquant d’ajouter des objets dans les charges utiles des requêtes POST, afin de modifier des propriétés qui devraient être immuables.

Node.js est un environnement serveur open source qui peut être utilisé sur différentes plateformes pour développer des applications web. Il permet aux développeurs de créer rapidement des applications puissantes et évolutives. Node.js est une plateforme éprouvée et prête pour la production, mais la sécurité des applications pose des défis liés à la sécurité de la chaîne d’approvisionnement et aux packages tiers installés pour créer les applications. La création d’une application Node.js peut faire appel à des milliers de packages npm open source issus du registre npm, ce qui l’expose à de nombreux vecteurs d’attaque.

Les vulnérabilités d’affectation de masse sont courantes dans les applications Node.js qui envoient des charges utiles à la base de données dans des requêtes POST. Un attaquant peut exploiter ces vulnérabilités pour orchestrer des attaques par injection SQL ou d’autres attaques visant les données stockées dans les bases de données. Une vulnérabilité d’affectation de masse peut permettre à un attaquant de prendre le contrôle total d’un système ou de dérober des données sensibles. Il est donc essentiel de se protéger contre ce type d’attaque.

Cet article montre comment une vulnérabilité d’affectation de masse peut se produire dans un projet Node.js, comment un attaquant peut l’exploiter et comment protéger une application web contre ce type de menace.

Éliminer les vulnérabilités d’affectation de masse dans Node.js

Cet exemple utilise Mongoose, une bibliothèque de modélisation des données objet (ODM) pour Node.js et MongoDB. Elle fait le lien entre les objets du code et ceux de MongoDB, valide les schémas et facilite la gestion des relations entre les données.

Comme indiqué ci-dessus, pour protéger une application Node.js avec MongoDB contre l’affectation de masse, vous devez intervenir dans Mongoose. Voici un exemple d’exploitation d’une vulnérabilité d’affectation de masse dans une application Node.js.

Prérequis

Pour suivre ce projet, vous aurez besoin des éléments suivants :

  • Node.js installé

  • Un accès à une base de données MongoDB et une chaîne de connexion valide. Ce tutoriel utilise un environnement cloud MongoDB Atlas, mais vous pouvez aussi lancer un serveur MongoDB local et mettre à jour la chaîne de connexion en conséquence.

L’affectation de masse dans Node.js

Pour commencer ce projet, ouvrez votre terminal et exécutez cette commande :

npm init

Vous devez ensuite installer les dépendances :

npm i mongoose
npm i express

Pour gérer les données du formulaire ci-dessus, nous avons besoin d’un fichier newUser.js dans le dossier models pour gérer le schéma utilisateur.

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const userSchema = new Schema({

    first_name: String,
    last_name: String,
    email: String,
    password: String,
    isAdmin: {
        type: Boolean,
        protect: true,
        default: false
    }

});

const User = mongoose.model("User", userSchema);

module.exports = User;

Ce modèle contient tous les champs du formulaire d’inscription. Le champ supplémentaire isAdmin, de type Boolean, définit le rôle de l’utilisateur. Si vous définissez ce champ sur true, vous créez un utilisateur doté de privilèges d’administrateur. Si vous le définissez sur false, l’utilisateur n’aura pas ces privilèges.

Créez un répertoire nommé routes, puis ajoutez-y le fichier routes.js pour gérer les informations des utilisateurs :

    const express = require("express");
    const userModel = require("../models/newUser");

    const app = express();

    app.post("/add_user", async (request, response) => {
        const user = new userModel(request.body);

        try {
          await user.save();
          response.send(user);
        } catch (error) {
          response.status(500).send(error);
        }
    });

module.exports = app;

Le code ci-dessus récupère les données du formulaire, puis les structure à l’aide du modèle newUSer.

Ce code comporte deux failles qui introduisent une vulnérabilité d’affectation de masse. Premièrement, il utilise un nom et un type courants pour un champ sensible : isAdmin. Deuxièmement, le modèle utilisateur contient un champ sensible inutilisé. Pour exploiter la vulnérabilité, démarrons un serveur.

Créez un fichier nommé server.js à la racine du projet et ajoutez-y le code suivant, en remplaçant <APP_ID> par la valeur figurant dans votre chaîne de connexion :

const express = require("express");
const mongoose = require("mongoose");
const Router = require("./routes/routes")

const app = express();

app.use(express.json());

const username = "<USER>";
const password = "<USER_PASSWORD>";
const cluster = "<CLUSTER>";
const dbname = "<DATABSE_NAME>";

mongoose.connect(
  `mongodb+srv://${username}:${password}@${cluster}.<APP_ID>.mongodb.net/${dbname}?retryWrites=true&w=majority`,
  {
    useNewUrlParser: true,
    useUnifiedTopology: true
  }
);

const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error: "));
db.once("open", function () {
  console.log("Connected successfully");
});

app.use(Router);

app.listen(3000, () => {
  console.log("Server is running at port 3000");
});

Démarrez le serveur en exécutant node server.js.

Un acteur malveillant peut exploiter cette vulnérabilité en créant une requête curl POST qui contient le champ isAdmin défini sur true, comme dans le code ci-dessous :

curl --location --request POST 'http://localhost:3000/add_user' \
--header 'Content-Type: application/json' \
--data-raw '{
    "first_name": "Mike",
    "last_name": "Jones",
    "email": "mikejones@hmail.com",
    "isAdmin":"true"
}'

La requête ci-dessus crée un utilisateur, Mike, doté de privilèges d’administrateur. Cet utilisateur peut désormais se connecter au système avec ses identifiants et effectuer toutes les actions accessibles à un administrateur.

Se protéger contre l’affectation de masse

Nous pouvons renforcer la protection de l’application contre les attaques par affectation de masse en procédant à trois ajustements.

Créer des modèles allégés

Les modèles allégés ne contiennent que les champs que l’utilisateur est censé renseigner et excluent les champs sensibles.

Dans l’exemple ci-dessus, la première mesure, et la plus importante, consiste à supprimer le champ sensible du modèle newUser, comme indiqué ci-dessous :

const mongoose = require("mongoose");

// create a schema
var userSchema = new mongoose.Schema({
	first_name: String,
	last_name: String,
	email: String,
	password: String

	//NB: there is no isAdmin field

});

const User = mongoose.model("User", userSchema);

module.exports = User;

Ainsi, même si des attaquants accèdent au code source, ils ne pourront pas voir le champ sensible ni l’utiliser pour orchestrer une attaque. Évitez également les termes prévisibles dans la base de données pour définir le rôle d’un utilisateur, comme isAdmin, admin et role : un attaquant pourrait les deviner et lancer une attaque.

Valider le schéma des données saisies par l’utilisateur

Valider avec underscore

Vous pouvez également protéger votre application en spécifiant et en limitant les champs qu’une requête POST peut traiter. Cette méthode, appelée liste d’autorisation, utilise la bibliothèque underscore. Commencez par installer la bibliothèque :

npm install underscore

Accédez au dossier routes et remplacez le code de route.js par celui-ci :

const express = require("express");
const userModel = require("../models/newUser");
var _ = require('underscore');

const app = express();

app.post("/add_user", async (request, response) => {
    const user = new userModel( _.pick(request.body, 'first_name', 'last_name', 'email', 'password'));

    try {
      await user.save();
      response.send(user);
    } catch (error) {
      response.status(500).send(error);
    }
});

module.exports = app;

Dans le code ci-dessus, nous utilisons la fonction pick pour spécifier les variables extraites d’une requête POST. Cela empêche un attaquant d’utiliser le champ sensible isAdmin.

Le problème avec underscore, c’est que l’application doit traiter les données avant de savoir si elles sont valides. Autrement dit, les erreurs ne sont pas détectées assez tôt.

Valider avec Zod

Pour valider les schémas de manière robuste, utilisez Zod. Cet outil valide le schéma et garantit que les données respectent strictement la structure, le modèle et le type de données spécifiés. Zod détecte suffisamment tôt les données incomplètes ou incorrectes pour prévenir les erreurs de l’application. Vous pouvez également créer facilement un message d’erreur indiquant à l’utilisateur quels champs ont provoqué une erreur et de quel type d’erreur il s’agit.

Cet extrait de code montre le fonctionnement de Zod. Notez qu’il est écrit en TypeScript, ce qui nécessite une configuration supplémentaire.

import { z, AnyZodObject } from "zod";
import express, { Request, Response, NextFunction } from 'express';

const app = express();

app.use(express.json());

const dataSchema = z.object({
    body: z.object({
        first_name: z.string({
            required_error: "First name is required",
            invalid_type_error: "First name must be a string",
        }),
        last_name: z.string({
            required_error: "Last name is required",
            invalid_type_error: "Last name must be a string",
        }),
        password: z.string({
            required_error: "Password is required",
        }),
        email: z
            .string({
                required_error: "Email is required",
            })
            .email("Not a valid email"),
    }),
});

const validate =
    (schema: AnyZodObject) =>
    async (req: Request, res: Response, next: NextFunction) => {
        try {
            await schema.parseAsync({
                body: req.body,
                query: req.query,
                params: req.params,
            });
            return next();
        } catch (error) {
            return res.status(400).json(error);
        }
    };

app.post("/create",
    validate(dataSchema),
    (req: Request, res: Response): Response => {
        return res.json({
            ...req.body
        });
    }
);

Si un utilisateur saisit des données qui ne correspondent pas au schéma, un message d’erreur indiquant invalid_type_error s’affiche. S’il laisse un champ vide, il reçoit une erreur required_error.

Autres exemples de vulnérabilités d’affectation de masse dans les ORM

Examinons trois autres exemples de vulnérabilités d’affectation de masse dans des solutions ORM courantes.

Vulnérabilité d’affectation de masse dans du code Sequelize

Examinons un exemple de code Sequelize vulnérable à l’affectation de masse. Ce code définit un modèle utilisateur, crée un utilisateur et enregistre ses informations dans la base de données :

const { Sequelize } = require("sequelize");

const User = sequelize.define("users", {
    username: {
        type: DataTypes.STRING,
        allowNull: false
    },

    email: {
        type: DataTypes.STRING,
        allowNull: false
    },

    password: {
        type: DataTypes.STRING,
        allowNull: false
    },

    is_verified: {
        type: DataTypes.BOOLEAN,
        allowNull: false,
        default: false
    },

 });

Lors de la création d’un nouvel utilisateur, le contrôleur appelle le modèle ci-dessus, comme indiqué ci-dessous :

// Create a User
const user = {
    username: req.body.username,
    email: req.body.email,
    password: req.body.password,
    is_verified: false
  };

//Save User in the database
User.create(user)
.then(data => {
    res.send(data);
})
.catch(err => {
    res.status(500).send({
    message:
        err.message || "Error occurred. User not created!"
    });
});

Le code ci-dessus est vulnérable, car il expose le champ sensible is_verified. Un attaquant peut créer une requête POST contenant le champ is_verified défini sur true, ce qui lui permet de contourner l’étape de vérification.

Pour éliminer la vulnérabilité d’affectation de masse dans ce code, supprimons le champ is_verified lors de la création de l’utilisateur. Voici à quoi ressemblerait le code sécurisé :

// Create a User
const user = {
    username: req.body.username,
    email: req.body.email,
    password: req.body.password
  };

Cela limite les variables acceptées dans les requêtes POST au nom d’utilisateur, à l’adresse e-mail et au mot de passe.

Vulnérabilité d’affectation de masse dans du code Prisma

Voici un exemple de code Prisma vulnérable à l’affectation de masse, qui expose le champ sensible Role.

model User {
    id      		Int    @id @default(autoincrement())
    username        String
    email       	String
    Role    		Boolean    @default(USER)
  }

enum Role {
    USER
    ADMIN
}

Pour ajouter un utilisateur, nous créons une route add_user.

const { PrismaClient } = require("@prisma/client");
const express = require("express");
const prisma = new PrismaClient();

const app = express();

app.use(express.json());

app.post('/add_user', async (req, res) => {
    const user = await prisma.user.create({ data: req.body });
    res.json(user);
  });

Cette requête POST attend un corps contenant un objet utilisateur. Le code ci-dessus est vulnérable aux attaques par affectation de masse, car un pirate peut ajouter le champ role à ADMIN pour obtenir des privilèges d’administrateur.

Nous pouvons corriger cette vulnérabilité en excluant du modèle le champ sensible Role.

model User {
    id      		Int    @id @default(autoincrement())
    username        String
    email       	String
  }

Vulnérabilité d’affectation de masse dans du code MySQL

Voici un exemple de code MySQL vulnérable à l’affectation de masse, qui expose le champ sensible isAdmin :

// Defining the user data
let user = {
    username: req.body.username,
    email: req.body.email,
    password: req.body.password,
    isAdmin: req.body.role
};

Ce code ajoute l’utilisateur défini ci-dessus :

// Saving user details
app.post('/add_user',(req, res) => {

let myQuery = "INSERT INTO users SET ?";

    let query = connection.query(myQuery, user,(err, results) => {
        if(err) throw err;
        res.send(apiResponse(results));
    });

});

Le code ci-dessus présente une vulnérabilité d’affectation de masse, car il expose un champ sensible — isAdmin — et permet de l’inclure dans une requête POST. Un attaquant peut créer une requête POST qui définit le champ isAdmin sur true et crée ainsi un utilisateur administrateur.

Nous pouvons corriger cette vulnérabilité en excluant le champ sensible lors de la création de l’objet user. Voici le code sécurisé :

// Defining the user data
let user = {
    username: req.body.username,
    email: req.body.email,
    password: req.body.password
};

Vous pouvez définir la valeur par défaut du champ isAdmin sur false en modifiant la table à l’aide de la commande suivante :

ALTER TABLE users ALTER isAdmin SET DEFAULT ‘false’;

Sécuriser votre code Node.js

Node.js est sécurisé, mais les packages nécessaires au développement d’une application web ne le sont pas toujours. Dans les applications Node.js qui envoient des requêtes à une base de données, l’affectation de masse constitue l’un des vecteurs d’attaque les plus courants. Elle permet aux utilisateurs malveillants de lancer des attaques par injection SQL, qui consistent à envoyer des requêtes malveillantes à une base de données.

Toutefois, la validation des données saisies permet d’éviter facilement l’affectation de masse dans Node.js. Pour cela, vous pouvez ne pas exposer les champs sensibles, limiter les champs acceptés ou empêcher la modification de certains champs.

L’absence de validation robuste des données saisies par l’utilisateur expose un système à des attaques telles que la pollution de prototype. Ces attaques peuvent contourner des mécanismes de validation faibles comme underscore. Veillez donc à toujours utiliser des solutions robustes telles que Zod.

Consultez l’exemple complet du code source dans notre dépôt GitHub et lancez vos propres tests de sécurité en local pour découvrir comment éviter les vulnérabilités d’affectation de masse dans Node.js.

Suivez les 10 meilleures pratiques de Snyk en matière de sécurité Node.js pour optimiser la sécurité de votre application Node.js.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Publié dans: