Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

imgdecode npm Is Malware: Check Your Lockfiles (2026)

You probably haven’t heard of imgdecode. It bills itself on npm as a “native image buffer decoder for Node.js build pipelines” — and as of today, October 17, 2026, you can still npm install it. But imgdecode doesn’t decode images. Per a SafeDep technical analysis published October 13, the imgdecode npm package is a malicious shellcode loader for the MALFEX supply-chain campaign: a nine-line JavaScript file that loads a Windows DLL, downloads encrypted shellcode from GitHub, and injects it into a new dllhost.exe process. I checked the npm registry this evening — version 1.0.3 is still live and installable right now.

This post is the practical companion to the research: what the package actually does, the 60-second version of the MALFEX campaign, exactly how to check your lockfiles for it, what to do if you find it, and a five-minute dependency audit to run before your next npm install.

What the imgdecode npm package actually does

SafeDep’s analysis is unusually detailed, so here is the infection chain as they documented it — every step verified in the original writeup:

  1. The trigger is require(), not installation. The package’s index.js is nine lines long. It loads a prebuilt native addon at prebuilds/win32-x64/imgdecode.node. From version 1.0.1 onward, the malicious routine runs inside the N-API module initialisation function — meaning it fires the moment your Node.js process calls require('imgdecode'). No install script is needed, and the tarball contains no binding.gyp, so no build step happens either. The prebuilt DLL just loads.
  2. It only fires from an interactive Windows console. The DLL’s first move is to grab the standard output handle and call GetConsoleMode. That call fails when stdout is piped or redirected — which is exactly how CI runners, automated sandboxes, and most build tools work. SafeDep’s inference: the check deliberately favours execution from a developer’s terminal, where the valuable credentials live.
  3. The payload comes from GitHub, encrypted. Strings in the DLL are encoded with a one-byte XOR key (0x37). Decoded, they reveal the payload URL — raw.githubusercontent.com/cavecrew/proj/main/banner.jpg — and the passphrase nif-runtime-2027. The DLL downloads the file with WinINet, then decrypts it with AES-256-CBC, deriving the key from the SHA-256 hash of that passphrase. The first 16 bytes of the response are the IV; the rest is ciphertext.
  4. Injection into dllhost.exe. For any content that doesn’t decrypt to a JPEG or PNG signature, the DLL starts %SystemRoot%\System32\dllhost.exe suspended, allocates memory in it (VirtualAllocEx), writes the decrypted bytes (WriteProcessMemory), flips the memory to execute-read (VirtualProtectEx), and launches a remote thread at the first byte (CreateRemoteThread). The main thread of dllhost.exe stays suspended; only the injected thread runs. If injection fails, a fallback routine runs the shellcode inside the Node.js process itself.
  5. There is even a kill switch. After decryption, the DLL checks whether the plaintext starts with a JPEG or PNG signature — and if so, it stops. Inference: the operator could deactivate the loader by serving encrypted content that decrypts to a real image.

Two honesty notes, because the authenticity bar matters here. First, SafeDep could not recover banner.jpg itself — the cavecrew GitHub account was already returning 404s at analysis time. They did recover a sibling file, banner.png, which decrypts (same key, same IV layout) to a Go downloader whose hash matches Checkmarx’s MALFEX indicator list — and on October 13 the stage-3 server at 104.234.65.75:700 was still serving a 71.5 MB Node.js stealer named movinlike. The native loader’s link to that payload is a strong inference, not a confirmed chain. Second, the payload must start with raw shellcode rather than a PE header — the injector starts the thread at byte zero of the buffer.

And one detail for the comedy-tragedy file: the DLL ships with its debug symbols path still inside. It reads C:\Users\Administrator\Desktop\npm\imgdecode\build\Release\imgdecode.pdb. Opsec was not this operator’s strong suit.

The MALFEX campaign in 60 seconds

MALFEX is the name Checkmarx and CloudSEK gave, in early October 2026, to a campaign in which one operator — assessed to be a lone Portuguese-speaking threat actor — has been publishing malicious npm packages since August 6, 2023. The numbers: 12 packages published across five npm accounts, eight confirmed malicious, 40,767 downloads as of October 1. Three distinct infection pathways: a loader for Overlord RAT (a Go remote-access trojan that pulls its C2 address from Solana transactions), a chain that installs movinlike (a Node.js stealer targeting Discord, browsers, Telegram, and crypto wallets), and a plain downloader.

The operator’s signature move is speed. When npm removed native-runner and img-to-native on September 28, replacements were up within two days: runhelper and imgbundle on September 30, then imgdecode 1.0.0 that same evening. When those got removed too, version 1.0.3 of imgdecode — published October 11, six days after the Checkmarx disclosure — dropped the imgbundle dependency entirely and went standalone.

Here is the current status, verified against the npm registry this evening (October 17, 2026):

  • imgdecode — STILL LIVE, latest 1.0.3, fully installable.
  • function-flag, cdn-img-fetch, imgbundle — removed; npm replaced each with a 0.0.1-security holding package (October 7–8). Installing those names now gets you a placeholder.
  • runhelper — unpublished entirely (September 30).

imgdecode is the last MALFEX package standing on npm. That is the news value of this post: the window to warn people is right now, while the package is still installable.

Step 1: Check whether imgdecode is in your projects

Run these in every Node.js project you own, on your machine and in your CI cache directories. Start with the direct question:

npm ls imgdecode

If it’s installed anywhere in the tree, that command tells you which package pulled it in. Then check your lockfiles directly — a lockfile pins the package even if node_modules was cleaned:

grep -n '"imgdecode"' package-lock.json
grep -n 'imgdecode' pnpm-lock.yaml yarn.lock

And inspect what’s actually on disk, including the version — anything in the 1.0.x range from publisher malfexteste8 is the malicious package:

ls node_modules/imgdecode 2>/dev/null && node -p "require('./node_modules/imgdecode/package.json').version"

One more place people forget: global installs and CI caches. Check npm ls -g imgdecode, and if your CI caches node_modules between runs, audit the cache too — a poisoned cache re-installs the malware long after the registry takes it down.

Step 2: Check for the rest of the MALFEX package list

Even though npm removed the sibling packages, they can still sit in your node_modules or be pinned in a lockfile from when they were live. Grep for all of them:

grep -nE '"(imgdecode|tlxbnhd|tldriver|mxdriver|img-to-native|native-runner|function-flag|function-color|cdn-img-fetch|imgbundle|runhelper)"' package-lock.json

For pnpm and Yarn lockfiles, drop the quotes and search the bare names. The list above covers all eight confirmed-malicious packages from the Checkmarx/CloudSEK disclosure plus the three replacement packages (imgbundle, runhelper, imgdecode) from the September 30 wave.

A word of caution about what won’t catch this: npm audit. The audit command checks your tree against the GitHub Advisory database — vulnerability records for legitimate packages. imgdecode isn’t a vulnerable version of a real package; it’s malware wearing a package costume. Searching OSV today turns up MAL records for its siblings (imgbundle, runhelper, cdn-img-fetch, img-to-native, native-runner) but none for imgdecode itself. Audit tooling is at its worst against exactly this class of threat, so the manual lockfile check above is the one that matters.

Step 3: If you find one, treat the machine as compromised

SafeDep’s incident-response guidance is blunt: a Windows workstation that loaded imgdecode should be treated as potentially compromised. Here’s the condensed version, in order:

  1. Isolate the workstation and capture memory before rebooting — the native loader runs its shellcode in memory, so a reboot destroys evidence.
  2. Check for persistence. The MALFEX stealer maintains persistence via a Registry Run key and a scheduled task named MicrosoftEdgeUpdateService. Also look for dllhost.exe processes that are children of node.exe — that parent-child relationship should never happen on a clean machine.
  3. Rotate every credential the machine touched. Per SafeDep’s list: browser passwords, session cookies, Discord tokens, cloud keys, SSH keys, and npm and GitHub tokens. The stealer harvests seed phrases, .kdbx, .pem, .ppk and .env files, browser data, Discord and Telegram data, and crypto-wallet and gaming-app data — and it phones home over a WebSocket C2 at 104.234.65.75:7007 for file transfer, process control, and shell execution. Assume exfiltration, not just infection.
  4. If the machine held crypto seed phrases, move the funds to new wallets. Do this before anything else on a clean machine.
  5. Compare local Security and Sysmon logs against external logs — the recovered stealer shows evidence of log-tampering capability, so local logs alone may lie to you.

One more thing worth knowing about the stealer’s capabilities: SafeDep’s analysis of the movinlike sample is partly inference from string tables and embedded scripts — they did not recover the bytecode control flow. Treat the capability list as “the code contains the machinery for this” rather than “confirmed working.” The anti-analysis checks (VM, sandbox usernames, x64dbg, procmon, wireshark), the Discord token theft and discord_desktop_core patching, and the Chromium password/cookie targeting are all in the strings. That is plenty.

Why imgdecode is nastier than your average npm malware

Most malicious npm packages follow a tired script: a postinstall hook downloads a binary. imgdecode breaks the script in five ways, and each one defeats a common defence:

  1. No install script, no build step. The tarball has no binding.gyp and the archived manifest has no scripts — the prebuilt DLL loads with plain require(). As SafeDep puts it, “controls that only block install scripts do not stop this execution path.” If your defence strategy was --ignore-scripts, this package walks right past it.
  2. The console check evades CI and sandboxes. Because GetConsoleMode fails on redirected stdout, the loader stays dormant in CI runners, automated sandboxes, and most dynamic-analysis environments. It wakes up on a developer’s laptop — where the credentials are.
  3. A compiled DLL evades JavaScript-focused scanners. The malicious logic lives in machine code behind a nine-line JS shim. Scanners that analyse package JavaScript see nothing.
  4. The README lies. The package README documents an exec() function that version 1.0.3 doesn’t even export — misleading documentation, the same trick the sibling runhelper package used. If you skimmed the docs, you’d think it was a normal utility.
  5. The operator iterates in days, not months. Removal on September 28, three replacement packages on September 30, a standalone rewrite on October 11. Blocking one package name is whack-a-mole; the publisher handle (malfexteste8) and the campaign patterns are the real signal.

A five-minute audit before your next npm install

You can’t realistically reverse-engineer every dependency, but the MALFEX packages share tells that a quick audit catches. Make this a habit for any package you haven’t installed before:

  1. Read the publisher. imgdecode‘s sole maintainer is malfexteste8, a single-purpose account created to publish malware. Legitimate packages usually have maintainers with history — multiple packages, linked GitHub profiles, years of activity. A one-package account with a fresh creation date is a red flag.
  2. Check the publish timeline. npm view imgdecode time shows four versions in twelve days, three of them within 48 hours of first publication. Rapid-fire version bumps on a brand-new package often mean the author is iterating on evasion, not features.
  3. Peek inside the tarball without installing it. npm pack imgdecode --dry-run lists the package contents without downloading or running anything. A “JavaScript image decoder” shipping a prebuilds/win32-x64/*.node binary and no source for it should stop you cold. Also check npm view imgdecode scripts and the gypfile flag — native addons with prebuilt binaries and no visible build are the classic malware shape.
  4. Read the code you’d actually load. You don’t need to reverse a DLL: open the index.js entry point and check that it matches the README. imgdecode‘s README promised an exec() function the code never exported — a five-second read that exposes the disguise.
  5. Sanity-check the download curve. Under a thousand downloads on a package claiming to be build-pipeline infrastructure, all clustered in the last two weeks, is not organic adoption. Compare against established alternatives before reaching for the new shiny thing.

Make the checks automatic

Manual audits don’t scale, so wire the important ones into your workflow:

  • Gate installs on a scanner that does malware detection, not just vulnerability matching — Socket, Snyk, or GitHub’s dependency review with malware alerts. Plain npm audit is necessary but not sufficient, as the imgdecode case proves.
  • Review lockfile diffs in CI. A new dependency appearing in package-lock.json should be as review-worthy as a code change. Flag additions of packages with single-maintainer publishers or prebuilt binaries.
  • Pin and verify. Lockfiles plus npm ci (never npm install in CI) stop dependency confusion and range drift, though they don’t stop a malicious pinned package — which is why the review step matters.
  • Consider an allowlist for sensitive environments. On build machines that handle signing keys or production credentials, an explicit list of approved dependencies beats any denylist. MALFEX’s whole strategy is minting new names; you can’t denylist what doesn’t exist yet.

The uncomfortable truth this campaign demonstrates: npm’s removal process is reactive, and the operator is faster than the takedowns. imgdecode went from disclosure (October 5) to a standalone rewrite (October 11) to still-installable (October 17) while everyone was watching. Your lockfile is the last line of defence — check it.

Facts verified against the npm registry, the SafeDep technical writeup, and security-press coverage on October 17, 2026. The package status (“still live”) reflects the registry state at that time; re-check before acting on it.

Further Reading & References

Leave a Comment