Accueil / Articles / Sécuriser les API Express : authentification, validation, limitations de fréquence et surveillance.

Sécuriser les API Express : authentification, validation, limitations de fréquence et surveillance.

Un guide pratique pour comprendre la sécurité des API avec Express : authentification via Passport et JWT, modèles d’autorisation, chiffrement AES-GCM, validation des données, limitation des fréquences d’accès et journalisation.

5092 mots

Chaque API que vous mettez en ligne représente une porte d’accès à votre système, et les attaquants explorent ces portes de manière bien plus systématique que la plupart des équipes ne les testent. Une sécurité ajoutée a posteriori après le lancement a tendance à laisser des failles : une route sans protection, une requête construite à partir d’entrées brutes, un point d’accès de connexion acceptant volontiers un million de tentatives. Ce guide présente les normes à connaître, les menaces les plus fréquentes, ainsi que six couches de défense concrètes mises en œuvre avec Node.js et Express, afin que vous puissiez auditer une API existante ou en créer une nouvelle dotée de protections dès le premier jour.

Considérez ce qui suit comme une liste de contrôle à laquelle vous devrez revenir tout au long du cycle de développement : avant une publication, après un correctif, et chaque fois qu’une dépendance ou une route change. Effectuer régulièrement ces vérifications permet de détecter les vulnérabilités tant qu’elles sont encore mineures.

Pourquoi les API méritent une attention sécuritaire spécifique

Les produits modernes sont de plus en plus assemblés à partir d’API. Au lieu de développer chaque fonctionnalité en interne, les équipes intègrent des services de paiement, d’identification, de messagerie et de données via des interfaces bien définies, et de nombreuses entreprises proposent désormais des produits axés sur les API en premier lieu. Une étude de marché évalue l’économie des API à environ 20 milliards de dollars d’ici 2026 (résumé du rapport) ; quel que soit le chiffre exact, cette dépendance est réelle et en augmentation.

Cette dépendance a des effets dans les deux sens. Une API offre aux clients légitimes des fonctionnalités prêtes à l’emploi et réutilisables, mais elle fournit également aux attaquants un point d’accès documenté et adapté aux machines. Les enquêtes sectorielles mettent constamment en relation une grande partie des violations de sécurité avec les API ; le rapport de 2023 de Traceable attribue 74 % des violations de données à ces dernières.

Les API se trouvent fréquemment directement en face des données les plus sensibles détenues par une entreprise : des plateformes d’identité contenant des informations personnelles, des documents financiers, ainsi que des processus internes. Un accès non autorisé peut entraîner des données corrompues, un mauvais usage des services, des pertes financières, une perte de confiance de la part des clients et des sanctions réglementaires en vertu de la loi sur la protection des données. Étant donné l’importance de ces enjeux, la sécurité doit faire partie des accords de niveau de service aux côtés du temps de disponibilité, et c’est la responsabilité de chaque équipe de produit, et non seulement d’un groupe dédié à la sécurité. Un bon point de départ est de s’appuyer sur les normes déjà convenues par l’industrie.

Normes et cadres à connaître

Les normes de sécurité des API sont des spécifications formelles, des protocoles et des directives appliqués tout au long du cycle de développement logiciel afin que la protection soit cohérente plutôt que improvisée. Ce sont celles que vous rencontrerez le plus souvent :

  • OWASP API Security Top 10 : une liste classée des risques les plus critiques pour les API, gérée par le Open Web Application Security Project. Elle couvre des problèmes tels que l’autorisation au niveau de l’objet défectueuse, la falsification des requêtes côté serveur et une consommation de ressources non limitée, et constitue le meilleur point de départ pour une analyse des menaces.
  • OAuth 2.0 et 2.1 : un cadre d’autorisation déléguée. Un client obtient un jeton d’accès doté de scopes définis et l’utilise pour agir au nom d’un utilisateur sans jamais avoir accès à son mot de passe ; les jetons de renouvellement permettent au client d’obtenir de nouveaux jetons d’accès sans devoir solliciter à nouveau l’utilisateur.
  • OpenID Connect (OIDC) : une couche d’identité par-dessus OAuth. Elle standardise le jeton d’identité ainsi que la manière dont les clients le valident, ce qui rend possible l’authentification unique et la récupération des profils auprès des fournisseurs d’identité de manière interopérable.
  • JSON Web Tokens (JWTs) : un format de token compact et signé. Contrairement à une cookie de session opaque, un JWT contient ses informations (ID utilisateur, rôle, date d’expiration) directement à l’intérieur du token lui-même, et le serveur vérifie la signature plutôt que de rechercher une session. Il convient de noter qu’un JWT signé standard est codé, et non chiffré, ce qui permet à quiconque le détient de lire son contenu.
  • Transport Layer Security (TLS) : le protocole cryptographique qui sous-tend HTTPS. Il assure la chiffration des données en transit, l’authentification des parties communicantes ainsi que des vérifications d’intégrité permettant de détecter toute modification non autorisée.
  • Zéro Confiance : une approche architecturale qui considère chaque utilisateur et service comme non fiables tant qu’ils n’ont pas été vérifiés. Elle repose sur une vérification explicite, le principe du moindre privilège et l’hypothèse qu’une violation de sécurité a déjà eu lieu quelque part.
  • Cadre de cybersécurité NIST : directives particulièrement adaptées aux architectures cloud et de microservices, mettant l’accent sur l’authentification, l’autorisation et la protection des données.
  • Profils API de niveau financier (FAPI) : profils OAuth et OIDC renforcés pour des domaines à haut risque tels que la banque, la fintech et les données réglementées. Ils exigent une authentification client solide, un traitement plus strict des requêtes et des réponses, ainsi que des tokens restreints au niveau de l’expéditeur qui deviennent inutilisables en cas de vol, ce qui permet à la fois de prévenir la fraude et d’améliorer l’interopérabilité.
  • Ces éléments vous fournissent une base solide, bien qu’il ne s’agisse pas d’une liste exhaustive.

    Les menaces auxquelles vous devez vous prémunir

    Les vulnérabilités les plus fréquentes dans les API réelles sont :

    1. Authentification défaillante : des vérifications d’identité faibles ou absentes ainsi qu’une gestion insuffisante des sessions permettent à un attaquant de voler des cookies ou des tokens et de les réutiliser pour accéder à vos services.
    2. Autorisation au niveau de l’objet défaillante (BOLA) : l’API vérifie que l’utilisateur est connecté, mais pas qu’il a le droit d’accéder à un enregistrement spécifique ; ainsi, modifier un ID dans l’URL expose les données ou les processus internes d’une autre personne.
    3. Injection SQL : des données contrôlées par l’attaquant sont concaténées à une requête et exécutées par la base de données. Presque tous les clients de base de données offrent un mécanisme de paramètres permettant de transmettre des valeurs en toute sécurité.
    4. Injection de commandes : des données non fiables provenant d’une requête parviennent à une shell système ou à un processeur de commandes, permettant à l’attaquant d’exécuter des commandes arbitraires en utilisant les privilèges du processus serveur.
  • Scripting intersite (XSS) : un script injecté s’exécute dans le navigateur de la victime dans le contexte de votre application, permettant à l’attaquant d’agir en tant que cet utilisateur et de lire des données normalement protégées par la politique du même origine.
  • Misconfiguration de sécurité : clés API divulguées, variables d’environnement exposées, paramètres par défaut trop permissifs. Des identifiants compromis peuvent être utilisés pour appeler votre API ou générer des factures auprès de services tiers payants.
  • Exposition excessive ou sensible de données : un gestionnaire renvoie l’ensemble de l’objet de base de données au lieu des champs nécessaires au client. Même si l’interface utilisateur ne les affiche jamais, ces données restent dans les caches, le stockage local et l’onglet réseau du navigateur.
  • Dénial de service (y compris les attaques distribuées) : une vague d’requêtes épuise les ressources du serveur ou met l’API complètement hors ligne.
  • Dépendances tierces : votre API est également une cliente d’autres APIs et de paquets logiciels. Chacun d’eux élargit votre surface d’attaque, et toute fuite de données ou panne survenant chez eux devient alors un problème pour vous.
  • Chacune de ces situations correspond à une pratique de programmation spécifique. Le reste de ce guide les aborde en six niveaux, en utilisant JavaScript et Express tout au long.

    1. Authentification : prouver l’identité de l’appelant

    Toute API protégée doit exiger que le client prouve son identité avant de pouvoir effectuer des actions significatives, que ce soit par le biais d’un nom d’utilisateur et d’un mot de passe, d’une clé API ou d’un jeton signé. Les méthodes d’authentification se répartissent généralement en cinq catégories : authentification par nom d’utilisateur et mot de passe, authentification à plusieurs facteurs, authentification basée sur des jetons, authentification basée sur des certificats et biométrie.

    Une source fréquente de confusion concerne ce que remplace réellement un JWT. Les sessions serveur traditionnelles stockaient l’état sur le serveur et dépendaient des cookies du navigateur pour transmettre un identifiant de session. Un JWT élimine la nécessité de cette recherche côté serveur à chaque requête, mais il ne vérifie pas les identifiants par lui-même : quelqu’un doit encore vérifier le mot de passe une fois avant que le jeton ne soit émis. C’est pourquoi un flux hybride fonctionne bien. L’utilisateur se connecte avec son e-mail et son mot de passe, le serveur émet un JWT en cas de succès, et chaque requête ultérieure ne présente que le jeton. La vérification des identifiants et l’authentification par requête deviennent alors deux tâches distinctes, et le mot de passe n’est plus transmis avec chaque appel.

    Au sein d’Express, la bibliothèque Passport prend en charge ces deux aspects grâce à des stratégies interchangeables. La configuration se fait en trois étapes.

    Étape 1 : enregistrer une stratégie locale et une stratégie JWT

    La stratégie locale s’exécute une seule fois lors de la connexion et a pour mission de rechercher l’utilisateur par e-mail ainsi que de comparer le mot de passe soumis avec le hash enregistré. La stratégie JWT s’exécute à chaque requête protégée : elle extrait le jeton de l’en-tête Authorization: Bearer, vérifie la signature par rapport à JWT_SECRET, et identifie l’utilisateur mentionné dans la charge utile. L’exportation d’un middleware authenticateJWT prédéfini avec session: false rend explicite l’intention sans état.

    const passport = require("passport");
    const LocalStrategy = require("passport-local").Strategy;
    const { Strategy: JwtStrategy, ExtractJwt } = require("passport-jwt");
    
    // Local Strategy: Verify username and password during login.
    passport.use(
      new LocalStrategy(
        { usernameField: "email", passwordField: "password" },
        async (email, password, done) => {
          // Find the user and compare the hashed password.
          // If valid, return the user.
        }
      )
    );
    
    // JWT Strategy: Verify the token on protected requests.
    passport.use(
      new JwtStrategy(
        {
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
          secretOrKey: process.env.JWT_SECRET,
        },
        async (payload, done) => {
          // Find the user referenced in the token.
        }
      )
    );
    
    // Middleware
    const authenticateJWT = passport.authenticate("jwt", { session: false, });
    
    module.exports = { passport, authenticateJWT, };
    

    Les fonctions de vérification sont laissées ici sous forme de commentaires, et c’est là que réside le véritable travail en matière de sécurité. Utilisez un algorithme de hachage lent et avec sel, comme bcrypt ou Argon2, pour comparer les mots de passe, et retournez toujours la même erreur générique que l’e-mail soit inconnu ou que le mot de passe soit incorrect, afin que l’endpoint ne puisse pas être utilisé pour découvrir quels comptes existent.

    Étape 2 : s’authentifier lors de la connexion et signer un jeton

    Le gestionnaire de connexion invoque la stratégie locale via un callback personnalisé. Une erreur est transmise au mécanisme de gestion des erreurs d’Express, l’absence d’utilisateur génère un 401, tandis qu’une correspondance réussie produit un jeton signé avec l’ID et le rôle de l’utilisateur, ainsi qu’une date d’expiration tirée de JWT_EXPIRES_IN, avec une valeur par défaut de deux heures.

    const jwt = require("jsonwebtoken");
    const passport = require("passport");
    
    const login = (req, res, next) => {
      passport.authenticate("local", { session: false }, (err, user, info) => {
        if (err) return next(err);
        if (!user) return res.status(401).json({ message: info.message, });
    
        // Issue a signed JWT after successful authentication.
        const token = jwt.sign(
          { id: user.id, role: user.role,},
          process.env.JWT_SECRET,
          { expiresIn: process.env.JWT_EXPIRES_IN || "2h",}
        );
    
        return res.status(200).json({ message: "Login successful.", token, user,});
      })(req, res, next);
    };
    

    Deux points méritent une attention particulière. La durée de validité courte limite le temps pendant lequel un jeton volé reste utilisable ; si les sessions doivent durer plus longtemps, il convient de combiner des jetons d’accès à courte durée avec un mécanisme de renouvellement, comme celui décrit dans notre stratégie de jeton de renouvellement pour les systèmes d’authentification Node.js. De plus, la réponse renvoie le user tel quel. Si cet objet correspond à une ligne de base de données brute, il peut contenir le hash du mot de passe ainsi que des champs internes, ce qui représente précisément l’exposition excessive de données évoquée précédemment. Il est préférable de renvoyer uniquement un sous-ensemble explicite, tel que l’ID, l’e-mail et le rôle.

    Étape 3 : protéger les routes sécurisées

    Avec le middleware exporté, protéger une route signifie placer authenticateJWT avant le contrôleur dans la définition de la route. Les requêtes sans jeton valide sont rejetées avant même que toute logique métier ne soit exécutée.

    const {  Router } = require("express");
    const authRouter = Router();
    
    // Get auth middleware and sample prorected controller
    const authController = require("../controllers/auth.controller");
    const { authenticateJWT } = require("../middleware/authentication");
    
    // Use JWT as a guard to protect certain routes
    authRouter.get("/me", authenticateJWT, authController.me);
    authRouter.patch("/password", authenticateJWT, authController.updatePassword);
    

    Si vous préférez des routeurs légers, cette même protection peut être intégrée dans la chaîne de middleware du contrôleur lui-même. Dans tous les cas, faites en sorte que la protection soit la norme pour un routeur et exemptez délibérément les routes publiques, plutôt que de vous souvenir d’ajouter la protection route par route.

    2. Autorisation : déterminer ce que l’appelant peut faire

    L’authentification répond à la question « qui êtes-vous ? » ; l’autorisation répond à la question « que pouvez-vous faire ? ». Elle s’exécute généralement juste après l’authentification et évalue chaque identité par rapport aux règles d’accès avant d’autoriser ou de refuser une demande. Sans elle, tout utilisateur connecté peut lire des données sensibles ou déclencher des actions privilégiées, ce qui est précisément la cause des vulnérabilités BOLA.

    Trois modèles couvrent la plupart des besoins :

    • Le contrôle d’accès basé sur les rôles (RBAC) attribue des permissions aux rôles et ces derniers aux utilisateurs. Une API de blogging peut comporter des rôles d’administrateur, d’éditeur et de lecteur. Ce système convient aux fonctions et groupes stables, c’est pourquoi il est courant dans les applications d’entreprise.
    • Le contrôle d’accès basé sur les attributs (ABAC) évalue les attributs de l’utilisateur, du ressource ainsi que de l’environnement de la demande (département, niveau de sensibilité du ressource, heure de la journée, réseau) par rapport à des politiques prédéfinies. Il est adapté aux API dont les décisions dépendent fortement du contexte ou changent fréquemment.
    • Le contrôle d’accès basé sur les relations (ReBAC) accorde l’accès en fonction de la relation entre un utilisateur et une ressource spécifique, telle que la propriété ou l’appartenance à un groupe, ce qui est généralement vérifié en parcourant un graphe de relations. Ce système convient particulièrement aux produits collaboratifs comme le partage de documents ou les plateformes sociales.

    Créer soi-même des autorisations est un excellent moyen de comprendre leurs subtilités, mais les systèmes de production délèguent souvent l’émission et la validation des jetons à un fournisseur d’identité. Lorsqu’une API est enregistrée auprès d’un fournisseur tel que Microsoft Entra ID et configurée pour accepter des jetons porteurs, les points d’entrée sensibles valident les scopes et les rôles de chaque jeton avant d’exécuter la requête. Un jeton invalide ou l’absence de permissions appropriées entraîne une réponse 401 Unauthorized. Dans Express, le chemin protégé se présente comme ceci :

    app.get(
      "/api/orders",
      passport.authenticate("oauth-bearer", { session: false }),
      (req, res) => {
        res.json({ message: "Protected resource." });
      }
    );
    

    N’oubliez pas qu’un jeton validé ne définit que des permissions générales. Les vérifications au niveau de l’objet, telles que « cet ordre appartient-il à cet utilisateur ? », doivent toujours être effectuées dans votre gestionnaire ou dans la couche de données, car aucun fournisseur d’identité ne sait qui possède la ligne 4812 dans votre base de données.

    Pour les protocoles eux-mêmes, fiez-vous aux normes du secteur telles que OAuth 2.0, OpenID Connect et SAML. Vous pouvez mettre en œuvre ces flux vous-même ou les déléguer à des fournisseurs d’identité tels que Ping Identity, Okta, Microsoft Entra ID, AWS ou IBM Security Verify.

    3. Chiffrement : protection des données en transit et au repos

    Le chiffrement transforme les données lisibles en texte chiffré qui devient inutilisable sans la bonne clé. TLS protège les données pendant leur transmission ; le chiffrement au repos les protège là où elles sont stockées, y compris dans la base de données. Les systèmes sensibles utilisent généralement les deux, car sans chiffrement, des données telles que les identifiants financiers peuvent être interceptées ou récupérées à partir d’un stockage compromis.

    Les principales approches font un compromis entre vitesse et gestion des clés :

    • Chiffrement symétrique : il utilise une seule clé partagée. Il est très rapide et gère efficacement de grandes quantités de données, ce qui en fait le choix idéal pour le chiffrement des données au repos et du contenu principal, mais les deux parties doivent conserver ce secret de manière sécurisée.
    • Chiffrement asymétrique : il utilise une paire de clés, une publique et une privée, ce qui évite tout échange de secret partagé. Il est considérablement plus lent et n’est pratique que pour de petites quantités de données.
    • Chiffrement hybride : il combine les deux approches ; la cryptographie asymétrique protège une clé symétrique, tandis que cette dernière protège les données en masse. On bénéficie ainsi des avantages de l’échange de clés du premier type et de la vitesse du second.

    Pour le chiffrement symétrique, le module crypto intégré à Node prend en charge AES-256-GCM. Le gestionnaire ci-dessous sérialise le corps de la requête, génère un vecteur d’initialisation de 12 octets, chiffre les données, puis renvoie l’IV, la étiquette d’authentification GCM ainsi que le texte chiffré sous forme de chaînes hexadécimales.

    const crypto = require("crypto");
    
    const algorithm = "aes-256-gcm";
    const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
    
    app.post("/api/orders", (req, res) => {
      const iv = crypto.randomBytes(12);
    
      const cipher = crypto.createCipheriv(algorithm, key, iv);
    
      const encrypted = Buffer.concat([
        cipher.update(JSON.stringify(req.body), "utf8"),
        cipher.final(),
      ]);
    
      const payload = {
        iv: iv.toString("hex"),
        tag: cipher.getAuthTag().toString("hex"),
        data: encrypted.toString("hex"),
      };
    
      // Store or transmit the encrypted payload
      res.json(payload);
    });
    

    Plusieurs éléments garantissent la correction de cette méthode. La clé doit avoir exactement 32 octets (64 caractères hexadécimaux dans ENCRYPTION_KEY) et provenir d’un gestionnaire de secrets plutôt que du code source. L’IV doit être unique pour chaque opération d’encodage réalisée avec la même clé ; réutiliser un IV avec GCM a des conséquences catastrophiques, c’est pourquoi il est généré à chaque demande. La signature d’authentification permet à la partie chargée du déchiffrement de détecter toute modification, il faut donc la stocker avec le texte chiffré et la vérifier lors du déchiffrement. Dans un service réel, ce payload serait persisté ou transmis plutôt que renvoyé au demandeur comme le fait la démonstration.

    L’encodage asymétrique est également disponible dans ce même module. Les données chiffrées avec une clé publique ne peuvent être déchiffrées qu’avec la clé privée correspondante :

    const crypto = require("crypto");
    
    const encrypted = crypto.publicEncrypt(
      publicKey,
      Buffer.from("Sensitive API data")
    );
    

    Puisque RSA ne peut chiffrer qu’un en-tête de données plus petit que la taille de sa clé, publicEncrypt convient aux valeurs courtes telles qu’un champ secret ou une clé symétrique, mais pas à des documents entiers. C’est cette limitation que résout le flux de travail hybride : générer une clé AES temporaire, chiffrer l’en-tête de données avec elle, chiffrer la clé AES à l’aide de la clé publique RSA du destinataire, puis envoyer les deux. Le destinataire utilise sa clé privée pour récupérer la clé AES, puis déchiffre l’en-tête de données. La plupart des API n’auront jamais besoin de cette méthode dans leur code d’application, puisque TLS effectue déjà un échange similaire, mais il est utile de la comprendre pour les scénarios d’encryption bout en bout.

    4. Validation et nettoyage des entrées

    Lorsque votre API accepte des données de client, vous ne pouvez pas prédire ce qui arrivera. Des en-têtes mal formatés, des fragments SQL et des charges utiles de scripts ressemblent tous à des chaînes ordinaires jusqu’à ce que quelque chose les interprète. Deux techniques complémentaires permettent de faire face à ce problème. La validation rejette les entrées qui violent vos règles structurelles et sémantiques. La sanitisation transforme les entrées acceptées en une forme sécurisée et normalisée avant qu’elles n’atteignent vos traitements.

    Imposer d’abord le type de contenu

    Vérifier le format de la requête elle-même est le contrôle le moins coûteux. Cette petite factory de middleware utilise req.is() pour confirmer le Content-Type et répond par 415 Unsupported Media Type dans le cas contraire. Elle peut être intégrée de manière globale, par routeur ou par point d’entrée.

    const requireContentType = (type) => (req, res, next) => {
      if (!req.is(type)) {
        return res.status(415).json({ error: "Unsupported Media Type", });
      }
    
      next();
    };
    
    app.post("/api/users", requireContentType("application/json"),
      (req, res) => {
        res.json({ message: "User created." });
      }
    );
    

    Valider la structure et le sens de l’en-tête

    Avec le format imposé, la couche suivante vérifie que la requête est bien formulée. En utilisant express-validator, conservez les règles dans un module de validation dédié. Celui-ci exige une adresse e-mail syntaxiquement valide, effectue une vérification asynchrone personnalisée qui rejette les adresses déjà présentes dans la base de données, et impose une longueur minimale pour le mot de passe de huit caractères.

    const { body } = require("express-validator");
    const { getUserEmail } = require("../db/queries");
    
    const validateRegistration = [
      body("email")
        .isEmail()
        .withMessage("Invalid email format")
        .custom(async (value) => {
          if (await getUserEmail(value)) {
            throw new Error("Email is already in use");
          }
          return true;
        }),
    
      body("password")
        .isLength({ min: 8 })
        .withMessage("Password must be at least 8 characters long"),
    ];
    
    module.exports = { validateRegistration }
    

    L’array des validateurs est ensuite inséré dans la chaîne de middleware de la route. À l’intérieur du gestionnaire, validationResult(req) collecte tous les échecs, et la route renvoie une réponse 400 avec la liste complète au lieu de continuer.

    const { validationResult } = require("express-validator");
    const { validateRegistration } = require("../validators/userValidator");
    
    app.post("/api/register", validateRegistration, (req, res) => {
      const errors = validationResult(req);
    
      if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
      }
    
      res.json({ message: "Registration successful." });
    });
    

    Sanitisation après validation

    Puisque Express traite les middleware dans l’ordre, une chaîne de sanitisation peut être placée juste après la validation. Ici, le prénom est tronqué et échappé en HTML, tandis que l’adresse e-mail est normalisée.

    const sanitizeRegistration = [
      body("firstName").trim().escape(),
      body("email").normalizeEmail(),
    ];
    
    app.post(
      "/api/register",
      validateRegistration,
      sanitizeRegistration,
      (req, res) => {
        const errors = validationResult(req);
    
        if (!errors.isEmpty()) {
          return res.status(400).json({ errors: errors.array() });
        }
    
        res.json({ message: "Registration successful." });
      }
    );
    

    Ici, l’ordre des opérations est important d’une manière subtile. La vérification de l’unicité dans le validateur s’exécute avant normalizeEmail(), ce qui permet à deux orthographes identiques d’une même adresse de passer inaperçues et de créer des comptes dupliqués. Normaliser les données avant la recherche, ou imposer l’unicité sur la valeur normalisée au niveau de la base de données, comble cette faille. Faites également attention à l’utilisation de escape() : l’encodage HTML des entrées protège les templates qui affichent ces valeurs, mais il modifie les données stockées ; de nombreux équipes préfèrent stocker des valeurs brutes et effectuer l’encodage à l’étape de sortie. Si vous envisagez d’utiliser des bibliothèques de validation, notre comparaison entre Zod et express-validator décrit les avantages et inconvénients respectifs.

    Utilisez des requêtes paramétrées pour la base de données

    N’essayez jamais de construire des requêtes SQL en concaténant les entrées fournies par l’utilisateur. Les clients de base de données tels que pg et les ORM comme Prisma prennent en charge les requêtes paramétrées, qui envoient le texte de la requête et les valeurs séparément, de sorte que la base de données traite toujours les entrées comme des données et jamais comme du SQL exécutable.

    Avec pg, créez un pool de connexions une seule fois et exportez-le pour vos modules de données :

    const { Pool } = require("pg");
    
    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
    });
    
    module.exports = pool;
    

    Les requêtes utilisent alors des placeholders numérotés ($1, $2) auxquels sont associées les valeurs fournies dans un tableau distinct. Même si email contient une citation suivie de DROP TABLE, il est stocké en tant que chaîne de caractères littérale.

    app.post("/api/users", async (req, res) => {
      const { email, name } = req.body;
    
      await pool.query(
        "INSERT INTO users (email, name) VALUES ($1, $2)",
        [email, name]
      );
    
      res.status(201).json({ message: "User created." });
    });
    

    Ces éléments combinés forment un pipeline en couches. Express-validator vous permet d’isoler les règles en unités réutilisables qui s’exécutent en tant que middleware et signalent chaque échec, tandis que la paramétrisation garantit que même les entrées qui échappent à la validation ne peuvent pas modifier vos requêtes.

    5. Limitation des débits et throttling

    La limitation des débits définit le nombre de requêtes qu’un client peut envoyer dans une fenêtre temporelle donnée. Elle atténue les tentatives de force brute et d’attaque par déni de service, et empêche un utilisateur gourmand d’épuiser les ressources des autres.

    Des limites peuvent être appliquées selon différentes dimensions :

    • Pour chaque client : les requêtes sont comptées par clé API ou adresse IP. Lorsqu’un client atteint la limite, il doit attendre que la fenêtre temporelle se réinitialise ou obtenir une quota plus élevée, généralement dans un forfait payant.
    • Par géographie ou par période : les limites varient en fonction de la région ou de la fenêtre temporelle ; par exemple, un volume plus élevé est autorisé pour les régions où se trouvent vos clients, tandis que les limites sont plus strictes pour les sources de trafic suspects.
  • En fonction de la capacité du serveur : certaines parties d’une API sont acheminées vers une infrastructure dédiée ayant ses propres limites, comme un petit groupe de travailleurs en arrière-plan pour les tâches coûteuses.
  • De nombreux algorithmes existent (fenêtre fixe, fenêtre glissante, bucket de tokens), et vous n’avez pas besoin de les implémenter vous-même pour commencer. Le middleware express-rate-limit compte par défaut les requêtes par adresse IP. L’exemple ci-dessous fixe un budget général de 100 requêtes toutes les 15 minutes pour tout ce qui se trouve sous /api, ainsi que des restrictions beaucoup plus strictes de cinq tentatives toutes les cinq minutes pour le connexion, avec un message personnalisé pour les requêtes rejetées.

    const rateLimit = require("express-rate-limit");
    
    // Apply to all API routes
    const apiLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100,
    });
    
    // Apply stricter limits to authentication endpoints
    const loginLimiter = rateLimit({
      windowMs: 5 * 60 * 1000, // 5 minutes
      max: 5,
      message: "Too many login attempts. Please try again later.",
    });
    
    app.use("/api", apiLimiter);
    
    app.post("/api/login", loginLimiter, (req, res) => {
      res.json({ message: "Login successful." });
    });
    

    Les API publiques attribuent généralement une clé à chaque utilisateur, et limiter l’accès en fonction de cette clé est plus équitable que de le faire via l’IP, car de nombreux utilisateurs peuvent partager une même adresse derrière un proxy d’entreprise. Un keyGenerator personnalisé lit l’en-tête X-API-Key et l’utilise comme identifiant du compteur.

    const rateLimit = require("express-rate-limit");
    
    const apiKeyLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 1000,
      keyGenerator: (req) => req.get("X-API-Key"),
    });
    
    app.use("/api", apiKeyLimiter);
    

    Comme c’est écrit actuellement, toute requête omettant cet en-tête génère la même clé undefined et partage un seul groupe d’accès. En pratique, il convient de rejeter les requêtes sans clé plus tôt dans le processus ou de recourir à l’adresse IP.

    Régulation du trafic vers des endpoints coûteux

    Le throttling détermine la vitesse à laquelle les requêtes sont acceptées afin que des pics soudains ne submergent pas le service. La configuration suivante utilise le même middleware avec des intervalles très courts : au maximum dix requêtes par seconde pour l’API, et une seule requête de recherche tous les deux secondes par client, car la recherche est l’endpoint le plus gourmand en ressources.

    const rateLimit = require("express-rate-limit");
    
    // Throttle all API requests
    const apiThrottle = rateLimit({
      windowMs: 1000, // 1 second
      max: 10, // Allow up to 10 requests per second
    });
    
    // Apply a stricter throttle to resource-intensive endpoints
    const searchThrottle = rateLimit({
      windowMs: 2000, // 2 seconds
      max: 1, // Allow 1 request every 2 seconds
      message: "Please wait before sending another search request.",
    });
    
    app.use("/api", apiThrottle);
    
    app.get("/api/search", searchThrottle, (req, res) => {
      res.json({ results: [] });
    });
    

    En termes stricts, il s’agit toujours d’une limitation de fréquence avec de courtes fenêtres temporelles : les demandes excédentaires sont rejetées avec un code 429, sans être différées. Si vous souhaitez un véritable mécanisme de throttling qui ralentit les clients avant de les refuser, un package complémentaire comme express-slow-down ajoute des retards progressifs. Notez également que le stockage en mémoire par défaut compte les requêtes par processus ; donc, derrière un équilibreur de charge disposant de plusieurs instances, vous avez besoin d’un stockage partagé comme Redis pour que les limites soient respectées. Les dernières versions de express-rate-limit renomment également l’option max en limit ; consultez la documentation de la version que vous installez. Pour une alternative légère en TypeScript, consultez notre article sur un limiteur de fréquence minimal pour Express.

    6. Journalisation, surveillance et détection d’incidents

    Vous ne pouvez pas répondre à une attaque que vous ne voyez jamais. Le journalisation enregistre les requêtes et les réponses avec leurs métadonnées, leur contexte, leurs temps d’exécution et leurs codes d’erreur, afin que vous puissiez diagnostiquer les problèmes, effectuer des audits et comprendre l’utilisation réelle. Le suivi surveille les activités en temps réel, traque des indicateurs tels que la latence, les taux d’erreur et le débit, et met en évidence des anomalies pouvant signaler un abus, l’incapacité à atteindre les objectifs de niveau de service ou une vulnérabilité exploitée.

    Les principales approches, avec leurs avantages et inconvénients :

    • Journalisation des requêtes à l’aide de middleware comme Morgan permet d’enregistrer chaque requête HTTP entrante à moindre coût, mais ne fournit aucune information sur la santé du système.
  • Journalisation des applications avec Winston, Pino ou Bunyan enregistre les événements métier et de sécurité tels que les connexions, les opérations sur la base de données et les erreurs. Les systèmes plus importants nécessitent une sortie structurée et un stockage centralisé pour en tirer parti.
  • Métriques avec Prometheus et Grafana suivent les taux de requêtes, la latence, le CPU, la mémoire et les taux d’erreur. Ils montrent des tendances globales mais pas ce qui s’est passé au sein d’une seule requête.
  • Gestion centralisée des journaux avec l’ELK Stack, OpenSearch, Splunk ou CloudWatch agrège les journaux provenant de différents services, au prix d’une infrastructure supplémentaire et d’un travail opérationnel accru.
  • Surveillance des performances des applications avec Datadog, New Relic ou Dynatrace combine journaux, métriques, traces et alertes sur une même plateforme, ce qui entraîne des coûts de licence plus élevés et une complexité accrue de la plateforme.
  • Tout ceci se connecte à Express. Voici quatre éléments de base courants.

    Journalisation des requêtes avec Morgan

    L’enregistrement de Morgan au format combined écrit une ligne de style Apache pour chaque requête, incluant la méthode, l’URL, le statut, la taille de la réponse et l’agent utilisateur.

    const express = require("express");
    const morgan = require("morgan");
    
    const app = express();
    
    // Log every incoming request
    app.use(morgan("combined"));
    
    app.get("/api/users", (req, res) => {
      res.json({ message: "Users retrieved successfully." });
    });
    

    Journalisation structurée des applications avec Winston

    Winston enregistre les événements sous forme d’objets structurés. La journalisation des identifiants de l’utilisateur et des IDs de commande lors de leur création génère un historique d’audit que l’on peut rechercher ultérieurement.

    const winston = require("winston");
    
    const logger = winston.createLogger({
      transports: [
        new winston.transports.Console(),
      ],
    });
    
    app.post("/api/orders", (req, res) => {
      logger.info("Order created", {
        userId: req.user.id,
        orderId: req.body.id,
      });
    
      res.status(201).json({ message: "Order created." });
    });
    

    Faites attention à ce qui est inclus dans les journaux. Les identifiants d’utilisateur sont acceptables ; en revanche, les mots de passe, les tokens, les numéros complets de carte bancaire et l’intégralité du corps des requêtes ne le sont pas, et les journaux constituent une source fréquente de fuites de données sensibles.

    Journalisation centralisée des erreurs

    500 générique afin que les traces d’exécution et les détails internes n’atteignent jamais le client.

    app.use((err, req, res, next) => {
    /* Logger is built as an independent module or class */
      logger.error(err.message, {
        path: req.originalUrl,
        method: req.method,
      });
    
      res.status(500).json({
        error: "Internal Server Error",
      });
    });
    

    Exposition des métriques à Prometheus

    /metrics afin que Prometheus puisse les récupérer.

    const client = require("prom-client");
    
    client.collectDefaultMetrics();
    
    app.get("/metrics", async (req, res) => {
      res.set("Content-Type", client.register.contentType);
      res.end(await client.register.metrics());
    });
    

    Cette endpoint révèle des détails internes concernant votre service ; restreignez donc son accès au réseau de surveillance uniquement, ou mettez-la derrière une authentification, plutôt que de la laisser accessible au public.

    Les frameworks dotés d’une philosophie bien définie sont utiles en la matière. NestJS intègre un générateur de journaux intégré ainsi qu’une structure qui permet d’ajouter facilement des fonctionnalités de journalisation et de suivi des métriques pour chaque point d’entrée, tandis qu’avec un framework sans philosophie précise comme Express, il faut ajouter manuellement des fonctionnalités de journalisation, généralement sous forme de middleware exécuté avant l’envoi de la réponse. La détection d’incidents s’appuie alors sur ces journaux : des alertes pour les schémas suspects, tels que des pics de réponses 401 ou des rejets liés aux limites de fréquence, sont envoyées vers le canal de notification que votre équipe surveille réellement.

    La sécurité en tant que processus continu

    Aucun article ne couvre tout, mais avant toute publication, vous pouvez vous assurer que votre API répond à ces critères de base :

    • Chaque point d’entrée est accessible uniquement via HTTPS.
    • Un mécanisme OAuth ou un flux de token équivalent est en place.
    • Les JWT générés ont une date d’expiration définie.
    • Des limites protègent toutes les routes, avec des restrictions plus strictes pour la page de connexion.
  • Chaque entrée est validée avant utilisation.
  • Les requêtes ont été testées pour détecter d’éventuelles tentatives d’injection.
  • Les règles d’accès ont été testées, y compris les vérifications au niveau des objets.
  • Les clés et les secrets sont stockés en dehors du code source.
  • Les événements liés à la sécurité sont enregistrés.
  • Des tableaux de bord et des alertes ont été configurés.
  • Les réponses incluent des en-têtes de sécurité.
  • Les clients ne voient jamais d’informations sur les traces d’exécution ni de détails d’erreurs internes.
  • Les dépendances sont à jour et ont fait l’objet d’une vérification.
  • Express a été utilisé ici à titre d’exemple, mais les mêmes principes s’appliquent également aux autres frameworks backend, la plupart d’entre eux intégrant directement ces outils ou offrant des équivalents natifs. NestJS, par exemple, gère la validation des requêtes à l’aide d’objets de transfert de données. La documentation de votre framework indiquera la version idiomatique de chaque couche.

    Ces mêmes principes constituent également la base des déploiements cloud natives sur des plateformes telles qu’Azure, Google Cloud et AWS, qui y ajoutent leurs propres passerelles, services d’identité et mécanismes de limitation de débit gérés. Les pratiques spécifiques au cloud méritent un traitement distinct, mais les couches mentionnées ci-dessus suffisent déjà à amener une API à un niveau satisfaisant pour passer un examen de sécurité.

    Points clés

    • Séparer la vérification des identifiants de l’authentification par requête : vérifier le mot de passe une seule fois, puis s’appuyer sur des tokens signés à durée de vie courte.
    • L’authentification n’est pas l’autorisation. Valider les scopes et les rôles, tout en vérifiant la propriété de chaque objet touché par une requête.
    • Utiliser AES-GCM avec un IV unique par opération pour les données au repos ; réserver le chiffrement asymétrique aux petites valeurs et à l’échange de clés.
  • Vérifiez le type de contenu des couches, effectuez la validation et la nettoyage des données, utilisez des requêtes paramétrées, et réfléchissez à l’ordre dans lequel elles s’exécutent.
  • Limitez la fréquence d’accès à tout, appliquez des limites plus strictes aux connexions de connexion et aux endpoints gourmands en ressources, et utilisez un stockage partagé lorsque vous exécutez plus d’une instance.
  • Enregistrez les événements liés à la sécurité sans consigner de secrets, et transformez ces journaux en alertes sur lesquelles quelqu’un pourra agir.
  • Lectures complémentaires