|9 min de lecture|Yvann Lièvre

npm : --ignore-scripts ne suffit plus, la charge s'exécute à l'import

La compromission des paquets @asyncapi exécute la charge à l'import du module, pas à l'installation. Pourquoi --ignore-scripts échoue et comment détecter par YARA.

YARASupply chainnpmDétectionDevSecOps
npm : --ignore-scripts ne suffit plus, la charge s'exécute à l'import

Depuis des années, le conseil standard pour se protéger des paquets npm malveillants tient en une option : npm install --ignore-scripts. Elle désactive l'exécution des scripts de cycle de vie, notamment postinstall, par lequel transite l'immense majorité des charges de compromission de la chaîne d'approvisionnement. npm 12 va même plus loin en bloquant ces scripts par défaut. Le 14 juillet 2026, Microsoft Threat Intelligence a documenté une attaque qui rend ce conseil caduc : la compromission de l'organisation @asyncapi, où la charge ne s'exécute pas à l'installation, mais au chargement du module.

C'est un changement de paradigme qu'il faut intégrer. --ignore-scripts ne bloque que les hooks d'installation. Si le code malveillant se déclenche quand un build, un job d'intégration continue ou une application importe le module pour la première fois, aucune de ces protections n'entre en jeu. La charge se réveille au require ou à l'import, en plein cœur de l'exécution légitime.

Ce qui a été compromis

Microsoft rapporte que cinq versions de paquets, réparties sur quatre noms, ont été republiées en environ quatre-vingt-dix minutes, chacune portant le même chargeur injecté. Les paquets concernés sont @asyncapi/specs (dans la préversion 6.11.2-alpha.1 et la version stable 6.11.2), @asyncapi/generator@3.3.1, @asyncapi/generator-components@0.7.1 et @asyncapi/generator-helpers@1.1.1. Ensemble, ces paquets totalisent plus de deux millions de téléchargements hebdomadaires, ce qui donne la mesure de la surface exposée.

L'attaque a exploité les pipelines de publication de confiance dans GitHub Actions, permettant au code malveillant de passer sans reposer sur les hooks d'installation npm traditionnels. C'est la technique T1195.002 (Supply Chain Compromise: Compromise Software Supply Chain). La charge, apparentée au cadriciel Miasma déjà observé lors des attaques Shai-Hulud, combine le vol d'identifiants, l'empoisonnement d'outils d'assistance, la propagation de type ver et un canal de commande et contrôle multi-protocoles utilisant HTTP, Nostr, IPFS, BitTorrent, libp2p et Ethereum.

Pourquoi l'exécution à l'import change la détection

Une charge postinstall s'exécute une fois, au moment de l'installation, dans un contexte relativement identifiable. Une charge à l'import s'exécute chaque fois que le module est chargé, dans le processus applicatif lui-même. Cela déplace le point de détection : il ne s'agit plus de surveiller npm install, mais de repérer le comportement anormal d'un processus Node.js en cours d'exécution, et de disposer de signatures sur les artefacts déposés.

La charge écrit un fichier sync.js sous %LOCALAPPDATA%\NodeJS\ pour établir sa persistance, ce qui relève de T1547, et procède à la collecte massive d'identifiants (T1555). Ces artefacts fichier sont un point d'ancrage solide pour une règle YARA, qui reste valable même si le vecteur d'injection évolue.

Une règle YARA sur les artefacts et les marqueurs C2

La règle suivante cible les chaînes caractéristiques du chargeur : le chemin de persistance, et les marqueurs de canaux de commande et contrôle atypiques pour un paquet npm légitime. Un paquet de génération de code n'a aucune raison de référencer IPFS, Nostr ou une adresse Ethereum.

rule Npm_Asyncapi_ImportTime_Loader
{
    meta:
        description = "Detecte le chargeur injecte dans les paquets @asyncapi compromis (juillet 2026)"
        reference = "https://www.microsoft.com/en-us/security/blog/2026/07/15/unpacking-asyncapi-npm-supply-chain-compromise-import-time-payload-delivery/"
        author = "ThreatClaw"
        date = "2026-07-19"
    strings:
        $persist = "\\NodeJS\\sync.js" ascii wide nocase
        $ipfs = "ipfs" ascii nocase
        $nostr = "nostr" ascii nocase
        $eth = "ethereum" ascii nocase
        $b64eval = "Buffer.from" ascii
        $childproc = "child_process" ascii
    condition:
        $persist or
        ( 2 of ($ipfs, $nostr, $eth) and $b64eval and $childproc )
}

Cette règle se déclenche soit sur le chemin de persistance connu, soit sur la combinaison improbable, dans un module npm, de plusieurs protocoles de commande et contrôle exotiques avec de l'exécution dynamique de code et le lancement de processus enfants. Le second bras vise à capter des variantes qui changeraient le nom du fichier de persistance mais conserveraient le canal C2.

Compléter avec une détection à la construction

Côté prévention, un scanner de type Nuclei ou une inspection statique du répertoire node_modules avant le build permet de repérer les versions compromises précises. Épinglez vos dépendances par version exacte et par empreinte d'intégrité, et bloquez les versions listées ci-dessus dans votre gestionnaire de dépendances. Surveillez également les connexions sortantes inattendues d'un runner d'intégration continue vers des adresses IPFS ou des relais Nostr, qui n'ont rien à faire dans un pipeline de build de générateur de documentation.

La leçon opérationnelle est claire : le modèle mental « les scripts d'installation sont le seul risque » est dépassé. Une charge peut sommeiller dans le corps même d'un module et se réveiller à l'import, dans le processus le plus légitime qui soit. Détecter ces menaces demande des signatures maintenues sur les artefacts réels, pas seulement des règles sur le cycle de vie npm.

C'est la vocation du feed YARA ThreatClaw : des règles de détection construites à partir d'échantillons réels, testées contre un corpus de code légitime pour limiter les faux positifs, et actualisées au rythme des compromissions de la chaîne d'approvisionnement.

Articles liés