Skip to main content

Dans les coulisses de la compromission de keyv sur npm : malware preinstall, provenance fiable et hooks d’IDE

feature insights context

4 août 2026

0 minutes de lecture

Le 4 août 2026, des attaquants ont compromis le processus de publication de keyv et de packages npm associés. Les versions malveillantes ajoutent un hook preinstall qui exécute un chargeur obfusqué avant le démarrage du code de l’application, puis lance une charge utile de deuxième étape beaucoup plus volumineuse.

Il s’agit d’un incident actif touchant la chaîne logistique logicielle, et non d’une preuve de concept. Snyk Security Research a téléchargé et comparé de façon indépendante les archives publiées sans les installer, répertorié tous les packages renvoyés par npm pour le mainteneur jaredwray et identifié 11 versions malveillantes contenant les mêmes deux fichiers de charge utile. À 11:16 UTC, huit de ces versions portaient encore le tag latest.

En bref

  • Incident : Code malveillant intégré à des packages npm légitimes

  • Package principal : keyv@6.0.0

  • Versions touchées identifiées par Snyk : 11 parmi keyv, les packages liés à cacheable et ecto

  • Exécution : "preinstall": "node setup.mjs"

  • Charge utile : setup.mjs charge une deuxième étape de 727 680 octets nommée Math_Symbol.js

  • État observé à 11:16 UTC : Huit versions malveillantes portaient toujours le tag latest. Trois avaient été supprimées.

  • Avis : SNYK-JS-KEYV-18515941

  • CVE/GHSA : Aucun CVE ni GHSA n’avait été attribué au moment de l’analyse

  • Gravité opérationnelle : Critique, car l’installation permet l’exécution de code contrôlé par l’attaquant avec les privilèges du développeur ou de l’exécuteur CI

  • Remédiation : Épinglez une version ou revenez à une version antérieure connue comme saine. Aucun successeur sain de keyv@6.0.0 n’avait été publié.

  • Action immédiate : N’installez pas les versions touchées. Si l’une d’elles a été exécutée, isolez l’hôte, recherchez et désactivez les mécanismes de persistance, puis faites tourner les identifiants exposés depuis un système sain.

Gravité et statut de l’avis

Snyk a publié SNYK-JS-KEYV-18515941 pendant la rédaction de cet article. L’avis classe keyv@6.0.0 comme du code malveillant intégré, selon CWE-506.

Aucun CVE ni avis GitHub n’avait été attribué pendant nos recherches. L’installation concernée exécute du code contrôlé par l’attaquant sans privilèges supplémentaires ni interaction avec l’application, en utilisant les autorisations et identifiants accessibles au développeur ou au processus CI.

Ce que Snyk a confirmé de manière indépendante

Nous avons interrogé le point de terminaison de recherche du registre npm pour les packages maintenus par jaredwray. Il a renvoyé 61 noms de packages. Pour chacun, nous avons récupéré le manifeste du registre, examiné toutes les versions publiées le 4 août et vérifié leurs scripts de cycle de vie ainsi que les métadonnées de leurs archives.

Cette vérification a révélé les versions malveillantes suivantes et confirmé que les autres packages dans l’espace de noms @keyv n’avaient pas été compromis :

La dernière entrée est importante pour délimiter l’incident. ecto@5.0.1 est apparu après les premiers avertissements publics, et ses fichiers setup.mjs et Math_Symbol.js sont identiques octet pour octet à ceux de keyv@6.0.0. Les premières listes de packages qui omettent ecto sont incomplètes.

À 11:16 UTC, notre instantané indiquait que npm avait supprimé flat-cache@6.1.24, cacheable-request@13.0.20 et cache-manager@7.2.10, et replacé leurs tags latest sur 6.1.23, 13.0.19 et 7.2.9. Les huit autres versions touchées étaient toujours présentes et portaient le tag latest. L’état du registre peut évoluer rapidement pendant un incident actif : les miroirs privés et les fichiers de verrouillage restent donc des éléments de l’enquête, même après la suppression d’une version par npm.

L’archive publiée diffère en trois points

Nous avons récupéré keyv@6.0.0 et sa version candidate, keyv@6.0.0-rc.1, directement depuis le registre, puis comparé chaque fichier avec SHA-256. Nous n’avons ni installé le package ni exécuté l’une ou l’autre des charges utiles.

Seuls trois chemins diffèrent :

  • package.json est passé de la version 6.0.0-rc.1 à 6.0.0, les deux fichiers de charge utile ont été ajoutés à la liste des fichiers publiés et le script preinstall a été ajouté.

  • setup.mjs a été ajouté (29 918 octets).

  • Math_Symbol.js a été ajouté (727 680 octets).

Tous les fichiers de dist/ sont identiques octet pour octet entre la version candidate et la version stable compromise. La bibliothèque continue de fonctionner normalement après l’installation, tandis que le hook de cycle de vie s’exécute séparément. Ce petit diff est un indice de détection utile et une technique de dissimulation efficace.

La modification du manifeste est explicite :

 "scripts": {
   "build": "tsdown",
+  "preinstall": "node setup.mjs"
 },
 "files": [
   "dist",
   "LICENSE",
+  "setup.mjs",
+  "Math_Symbol.js"
 ]

Le manifeste du registre de keyv@6.0.0 indique la valeur d’intégrité suivante pour l’archive :

sha512-N/n4R+nD5SC0fYOpAp4ZnbwwxqGVodgEZ9D7Gm/VBocorU0aQimVyleDWSY6/axdO0/temub760n3hnMppZpUg==

Voici les empreintes calculées de façon indépendante :

keyv-6.0.0.tgz
sha256 d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782

setup.mjs, 29,918 bytes
sha256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668

Math_Symbol.js, 727,680 bytes
sha256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc

Nous avons ensuite répété la vérification des empreintes de la charge utile sur chaque version touchée récupérable lors de notre première analyse. Les neuf archives alors disponibles contenaient toutes le même fichier setup.mjs de 29 918 octets et le même fichier Math_Symbol.js de 727 680 octets, avec exactement les empreintes indiquées ci-dessus. npm a supprimé l’une de ces versions par la suite. Pour la détection, appuyez-vous sur les empreintes et le comportement du cycle de vie, plutôt que sur un seul nom de package.

Fonctionnement du malware

npm exécute automatiquement preinstall lors de l’installation des dépendances. Le développeur n’a pas besoin d’importer keyv, de démarrer une application ni d’appeler une API vulnérable. La résolution et l’installation du package concerné suffisent.

L’analyse statique de setup.mjs révèle des vérifications de plateforme pour Linux, macOS et Windows, l’utilisation de child_process.execFileSync, des opérations sur le système de fichiers, un accès HTTPS et une gestion de l’environnement d’exécution Bun. Le chargeur moins obfusqué, ajouté sous .claude/setup.mjs, identifie la version Bun 1.3.13 et le nom du fichier de deuxième étape, math_init.js. Le chargeur peut récupérer sur GitHub une version adaptée de Bun si Bun est absent, exécuter la charge utile plus volumineuse et supprimer les fichiers temporaires de l’environnement d’exécution.

L’analyse indépendante du malware fournie avec les rapports d’incident indique que la deuxième étape chiffrée cible les jetons GitHub et npm, les identifiants cloud, les clés privées, les chaînes de connexion aux bases de données, les jetons Vault, les jetons de compte de service Kubernetes et la mémoire des exécuteurs GitHub Actions. Elle signale également un mécanisme de persistance gh-token-monitor, qui surveille un jeton GitHub volé et exécute un gestionnaire fourni lorsque le jeton cesse de fonctionner.

Nous n’avons pas exécuté ni déchiffré nous-mêmes Math_Symbol.js ; ces détails sur les capacités de la deuxième étape doivent donc rester attribués à leur source. Les recommandations opérationnelles concordent avec l’analyse antérieure de Snyk sur le même mécanisme de persistance gh-token-monitor : contenir et désactiver le moniteur avant de révoquer les jetons.

Un second vecteur d’exécution cible les outils de développement

Le hook de cycle de vie npm n’est qu’un point d’entrée. Un enregistrement de l’API GitHub pour le commit d8c850c7 montre l’ajout de cinq fichiers au dépôt keyv :

  • .claude/settings.json

  • .claude/setup.mjs

  • .claude/math_init.js

  • .vscode/tasks.json

  • .vscode/setup.mjs

La configuration Claude enregistre une commande SessionStart qui appelle .vscode/setup.mjs. La tâche VS Code utilise runOn: "folderOpen" et appelle .claude/setup.mjs. Ces fichiers créent un autre vecteur d’exécution lorsqu’un développeur ou un agent de programmation fait confiance à la configuration locale du projet et l’active. Selon la confiance accordée à l’espace de travail et les paramètres de l’utilisateur, VS Code peut demander confirmation avant d’autoriser les tâches automatiques. L’ouverture d’un dépôt cloné constitue donc une condition d’exposition, pas la preuve que du code a été exécuté.

GitHub a vérifié cryptographiquement le commit, dont l’identité d’auteur est github-actions[bot]. Le badge de vérification prouve que GitHub a signé l’objet commit. Il ne prouve pas que la modification a été autorisée par le mainteneur du projet. Les éléments disponibles étayent la compromission d’un compte, d’un identifiant, d’une session ou du processus de publication. Ils ne permettent pas d’identifier la personne qui en est à l’origine ; le mainteneur doit être considéré comme une victime de l’incident.

Une provenance valide a signé la version malveillante

Le manifeste npm désigne GitHub Actions comme éditeur de confiance pour keyv@6.0.0 et renvoie à une attestation npm pour cette version. Le code source malveillant figurait dans l’état du dépôt associé au tag ; le workflow légitime a donc compilé et attesté l’artefact malveillant.

Le patch du commit de publication a également ajouté un test qui exécutait setup.mjs via execFileSync. Un commit ultérieur a supprimé uniquement ce test. L’exécution de la suite de tests de la version aurait donc pu lancer le malware dans l’environnement CI avant la publication.

La provenance reste un élément précieux pour établir l’origine d’une compilation. Cet incident en montre les limites : elle peut attester fidèlement une compilation dont le code source ou le contexte du workflow a déjà été compromis.

Impact et ampleur potentielle

Les principaux packages touchés affichent des volumes d’installation très élevés. Entre le 5 juillet et le 3 août, npm a enregistré 619 682 667 téléchargements de keyv, 579 751 309 de flat-cache et 571 240 025 de file-entry-cache.

Ces chiffres se chevauchent fortement, car les packages dépendent les uns des autres et se retrouvent dans les mêmes chaînes d’outils. Ils mesurent la portée dans l’écosystème, et non le nombre d’hôtes compromis. La fenêtre d’exposition de chaque version malveillante était également bien plus courte qu’un mois.

Snyk observe un impact potentiel important parmi les projets surveillés. L’utilisation transitive compte, car flat-cache et file-entry-cache sont souvent introduits par des outils de développement tels qu’ESLint. Les ordinateurs portables des développeurs et les exécuteurs CI sont des cibles particulièrement intéressantes, car ils contiennent souvent des identifiants GitHub, npm, cloud, de signature et de déploiement.

Au moment de la rédaction, aucun décompte public vérifié des exécutions réussies de la deuxième étape ou des identifiants exfiltrés n’est disponible. Les packages ont été publiés activement sur le registre npm de production et, selon notre instantané, huit portaient encore le tag latest : il s’agit donc d’une diffusion réelle, et non d’un exploit de laboratoire. L’avis de Snyk classe la maturité de l’exploitation comme « Attacked », sans publier de nombre de victimes.

Comment détecter une exposition

Commencez par examiner l’arbre des dépendances résolues, y compris les dépendances transitives :

npm ls keyv flat-cache file-entry-cache cacheable-request cacheable \
  @cacheable/utils cache-manager @cacheable/net \
  @cacheable/node-cache @cacheable/memory ecto --all

Recherchez les versions concernées dans les fichiers de verrouillage : elles peuvent y rester épinglées même après la modification d’un dist-tag par npm.

rg -n \
  'keyv|flat-cache|file-entry-cache|cacheable-request|cacheable|cache-manager|@cacheable/|ecto' \
  package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock

Examinez les manifestes installés à la recherche du hook exact, sans exécuter le code du package :

find node_modules -name package.json -print0 |
  xargs -0 node -e '
    const fs = require("node:fs");
    for (const file of process.argv.slice(1)) {
      try {
        const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
        if (pkg.scripts?.preinstall === "node setup.mjs") {
          console.log(`${pkg.name}@${pkg.version} ${file}`);
        }
      } catch {}
    }
  '

Recherchez la charge utile et les indicateurs de persistance :

find "$HOME" /tmp \
  \( -name setup.mjs -o -name Math_Symbol.js -o -name math_init.js \
     -o -name gh-token-monitor.sh -o -name gh-token-monitor.service \
     -o -name com.user.gh-token-monitor.plist \) \
  -print 2>/dev/null

Examinez également les dépôts accessibles avec les identifiants GitHub exposés afin d’y repérer des fichiers .claude/settings.json ou .vscode/tasks.json inattendus, des fichiers de workflow contenant toJSON(secrets) et de nouveaux artefacts GitHub Actions.

Les clients Snyk peuvent consulter le nouvel avis sur keyv@6.0.0. Relancez les tests de dépendances et surveillez vos projets à mesure que les renseignements sur l’incident évoluent :

snyk test --all-projects
snyk monitor --all-projects

La leçon Snyk Learn sur les packages légitimes compromis fournit des informations complémentaires pour intégrer l’intégrité des dépendances et les contrôles du registre aux workflows de développement habituels.

Remédiation

Si un package touché a été installé

Considérez la machine ou l’exécuteur comme potentiellement compromis, même si node_modules a déjà été supprimé.

  1. Isolez l’hôte du réseau habituel. Conservez les journaux, les données des processus, les journaux npm, les sorties des tâches CI et les horodatages du système de fichiers pour l’enquête.

  2. Recherchez les mécanismes de gh-token-monitor avant de révoquer les identifiants GitHub. Vérifiez ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/, ~/.config/systemd/user/gh-token-monitor.service et ~/Library/LaunchAgents/com.user.gh-token-monitor.plist.

  3. Désactivez tout mécanisme de persistance détecté. Coordonnez sa suppression avec l’équipe de réponse aux incidents et conservez-en une copie à des fins d’analyse forensique. La révocation peut être détectée par le mécanisme de surveillance.

  4. Faites tourner les identifiants depuis un système dont l’intégrité est connue. Cela inclut les jetons d’accès personnels et les jetons d’application GitHub, les jetons npm, AWS, GCP, Azure, Vault, Kubernetes, les identifiants de bases de données, les clés privées et tous les secrets accessibles aux tâches CI concernées.

  5. Examinez les journaux d’audit des comptes et des services cloud. Recherchez des publications npm inattendues, la création de dépôts, des modifications de workflows, des artefacts Actions, des appels d’API cloud et l’utilisation de jetons depuis des emplacements inconnus.

  6. Supprimez les artefacts concernés des registres privés et des caches. La suppression sur npm n’efface pas les copies déjà stockées dans un proxy interne ou le cache d’un développeur.

Verrouillez les versions dont l’innocuité est vérifiée avant la réinstallation

Les versions antérieures suivantes ne comportaient aucun hook de cycle de vie à l’installation dans leurs manifestes de registre au moment de notre vérification :

{
  "overrides": {
    "keyv": "5.6.0",
    "flat-cache": "6.1.23",
    "file-entry-cache": "11.1.5",
    "cacheable-request": "13.0.19",
    "cacheable": "2.5.0",
    "@cacheable/utils": "2.5.0",
    "cache-manager": "7.2.9",
    "@cacheable/net": "2.1.0",
    "@cacheable/node-cache": "3.1.1",
    "@cacheable/memory": "2.2.0",
    "ecto": "5.0.0"
  }
}

keyv@5.6.0 est la dernière version stable 5.x dont l’innocuité est confirmée dans notre instantané. Revenir de keyv@6.0.0 à la version 5.x peut nécessiter des modifications du code. 6.0.0-rc.1 avait une sortie dist/ identique octet pour octet et aucun hook de cycle de vie, mais les équipes de production devraient privilégier une version stable éprouvée, sauf si elles ont explicitement validé cette version candidate.

Après avoir ajouté les substitutions, régénérez le fichier de verrouillage sans exécuter les scripts de cycle de vie :

npm install --package-lock-only --ignore-scripts
rm -rf node_modules
npm cache clean --force
npm ci --ignore-scripts
npm ls keyv flat-cache file-entry-cache cacheable-request cacheable \
  @cacheable/utils cache-manager @cacheable/net \
  @cacheable/node-cache @cacheable/memory ecto --all

La désactivation des scripts de cycle de vie limite cette voie d’exécution. Certains packages légitimes nécessitent des scripts d’installation : les équipes devraient donc gérer une liste d’autorisation restreinte plutôt que d’activer les scripts globalement. Les bonnes pratiques de sécurité npm de Snyk détaillent davantage les installations déterministes, le contrôle des scripts et l’examen des packages.

Chronologie de l’incident

Toutes les heures ci-dessous sont en UTC, le 4 août 2026.

  • 09:02 à 09:17 : Le commit ee2681a9 prépare keyv@6.0.0, ajoute le hook de cycle de vie, les fichiers de charge utile et un test qui exécute le chargeur. GitHub indique une heure d’auteur de 09:02 et une heure de validation de 09:17.

  • 09:04 : Le commit vérifié d8c850c7 ajoute les hooks d’exécution de Claude et VS Code.

  • 09:23 : Le commit f97eabcd supprime le test preinstall.

  • 09:30 à 09:32 : Plusieurs packages @keyv/* en version 6 sont publiés sans le hook malveillant.

  • 09:35 : npm publie keyv@6.0.0 avec le hook malveillant.

  • 09:51 : Le commit 1f79edd8 ajoute les fichiers de charge utile aux espaces de travail @keyv/*, créant un risque pour les versions ultérieures.

  • 09:49 à 09:51 : Les issues GitHub #2044, #2045 et #2046 signalent l’incident. L’API des issues a ensuite renvoyé 410 Gone.

  • 10:09 à 10:14 : Les versions malveillantes de la famille Cacheable sont publiées.

  • 10:18 et 10:20 : Le chercheur en sécurité Charlie Eriksen publie les deux avertissements publics fournis.

  • 10:28 : ecto@5.0.1 est publié avec la même charge utile, portant à 11 le nombre de packages liés au mainteneur.

  • 10:39 : Les métadonnées du registre npm sont modifiées après la suppression de cacheable-request@13.0.20.

  • 10:42 : Les métadonnées du registre npm sont modifiées après la suppression de flat-cache@6.1.24.

  • 11:11 : Les métadonnées du registre npm sont modifiées après la suppression de cache-manager@7.2.10.

  • 11:16 : L’instantané du registre de Snyk recense encore huit versions malveillantes sous le tag latest.

  • Pendant la rédaction : Snyk publie SNYK-JS-KEYV-18515941 et environ 70 autres avis de sécurité.

Cette chronologie concerne un incident en cours. Revérifiez les manifestes npm, les balises de distribution et les avis de sécurité Snyk juste avant publication.

Webinaire à la demande

OpenAI a corrigé sa propre copie, puis a compromis un environnement de production

Regardez le webinaire à la demande pour comprendre pourquoi l’auto-validation échoue par nature, pourquoi une architecture multi-modèle aggrave le problème et à quoi ressemble une validation indépendante en pratique. Repartez avec un cadre pour gouverner chaque ressource d’IA dans votre environnement, quel que soit le laboratoire qui l’a développée.