Skip to Content

Chapitre 10.4 — Node, npm & SSRF

⏱️ TL;DR — Deux risques propres au runtime Node. La supply chain npm : un projet tire des centaines de dépendances transitives ; chacune est du code tiers exécuté chez toi. On surveille les vulnérabilités connues (npm audit, Dependabot), on verrouille (lockfile), on se méfie du typosquatting et des scripts post-install (Partie 12 en détail). Le SSRF : tout fetch côté serveur (Server Action, route handler) vers une URL fournie par l’utilisateur peut être détourné vers le réseau interne / les métadonnées cloud (Partie 8) — allow-list obligatoire. Enfin, quelques pièges Node : child_process (command injection), la désérialisation, et ne pas exposer de stacktraces.

🎯 Objectifs

  • Gérer la supply chain npm : npm audit, lockfile, Dependabot, vigilance typosquatting/scripts.
  • Repérer et fermer les SSRF via fetch côté serveur (allow-list).
  • Éviter les pièges Node : child_process (command injection), erreurs exposées.
  • Intégrer ces vérifications à la CI.

La supply chain npm : du code tiers chez toi

Un projet Node/Next.js « minimal » installe souvent des centaines de paquets (dépendances directes et transitives). Chacun est du code tiers qui s’exécute avec les droits de ton application — à l’install (scripts) comme à l’exécution. C’est une surface d’attaque majeure (Partie 12), et le vecteur de nombreux incidents réels (paquets compromis, mainteneurs piratés).

Les mesures d’hygiène de base (approfondies en Partie 12) :

npm audit # signale les dependances aux vulnerabilites connues npm audit fix # applique les correctifs compatibles npm ci # installe EXACTEMENT le lockfile (reproductible, pour la CI)
  • Surveiller les vulnérabilités : npm audit (+ Dependabot/Renovate pour les PR de mise à jour automatiques). Intègre npm audit à la CI (échec si vulnérabilité critique).
  • Verrouiller : committer le lockfile (package-lock.json) et installer avec npm ci (installation exacte et reproductible), pas npm install en CI.
  • Se méfier du typosquatting : un paquet au nom proche d’un populaire (reakt, crossenv) peut être malveillant. Vérifie le nom exact, le mainteneur, la popularité avant d’ajouter une dépendance.
  • Scripts post-install : npm install exécute des scripts (postinstall) des paquets — un vecteur d’exécution de code. On peut les désactiver (--ignore-scripts) pour les paquets qui n’en ont pas besoin, et surveiller ceux qui en ont.
  • Réduire : moins de dépendances = moins de surface. Évite d’ajouter un paquet entier pour une fonction triviale.

⚠️ Piègenpm install en CI (au lieu de npm ci) peut résoudre des versions différentes du lockfile (selon les plages ^/~), introduisant silencieusement une version non testée — voire compromise. Et ignorer les alertes npm audit laisse des CVE connues en production. Utilise npm ci (installation exacte du lockfile) en CI/CD, commit le lockfile, et traite les alertes d’audit plutôt que de les ignorer. La reproductibilité est une propriété de sécurité.

SSRF via fetch côté serveur

Rappel de la Partie 8 appliqué à Node : tout fetch (ou client HTTP) côté serveur — dans un Server Component, une Server Action, un route handler — vers une URL contrôlée par l’utilisateur est un vecteur SSRF. L’attaquant pointe l’URL vers le réseau interne ou les métadonnées cloud (http://169.254.169.254/) pour exfiltrer des credentials.

// ❌ SSRF : le serveur fetch une URL fournie par l'utilisateur export async function POST(req: Request) { const { url } = await req.json() const r = await fetch(url) // et si url pointe vers le reseau interne / les metadonnees ? return Response.json(await r.json()) } // ✅ Allow-list + rejet des IP internes (voir Partie 8) const HOTES_OK = new Set(['api.partenaire.com']) function urlAutorisee(u: string) { const p = new URL(u) return p.protocol === 'https:' && HOTES_OK.has(p.hostname) // + verifier l'IP resolue }

La parade est celle de la Partie 8 : allow-list de destinations, rejet des IP internes/privées/link-local (en vérifiant l’IP réellement contactée, redirections comprises), restriction du schéma/port, et segmentation réseau. Ne jamais fetch une URL brute venant du client.

Autres pièges Node

  • child_process : exec/execSync passent par un shell → command injection si une entrée y entre (Partie 6). Utilise execFile/spawn avec les arguments en tableau, sans shell: true. Mieux : évite d’appeler des commandes externes.
  • Désérialisation / eval : pas de eval, pas de désérialisation d’entrée non fiable (un JSON.parse est sûr — il ne crée pas de code —, mais des libs de désérialisation « riche » peuvent l’être moins).
  • Stacktraces exposées : ne renvoie jamais error.stack/error.message bruts au client (fuite d’infos, Partie 6). Loggue côté serveur, réponds un message générique.
  • Prototype pollution : un piège JS spécifique où un attaquant injecte des clés __proto__/constructor via un merge/parse non sûr pour polluer Object.prototype. Utilise des libs à jour, valide les entrées (Zod), évite les merges récursifs naïfs sur des objets non fiables.
  • ReDoS : une expression régulière mal conçue sur une entrée non bornée peut provoquer un déni de service (temps exponentiel). Se méfier des regex complexes sur des entrées longues.

💡 Réflexe — Trois automatismes Node : (1) npm ci + lockfile committé + npm audit en CI (supply chain) ; (2) allow-list pour tout fetch serveur vers une URL utilisateur (SSRF) ; (3) execFile/spawn en tableau (jamais exec avec une entrée), et pas de stacktrace au client. Ces réflexes couvrent l’essentiel des risques propres au runtime.

🎯 Côté attaquant — Sur une app Node, l’attaquant vise : les dépendances (une CVE connue non patchée, un paquet compromis) — il regarde ton package.json/lockfile s’il fuit ; les fetch serveur sur des URL qu’il fournit (SSRF vers les métadonnées cloud = jackpot) ; les endpoints qui appellent child_process (command injection) ; et les erreurs verbeuses (stacktraces) pour cartographier. La richesse de l’écosystème npm est aussi sa faiblesse : beaucoup de code tiers = beaucoup de surface.

🧭 Sur FormaCampus — Côté runtime, FormaCampus commit le lockfile, installe avec npm ci, et fait tourner npm audit (+ Dependabot) en CI (build cassé sur vulnérabilité critique). Tout fetch serveur vers une URL utilisateur (import de ressource, aperçu) passe par une allow-list + rejet des IP internes (Partie 8), et le service est segmenté pour ne pas joindre les métadonnées cloud. Aucun child_process.exec avec entrée (traitements via libs natives ou spawn en tableau), pas de stacktrace renvoyée au client (message générique + log serveur), et les entrées sont validées par Zod (ce qui limite aussi la prototype pollution). L’ajout d’une dépendance passe par une revue (nom, mainteneur, popularité) pour éviter le typosquatting.

✏️ Exercices

Exercice 1 — Durcis la CI. Une CI fait npm install puis déploie, et ignore les alertes npm audit « pour ne pas bloquer ». Cite deux améliorations de sécurité.

✅ Solution

(1) Remplacer npm install par npm ci : installation exacte et reproductible du package-lock.json (committé), au lieu de re-résoudre potentiellement des versions différentes (^/~) — évite d’introduire silencieusement une version non testée/compromise. (2) Intégrer npm audit (au moins --audit-level=high) comme étape bloquante de la CI, et brancher Dependabot/Renovate pour les mises à jour : ne plus ignorer les vulnérabilités connues, les corriger rapidement. Bonus : --ignore-scripts là où c’est possible, et une revue des nouvelles dépendances (typosquatting). La reproductibilité et le suivi des CVE sont des propriétés de sécurité.

Exercice 2 — Le fetch serveur. Un route handler récupère un flux RSS depuis une URL fournie par l’utilisateur : await fetch(body.rssUrl). Quel risque, et comment le fermer ?

✅ Solution

Risque : SSRF — l’URL vient du client, donc l’attaquant peut la pointer vers le réseau interne (http://localhost:..., IP privées) ou les métadonnées cloud (http://169.254.169.254/...) pour exfiltrer des credentials d’instance. Le serveur, à l’intérieur du réseau, atteint des ressources non exposées. Fermer : (1) allow-list de domaines autorisés (ou au minimum schéma https uniquement) ; (2) résoudre le DNS et rejeter toute IP interne/privée/link-local (en vérifiant l’IP réellement contactée, y compris après redirection — ne pas suivre aveuglément les redirections) ; (3) restreindre le port ; (4) segmenter le réseau pour que le serveur ne puisse pas joindre les métadonnées. Ne jamais fetch une URL brute fournie par l’utilisateur (Partie 8).

🧠 Quiz de révision

1. Pourquoi la supply chain npm est-elle une surface d’attaque majeure ?

Parce qu’un projet tire des centaines de dépendances (directes et transitives), chacune étant du code tiers exécuté avec les droits de ton app (à l’install et à l’exécution). Un paquet vulnérable ou compromis met en danger toute l’appli. On surveille (npm audit, Dependabot), on verrouille (lockfile, npm ci), on se méfie (typosquatting, scripts).

2. Pourquoi préférer npm ci à npm install en CI ?

npm ci installe exactement le package-lock.json (reproductible), alors que npm install peut re-résoudre des versions différentes (plages ^/~), introduisant silencieusement une version non testée voire compromise. La reproductibilité est une propriété de sécurité.

3. Où se cache le risque SSRF dans une app Next.js/Node ?

Dans tout fetch côté serveur (Server Component, Server Action, route handler) vers une URL fournie par l’utilisateur : l’attaquant la pointe vers le réseau interne ou les métadonnées cloud. Parade : allow-list + rejet des IP internes + segmentation (Partie 8).

4. Comment exécuter une commande système sans command injection en Node ?

Utiliser execFile/spawn avec les arguments en tableau, sans shell: true (les métacaractères deviennent des arguments littéraux). Éviter exec/execSync (qui passent par un shell) avec une entrée. Mieux : éviter d’appeler des commandes externes si une lib native suffit.

5. Cite deux autres pièges spécifiques au runtime JS/Node.

Par ex. : la prototype pollution (injection de clés __proto__/constructor via un merge/parse non sûr → polluer Object.prototype) ; le ReDoS (regex à complexité exponentielle sur une entrée non bornée → déni de service) ; l’exposition de stacktraces au client (fuite d’infos). On valide les entrées (Zod), on évite les merges naïfs et les regex risquées, on renvoie des erreurs génériques.


Fin de la Partie 10. Le stack JavaScript moderne est couvert : frontière client/serveur et secrets, Server Actions/route handlers (valider + autoriser), Auth.js et les limites du middleware, et le runtime Node (supply chain, SSRF). On aborde maintenant un cas EdTech central : la Partie 11 — Sécurité Moodle & LMS.

Last updated on