Web Application Security
Chapitre 7 / 9 · 16 min de lecture

Déni de service & dépendances tierces

Deux menaces souvent négligées : épuiser les ressources (DoS, ReDoS) et hériter des failles de ses dépendances — avec leurs parades.

Deux familles de risques figurent rarement en tête des préoccupations d'un développeur, et pourtant elles pèsent lourd. La première, le déni de service (Denial of Service, DoS), ne vole aucune donnée : elle rend simplement votre application indisponible en épuisant ses ressources — processeur, mémoire, connexions, bande passante. La seconde, les dépendances tierces, concerne tout le code que vous n'avez pas écrit : une vulnérabilité connue dans une bibliothèque que vous importez devient instantanément votre vulnérabilité.

Ce chapitre est, comme tous les autres, éducatif et défensif. Il décrit conceptuellement le mécanisme de ces menaces pour vous apprendre à les neutraliser dans votre propre code, jamais pour viser le système d'autrui. Hoffman le rappelle sans détour : les techniques de test ne s'appliquent qu'à vos propres applications, ou avec une autorisation écrite explicite du propriétaire. La plupart des programmes de bug bounty interdisent d'ailleurs purement et simplement les soumissions de DoS, car elles perturbent l'usage des utilisateurs légitimes.

Le déni de service en bref

Un DoS vise un seul but : consommer une ressource jusqu'à ce qu'il n'en reste plus pour les utilisateurs légitimes. Contrairement à une faille comme le XSS, qui est binaire (soit la faille existe, soit elle n'existe pas), le DoS se mesure sur une échelle. Une fonction lente sera invisible pour un utilisateur sur un puissant ordinateur de bureau, mais bloquera un mobile ancien. Il est donc plus juste de raisonner en termes de risque élevé / moyen / faible que de « vulnérable / sûr ».

Hoffman distingue trois objectifs possibles, qu'il faut tous garder en tête lorsqu'on bâtit un plan de défense :

  • épuiser les ressources du serveur (CPU, RAM, écritures disque, connexions) ;
  • épuiser les ressources du client (bloquer le navigateur de la victime) ;
  • demander des ressources indisponibles.

Note

Un DoS ne cause presque jamais de dommage permanent : il dégrade la disponibilité le temps de l'attaque. Mais cette dégradation peut aller du simple ralentissement à un verrouillage complet de l'application. Et certaines tentatives de DoS provoquent au passage des fuites de données via des logs ou des messages d'erreur — restez attentif à ce qui apparaît dans vos journaux pendant un pic anormal.

La toute première défense est donc transverse : journaliser (log) chaque requête avec son temps de réponse, et mesurer aussi les tâches asynchrones de fond (sauvegardes, jobs déclenchés par une API mais sans réponse immédiate). Sans cette observabilité, un DoS qui passe par un canal légitime — un point d'API ordinaire — est presque impossible à détecter après coup.

Le ReDoS : quand une regex bloque le serveur

Comment ça marche

Les expressions régulières (regex) servent partout : valider un champ de formulaire, limiter un mot de passe à certains caractères, plafonner la longueur d'un commentaire. Elles sont en général très rapides. Mais certaines regex peuvent être amenées à s'exécuter de façon catastrophiquement lente : c'est le déni de service par expression régulière (regex DoS, ReDoS). On parle de regex « malveillante » ou « démoniaque » (evil regex).

Le mécanisme tient en un mot : le retour sur trace (backtracking). Lorsqu'un groupe répété et ambigu échoue à matcher en fin de chaîne, le moteur revient en arrière pour essayer toutes les combinaisons possibles avant de conclure à l'échec. Hoffman donne l'exemple /^((ab)*)+$/. Sur l'entrée abab, l'évaluation est instantanée. Mais ajoutez un caractère qui invalide la fin — abababababababa avec un « a » de trop — et le moteur doit explorer un nombre de combinaisons qui double tous les deux caractères ajoutés :

EntréeTemps d'exécution
abab...a (23 caractères)8 ms
abab...a (25 caractères)15 ms
abab...a (27 caractères)31 ms
abab...a (29 caractères)61 ms

Cette croissance exponentielle finit par geler un cœur de processeur côté serveur, ou par faire planter complètement le navigateur côté client. Le plus pernicieux : cette regex n'est lente que pour certaines entrées précises. Elle peut donc dormir des années dans une base de code avant qu'une entrée piégée ne réveille le problème, semblant surgir de nulle part.

Attention

Les regex à risque suivent typiquement la forme (a[ab]*)+ : un groupe répété (*) lui-même placé sous un quantificateur gourmand (+). Le danger naît du quantificateur imbriqué appliqué à un groupe ambigu. C'est cette structure-là qu'il faut traquer en relecture de code.

Comment se défendre

Le ReDoS est, selon Hoffman, la forme de DoS la plus facile à prévenir — à condition de savoir la reconnaître. Les contre-mesures s'articulent autour de trois axes.

1. Empêcher les regex démoniaques d'entrer dans la base de code. Lors de la relecture, repérez le motif « quantificateur sur un groupe répété ambigu » et réécrivez-le sans imbrication. Réécrivez le groupe pour qu'une seule décomposition soit possible.

// ❌ Avant : groupe répété sous un quantificateur gourmand.
//    Backtracking catastrophique sur une entrée piégée.
const evil = /^((ab)*)+$/;
evil.test("ababababababababababababababa"); // peut geler le thread

// ✅ Après : un seul quantificateur, aucune ambiguïté.
//    "ab" répété 0..N fois, sans groupe imbriqué.
const safe = /^(?:ab)*$/;
safe.test("ababababababababababababababa"); // instantané

Comme distinguer les regex sûres des regex démoniaques sans faux positifs est difficile, c'est l'un des cas où un outil d'analyse statique (un linter dédié aux regex, ou un testeur de performance de regex) rend de grands services. Intégrez-le à votre pipeline pour qu'il signale toute regex syntaxiquement suspecte.

2. Ne jamais accepter de regex fournie par l'utilisateur. Hoffman est catégorique : laisser un utilisateur téléverser ses propres expressions régulières, « c'est marcher dans un champ de mines en espérant avoir mémorisé la carte ». Vérifiez aussi qu'aucune dépendance intégrée n'utilise de regex fournies par l'utilisateur ni de regex mal écrites.

3. Borner les entrées et le temps d'exécution. Limitez la taille des entrées avant de les passer au moteur de regex : une chaîne plafonnée à une longueur raisonnable réduit mécaniquement l'amplitude du backtracking. Et prévoyez un délai d'expiration (timeout) au-delà duquel l'évaluation est abandonnée.

// ✅ Borner l'entrée AVANT de valider : un champ trop long
//    est rejeté sans même invoquer le moteur de regex.
const MAX = 256;

function validerPseudo(saisie) {
  if (typeof saisie !== "string" || saisie.length > MAX) {
    return false; // rejet immédiat, coût constant
  }
  return /^[a-z0-9_]{3,32}$/i.test(saisie);
}

Le DoS logique : une opération coûteuse à volonté

Comment ça marche

Un DoS logique (logical DoS) ne repose sur aucune faille de syntaxe : il exploite une opération coûteuse que l'application autorise à déclencher à volonté. Ce sont les DoS les plus difficiles à détecter, car ils empruntent des canaux parfaitement légitimes. Les opérations à surveiller sont celles qui consomment des ressources critiques :

  • toute opération que vous confirmez synchrone ;
  • les écritures en base de données et sur disque ;
  • les jointures SQL ;
  • les sauvegardes de fichiers ;
  • les boucles logiques.

Une seule route d'API complexe en cumule souvent plusieurs. Hoffman prend l'exemple d'une application de partage de photos exposant GET /metadata/:userid. Pour un compte neuf (1 album, 1 photo), la réponse arrive en 120 ms. Pour un power user (28 albums, 490 photos), elle grimpe à 1 870 ms. L'opération passe à l'échelle avec les données du compte : un attaquant qui gonfle artificiellement son propre compte (600 albums, 3 500 photos) puis martèle ce point d'accès draine les ressources du serveur pour tout le monde — la requête peut même expirer côté logiciel pendant que la base, elle, continue de mobiliser des ressources.

Comment se défendre

Un système sans logique exploitable ne tombe généralement pas victime d'un DoS logique. La défense consiste donc à identifier les zones du code qui consomment des ressources critiques, puis à les encadrer.

Limitation de débit (rate limiting). C'est la parade centrale : plafonner le nombre de requêtes qu'un même client peut adresser à une opération coûteuse dans une fenêtre de temps donnée. Au-delà du quota, on renvoie un 429 Too Many Requests.

// ✅ Limitation de débit sur une route coûteuse (Express).
import rateLimit from "express-rate-limit";

const limiteMetadata = rateLimit({
  windowMs: 60 * 1000, // fenêtre d'une minute
  max: 20, // 20 requêtes max par IP et par fenêtre
  standardHeaders: true,
  message: "Trop de requêtes, réessayez plus tard.",
});

app.get("/metadata/:userid", limiteMetadata, handlerMetadata);

Pagination et quotas. Plutôt que de renvoyer toutes les métadonnées d'un coup, imposez une pagination : l'opération ne traite jamais plus d'un lot borné, quel que soit le volume du compte. Le coût par requête devient ainsi prévisible et plafonné.

// ❌ Avant : coût proportionnel à la taille du compte.
async function handlerMetadata(req, res) {
  const tout = await db.metadataPourUtilisateur(req.params.userid);
  res.json(tout); // peut renvoyer des milliers d'enregistrements
}

// ✅ Après : pagination bornée, coût par requête plafonné.
async function handlerMetadata(req, res) {
  const limite = Math.min(Number(req.query.limit) || 50, 100);
  const page = await db.metadataPagee(req.params.userid, {
    limite,
    apres: req.query.cursor,
  });
  res.json(page); // au plus 100 enregistrements
}

Délais d'expiration sur les opérations. Imposez un timeout aux opérations longues (requêtes en base, appels réseau) afin qu'aucune ne mobilise indéfiniment une ressource. Et privilégiez l'asynchrone : une tâche réellement coûteuse (sauvegarde, traitement lourd) devrait être déléguée à une file d'attente de fond plutôt que d'occuper la requête HTTP.

Piège courant

Un timeout côté serveur applicatif ne suffit pas toujours. Comme le note Hoffman, même si le serveur abandonne et ne renvoie rien au client, la base de données peut continuer à coordonner des ressources pour la requête abandonnée. Pensez à propager l'annulation jusqu'à la couche de données (timeout de requête côté base, annulation explicite).

Le DDoS : surtout l'affaire de l'infrastructure

Le déni de service distribué (Distributed Denial of Service, DDoS) est la variante la plus médiatisée : un grand nombre de machines compromises — un réseau de robots (botnet) — submerge le serveur de trafic d'apparence légitime. Ces machines sont de vrais ordinateurs, mobiles ou objets connectés (routeurs, points d'accès…) infectés par un malware qui les pilote à distance, ce qui rend le tri entre clients légitimes et illégitimes très ardu.

Contrairement au DoS d'un attaquant unique, qui vise généralement un bug applicatif, le DDoS attaque le plus souvent au niveau réseau : il vise directement l'adresse IP du serveur, typiquement avec du trafic UDP, pour noyer la bande passante disponible. Il ne cible donc pas un point d'API précis. Un DDoS ne se prévient pas, il se mitige — et cette mitigation relève avant tout de l'infrastructure :

  • Service de gestion de bande passante (CDN/WAF). La parade la plus simple consiste à déléguer la protection volumétrique à un fournisseur qui analyse chaque paquet en amont et n'achemine vers votre serveur que le trafic jugé sain. Ces services interceptent des volumes de requêtes que l'infrastructure d'une petite application ne pourrait jamais absorber.
  • Trou noir (blackholing). On route le trafic suspect ou répété vers un serveur « trou noir » qui imite votre application mais n'exécute rien, tandis que le trafic légitime atteint le vrai serveur. Efficace contre les petites attaques, cette technique tient mal à grande échelle et peut détourner du trafic légitime si le ciblage manque de précision.

À retenir

Tout filtre anti-DDoS trop sensible bloquera aussi des utilisateurs légitimes. Avant de déployer une mitigation agressive, mesurez finement les schémas d'usage réels de vos utilisateurs : c'est la seule façon de distinguer un pic légitime d'une attaque. Et même si vous ne vous croyez pas une cible, prévoyez ces mitigations « au cas où » : elles s'activent quand le besoin survient.

Tableau de synthèse : menaces DoS et contre-mesures

Menace / vecteurContre-mesure principale
ReDoS (regex à backtracking)Regex sans quantificateur imbriqué ; linter de regex
Regex fournie par l'utilisateurInterdire ; ne pas accepter de regex externe
Entrée trop longue passée à une regexPlafonner la taille de l'entrée ; timeout d'évaluation
DoS logique (opération coûteuse à volonté)Limitation de débit ; pagination ; quotas
Requête longue mobilisant la baseTimeouts propagés jusqu'à la couche de données ; jobs asynchrones
DoS difficile à détecter après coupJournalisation des temps de réponse, y compris des tâches de fond
DDoS volumétrique (niveau réseau)CDN / WAF / gestion de bande passante ; blackholing

Les dépendances tierces : hériter des failles des autres

Le logiciel moderne est bâti sur l'open source (OSS) : Reddit, Twitch, YouTube, LinkedIn reposent tous sur des bibliothèques tierces. C'est commode, mais cela pose un risque de sécurité réel. Une chaîne n'a que la solidité de son maillon le plus faible, et ce maillon est souvent celui qui a subi le moins de contrôle qualité.

Pourquoi ? Parce que vous dépendez d'un code qui n'a pas été audité avec la même rigueur que le vôtre. Auditer une grande base OSS est coûteux, et c'est une analyse à un instant T : le code OSS évolue en permanence, et auditer chaque pull request entrante serait ruineux. La plupart des organisations acceptent donc ce risque en échange d'un gain de temps de développement. On parle de risque de chaîne d'approvisionnement (supply chain).

L'arbre de dépendances

Le piège le plus sous-estimé : vos dépendances ont elles-mêmes des dépendances — les dépendances de quatrième niveau (fourth-party). L'ensemble forme un arbre de dépendances (dependency tree) qui, dans une grande application, peut compter des milliers de sous-dépendances. Pire, le même paquet y apparaît souvent en plusieurs versions :

Application v1.6 → JQuery 3.4.0
Application v1.6 → Framework SPA v1.3.2 → JQuery 2.2.1
Application v1.6 → Bibliothèque UI v4.5.0 → JQuery 2.2.1

Or la version 2.2.1 peut comporter des vulnérabilités critiques absentes de la 3.4.0. Il faut donc évaluer chaque dépendance unique, et chaque version unique de chaque dépendance. À cette échelle, la revue manuelle est impossible : l'évaluation doit être automatisée.

# Inventorier l'arbre complet (top-level + sous-dépendances).
npm ls

# Lister sur une profondeur donnée, pour comparer
# ensuite à une base de vulnérabilités connues.
npm list --depth=5

Scanner les vulnérabilités connues

Heureusement, les vulnérabilités découvertes dans les paquets populaires sont publiées dans des bases comme la National Vulnerability Database (NVD) du NIST ou la base CVE (Common Vulnerabilities and Exposures) de Mitre, avec un score de gravité. Les grandes dépendances très utilisées — jQuery est sur plus de 10 millions de sites — ont déjà été scrutées par de nombreuses entreprises : la plupart des failles sérieuses y sont documentées.

La défense concrète consiste à comparer en continu votre arbre de dépendances à ces bases CVE, de façon automatisée. C'est exactement ce que font les outils de l'écosystème :

# Auditer l'arbre npm contre la base de vulnérabilités connues.
npm audit

# Appliquer les correctifs disponibles (mises à jour compatibles).
npm audit fix

Vous pouvez aussi brancher un scanner tiers (Snyk) ou un robot qui ouvre automatiquement des pull requests de mise à jour (Dependabot) pour ne jamais accumuler de retard sur les correctifs. Privilégiez une base financée par l'État, comme le NIST, gage de pérennité.

Note

Les bases CVE sont peu utiles pour les petits paquets confidentiels (un dépôt à deux contributeurs téléchargé trois cents fois) : personne ne les a audités. Ce constat plaide pour la règle suivante — minimiser le nombre de dépendances et préférer les paquets matures et largement scrutés.

Verrouiller et vérifier l'intégrité des versions

Par défaut, npm précède chaque dépendance d'un accent circonflexe (^) qui autorise les montées de version automatiques : ^1.0.23 pourra passer à 1.0.24 à votre insu. Pour les dépendances très intégrées à votre cœur applicatif, Hoffman recommande d'auditer une version précise, puis de la verrouiller.

// ❌ Avant : le caret autorise une montée de patch automatique.
{
  "dependencies": {
    "ma-lib": "^1.0.23"
  }
}

// ✅ Après : version exacte, auditée et figée.
{
  "dependencies": {
    "ma-lib": "1.0.23"
  }
}

Mais retirer le caret ne fige que la dépendance de premier niveau, pas ses descendantes. C'est le rôle du fichier de verrouillage (lockfile) : package-lock.json (ou la commande historique npm shrinkwrap, qui génère npm-shrinkwrap.json) fige tout l'arbre à des versions exactes. On élimine ainsi le risque qu'une sous-dépendance se mette à jour vers du code vulnérable.

Attention

Verrouiller la version ne protège pas si un mainteneur republie du code différent sous un numéro de version existant. Honorer la règle « nouveau code → nouveau numéro » dépend entièrement du mainteneur. Pour fermer cette brèche, le lockfile enregistre un hash d'intégrité (integrity) ; pour aller plus loin, Hoffman suggère de référencer des SHA Git précis ou de déployer votre propre miroir npm contenant les versions exactes auditées.

Paquets malveillants et typosquatting

Au-delà des failles accidentelles, l'écosystème a connu des attaques délibérées sur la chaîne d'approvisionnement. Hoffman cite des cas réels :

  • eslint-scope (2018) : les identifiants du mainteneur sont compromis ; une nouvelle version publiée vole les identifiants locaux de toute machine qui l'installe.
  • event-stream / flatmap-stream (2018) : une dépendance malveillante est ajoutée pour dérober les portefeuilles Bitcoin des utilisateurs.
  • left-pad (2016) : le retrait d'un minuscule utilitaire casse le pipeline de millions d'applications — illustrant la fragilité d'une dépendance d'apparence anodine.

Le code malveillant est conçu pour passer inaperçu : il combine ingénierie sociale et obfuscation. Méfiez-vous donc des paquets installés par erreur de frappe (typosquatting — un nom proche d'un paquet légitime) et des scripts d'installation (postinstall), qui s'exécutent automatiquement, souvent avec des privilèges élevés.

Piège courant

Les installeurs auto-configurés (à la WordPress, ou un script postinstall) sont, selon Hoffman, le mode d'intégration le plus risqué : ils requièrent des privilèges élevés et peuvent aboutir à une porte dérobée d'exécution de code à distance (RCE) tournant en administrateur. Si vous devez en passer par là, analysez le script du dépôt OSS avant de l'exécuter, et désactivez les scripts d'installation par défaut (npm ci --ignore-scripts) pour les dépendances non vérifiées.

Isoler les dépendances risquées

Enfin, appliquez le principe du moindre privilège (principle of least authority) aux dépendances elles-mêmes. Une dépendance compromise peut accaparer les ressources et les fonctionnalités de votre serveur applicatif si elle y tourne directement. La séparation des préoccupations consiste à exécuter une intégration risquée sur son propre serveur (idéalement maintenu par vous), et à communiquer avec elle par HTTP en échangeant du JSON.

Le format JSON garantit qu'aucune exécution de script n'est possible sur votre serveur applicatif sans un enchaînement de vulnérabilités supplémentaires, et fait se comporter la dépendance comme une fonction pure tant que vous n'y persistez pas d'état. Le compromis : une latence accrue, et le fait que des données confidentielles transmises restent exposées si le pare-feu est mal configuré. À défaut d'un serveur dédié, isolez au moins la dépendance dans son propre environnement aux ressources cloisonnées.

Tableau de synthèse : risques de dépendances et contre-mesures

Menace / vecteurContre-mesure principale
Faille connue dans une dépendance (CVE)Scanner (npm audit, Snyk, Dependabot) en continu
Sous-dépendances (4e niveau) non visiblesInventorier l'arbre (npm ls) ; évaluation automatisée
Montée de version automatique vers du vulnérableRetirer ^ ; lockfile / npm shrinkwrap
Republication sous un numéro existantHash d'intégrité ; SHA Git ; miroir npm privé
Surface d'attaque excessiveMinimiser le nombre de dépendances ; préférer le mature
Typosquatting / paquet malveillantVérifier le nom ; auditer une version précise avant de figer
Script d'installation malveillant--ignore-scripts ; analyser le script ; éviter les installeurs auto
Dépendance compromise prenant le contrôleMoindre privilège ; isolation sur serveur dédié (JSON via HTTP)

À retenir

  • Le DoS se mesure sur une échelle, pas en binaire : raisonnez en risque élevé/moyen/faible, et journalisez les temps de réponse (tâches de fond comprises) pour le détecter.
  • ReDoS : fuyez les quantificateurs imbriqués sur des groupes ambigus, refusez toute regex fournie par l'utilisateur, bornez la taille des entrées et posez des timeouts.
  • DoS logique : encadrez les opérations coûteuses par la limitation de débit, la pagination, les quotas et des délais d'expiration propagés jusqu'à la base.
  • DDoS : il se mitige plutôt qu'il ne se prévient — déléguez la protection volumétrique à un CDN/WAF, en mesurant d'abord vos usages légitimes.
  • Dépendances tierces : leur faille devient la vôtre (supply chain). Inventoriez l'arbre, scannez les CVE en continu, mettez à jour, et minimisez le nombre de paquets.
  • Verrouillez les versions (lockfile + hash d'intégrité), méfiez-vous du typosquatting et des scripts d'installation, et isolez par moindre privilège les dépendances risquées.
  • Cadre éthique : ne testez ces failles que sur vos propres systèmes ou avec autorisation écrite ; pratiquez la divulgation responsable.