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 : toutfetchcô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
fetchcô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ègrenpm audità la CI (échec si vulnérabilité critique). - Verrouiller : committer le lockfile (
package-lock.json) et installer avecnpm ci(installation exacte et reproductible), pasnpm installen 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 installexé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ège —
npm installen CI (au lieu denpm 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 alertesnpm auditlaisse des CVE connues en production. Utilisenpm 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/execSyncpassent par un shell → command injection si une entrée y entre (Partie 6). UtiliseexecFile/spawnavec les arguments en tableau, sansshell: 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 (unJSON.parseest 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.messagebruts 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__/constructorvia un merge/parse non sûr pour polluerObject.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 auditen CI (supply chain) ; (2) allow-list pour toutfetchserveur vers une URL utilisateur (SSRF) ; (3)execFile/spawnen tableau (jamaisexecavec 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 ; lesfetchserveur sur des URL qu’il fournit (SSRF vers les métadonnées cloud = jackpot) ; les endpoints qui appellentchild_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 tournernpm audit(+ Dependabot) en CI (build cassé sur vulnérabilité critique). Toutfetchserveur 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. Aucunchild_process.execavec entrée (traitements via libs natives ouspawnen 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 à 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.