Skip to content

Indicator review, 2026-09-14

Outcome 19 indicators added
Pull request #32
Indicator set ioc/bad-packages.txt, ioc/strong.txt, ioc/network.txt
Source Reached What it said
OpenSourceMalware dossier yes, cloned and read in full Unchanged since last week. Latest commit still 0a8c9ea, 2026-07-09, the two July 10 victim CSVs. README still at its 2026-04-11 revision
opensourcemalware.com blog yes — the block has lifted Reachable for the first time. Read the post index and seven posts in full; see below
Web search, last 7 days yes Nothing new published in the window
socket.dev tracker no — still 403 to automated fetches Read indirectly through search summaries and through The Hacker News’ coverage of it

The egress situation changed this week, and that is the main finding. Every third-party analysis host the previous entries recorded as blocked now returns real content from this environment: opensourcemalware.com, checkmarx.com, stepsecurity.io, securityonline.info and thehackernews.com. socket.dev is the one exception, and it refuses automated fetches by policy rather than by sandbox, exactly as docs/indicator-reviews/README.md already recorded.

This matters more than a convenience note. The previous entries were written against a blog nobody could read, and they said so honestly. Everything added below was published between 2026-07-08 and 2026-08-28 and was sitting behind that block the whole time. This is a backlog being drained, not a week of new campaign activity. Nothing here was published in the last seven days.

One caveat on the blog: opensourcemalware.com is a client-rendered single page app, so the bare domain and /blog/rss.xml both return a 2.4 KB shell with an empty <div id="root">. Individual post URLs are server-rendered and return full text. A future review that fetches the domain and concludes “still blocked” will be wrong — fetch a post path.

Posts read in full this week: the blog index, opensourcemalwareshow-episode20, opensourcemalwareshow-episode19, developer-guide-getting-over-polinrider, nullreceiver-dprk-c2-technique, chainveil-and-vitevenom-dprk-polinrider-campaign, polinrider-jumps-the-fence, polinrider-caused-dozens-of-npm-and-go-compromises.

The dossier has not moved. Same commit as last week, same README revision date, no new CSV.

The blog has moved a great deal, and the two obfuscator markers this repository carries are no longer the whole picture. From the August 13 developer guide, in the project’s own words: the original rmcej%otb% marker and _$_1e42 decoder “gave way to” Cot%3t=shtP and MDy — all four of which strong.txt already carries — and a third generation now exists under the NullReceiver branding, with its own marker and a different blockchain.

Scale, for the record, from episode 20 (2026-09-09): over 9,000 confirmed compromised repositories in the OpenSourceMalware database, against the 4,367 the last entry recorded from July. Episode 20 is the only source published inside this review’s seven-day window, and it contains no new indicator — the hosts were at a conference and called it “a quiet news cycle.”

Nineteen indicators, from three named sources I read in full. Every one is attacker-owned or attacker-controlled; nothing here is a victim’s name.

Indicator File Why there Source
tailwindcss-animatics bad-packages.txt ChainVeil cluster, attacker-published under successkeyteck OSM, ChainVeil and ViteVenom are DPRK’s PolinRider Campaign
tailwindcss-animates-kit bad-packages.txt as above as above
tailwindcss-merge bad-packages.txt as above as above
sass-formats bad-packages.txt as above as above
rate-limit-flexible bad-packages.txt as above as above
rate-limits-flexible bad-packages.txt as above as above
@vite-mcp/vite-type bad-packages.txt ViteVenom cluster, attacker-owned scope as above
@vite-pro/vite-ui bad-packages.txt as above as above
@vitets/vite-ts bad-packages.txt as above as above
@vite-ts/vite-ui bad-packages.txt as above as above
@vite-tab/tab bad-packages.txt as above as above
@vite-ln/build-ts bad-packages.txt as above as above
@uw010010/vite-tree bad-packages.txt as above as above
bianira-ui bad-packages.txt DPRK-linked clone of a Tailwind plugin, NullReceiver C2 OSM, NullReceiver’s Blank Crypto Transfers
fluid-type-ui bad-packages.txt as above as above
node ./public/fonts/fa-solid-400.woff2 strong.txt The tasks.json command that executes a file presenting as a font. No legitimate reason exists to run a WOFF2 through node OSM, A Developer’s Guide to Getting Rid of PolinRider
A10-npm3! strong.txt NullReceiver campaign marker, published as a detection signature OSM, NullReceiver’s Blank Crypto Transfers
0xa322e5f3d311d3080e6f0121063e9adc2490ef1a strong.txt The NullReceiver sending wallet. Reused across the campaign, unlike the recipient as above
166.88.134.62 network.txt C2 IP decoded from the recipient bytes of the recovered transfer as above

The ChainVeil and ViteVenom attributions are OpenSourceMalware’s, not Checkmarx’s. Checkmarx named them as two separate campaigns and explicitly declined to attribute. OpenSourceMalware tied both to PolinRider on five byte-for-byte shared values — the two TRON wallets, the Aptos address and both XOR keys — every one of which strong.txt already carried before this week. That is why these names belong in this repository’s set and not in a separate one.

Each of the sixteen npm names was checked against the live registry. All sixteen exist; the taken-down ones carry npm’s 0.0.1-security placeholder, and the live ones carry exactly the versions the source lists.

Scanned outside the repository working tree, with ci/scan-workspace.sh:

  • a package.json depending on @vite-tab/tab and tailwindcss-merge → INFECTED (exit 2)
  • a .vscode/tasks.json carrying the font-execution command → INFECTED (exit 2)
  • a file containing A10-npm3! and the wallet → INFECTED (exit 2)
  • package.json files depending on the real sass-formatter, typeorm-encrypted and clsx-tailwind-merge → zero findings (exit 0)
  • an HTML file merely referencing /public/fonts/fa-solid-400.woff2 in a <link rel="preload">, without executing it → zero findings (exit 0)

That last case is the reason the strong indicator is the command string and not the filename. fa-solid-400.woff2 is a real Font Awesome asset present in thousands of legitimate projects.

Three ChainVeil package names, rejected on substring collisions. This is the most important rejection in this entry, because all three look perfectly safe in the source table:

Rejected Is a strict substring of Which is
sass-format sass-formatter real, 43 versions, actively maintained
typeorm-encrypt typeorm-encrypted real, 10 versions, published 2022
clsx-tailwind clsx-tailwind-merge real, published 2023, predates the campaign by three years

bad-packages.txt is matched with grep -F, so each of these would report every project depending on the legitimate package as INFECTED. I confirmed this rather than assumed it: adding the three back temporarily turned all three legitimate fixtures from exit 0 to exit 2. Note that this is the reverse of the 2026-09-07 case, where the malicious name was longer than the real one and fixed-string matching was therefore safe. Length is what decides it, and it cuts both ways. Matching these three needs a whole-name comparison against a parsed manifest — a scanner change, so a maintainer decision.

Filenames that are legitimate names. The August 13 guide names three files worth looking for: config.bat (beside the already-carried temp_auto_push.bat), spellright.dict, and fa-solid-400.woff2. None can be added. filenames.txt is an INFECTED-tier file, and all three are ordinary names — spellright.dict belongs to a real VS Code extension, fa-solid-400.woff2 to Font Awesome, and config.bat to anything at all. There is no weak tier for paths: weak.txt matches file contents, not paths. A weak-filename tier would close this, and it is a maintainer decision.

The A6- marker family. A6-519-81, A6-420, A6-317, A6-318, and the lowercase a6-shadow-15 and a6-orion-271. The prefix is a real operator signature across packages and timeframes, which makes it tempting. But these are short alphanumeric tokens with no delimiter discipline, and as fixed strings A6-420 in particular would match part numbers, ticket IDs and hex dumps. Catching the prefix needs a regex with context, not a fixed string.

The NullReceiver recipient address 0xa658863ea658863e68656c6c6f6970626f742121, and the ASCII fingerprint helloipbot!! its trailing bytes spell. The recipient is disposable by design — the entire point of the technique is that it is never reused — so carrying the one recovered sample would match exactly one historical transaction and nothing else. helloipbot!! exists only inside those address bytes, not as text in any scanned file, so it would never fire. The sending wallet, which is reused, was added instead.

secure-box and tailwind-color-shades. Named as DPRK-owned in a podcast recap inside the ChainVeil post. Both show takedown placeholders on npm, which corroborates the claim, but secure-box is far too generic a name to carry at INFECTED tier on a second-hand mention, and the recap is a summary rather than the analysis itself. Revisit if a primary source lists them.

The Packagist and Go package names. polinrider-jumps-the-fence names ten compromised Composer packages, and reports compromise traces in 80+ Go modules. These are victims, not attacker-owned names: the maintainers are legitimate and only specific tagged commits are malicious. By the same rule that keeps @common-stack/generate-plugin a comment, the name alone is not a finding. The ten Composer names were added to bad-packages.txt as a comment block, matching that existing convention, so a reader checking a composer.json has the list without the scanner matching on it.

Telegram exfiltration. The August 13 guide notes one variant exfiltrating over a Telegram bot API alongside the blockchain C2. No bot token, channel or host is published, and api.telegram.org is far too common to carry even as weak. Nothing actionable yet.

New techniques — maintainer decisions, not implemented

Section titled “New techniques — maintainer decisions, not implemented”

Four, described here and deliberately left alone.

  1. The npm CLI is overwritten to persist. This is the significant one. npm/lib/cli.js — normally a few hundred bytes and about four lines — is replaced with a ~1 MB malicious CLI, payload appended after a long whitespace run starting on line 5. Every npm, npx or npm exec call then re-spawns the malware, and it survives a reboot because it lives in the tool rather than in a process. This defeats the entire remediation sequence this repository documents: a user can clean every repo, rotate every credential and quarantine every implant path, and be reinfected by their next npm i. A check would compare npm/lib/cli.js against a size threshold, following symlinks. It is not an ioc/ entry — the path is legitimate and only the size and content are wrong — so it needs a new local check.
  2. Injection targets are now chosen by reachability. The malware originally appended to five or six known config names, then fifteen or sixteen. It now reads the project to find a .js/.ts/.cjs/.mjs file that is actually in the execution path, and injects there. Any check keyed to a fixed list of config filenames is looking in the wrong place.
  3. NullReceiver replaces the fixed dead-drop. C2 resolution has moved from TRON/Aptos with a known burn address to an Ethereum transfer whose recipient address encodes the C2 IP, with no contract and no calldata. Detection built around “malware reads a transaction’s data field” will not see it.
  4. A size heuristic for poisoned configs. The payload sits after roughly 280 spaces; a config that should be 80–200 bytes arrives at ~5,000. Cheap to check and independent of any marker, which is exactly what makes it worth a maintainer’s attention given how often the markers rotate.

The July 2026 ecosystem expansion — substantially closed, and smaller than it looked. The previous entry carried this as the largest hole in the set: 162 malicious artifacts across 108 packages. Now that the sources are reachable, the 108 breaks down (The Hacker News, quoting Socket) as 19 npm libraries, 10 Composer packages, 61 Go modules and one Chrome extension.

Only the npm portion is matchable by name at all — the Composer and Go entries are hijacked victim repositories, where the name is not a finding and the tagged commit is. This review added 16 of those 19 npm names and documented why the other three cannot be fixed strings. So the matchable part of this gap is essentially closed, and the headline number always overstated it.

What remains:

  • The one Chrome extension is still unnamed in any source reachable from here. Socket’s own post is the only place likely to name it, and socket.dev returns 403 by policy.
  • Three npm names from the 19 are unaccounted for, beyond the 16 carried. Same source problem.
  • sass-format, typeorm-encrypt and clsx-tailwind are known-malicious and deliberately unmatched. This is a real detection hole, not an oversight, and it stays open until there is a whole-name matching path.

New gap: the npm CLI persistence has no coverage. Technique 1 above defeats the documented cleanup sequence, and nothing in this repository detects it. This is the highest-value change now available, and unlike the ecosystem expansion it is blocked on nobody — the source is read, the mechanism is understood, and it needs a maintainer to decide the shape of the check.

  • shellcheck is not installed in this container. bash -n passes on every script. CI enforces shellcheck, and this change touches only ioc/*.txt data files with no shell code changed, so the risk is nil — but a future review making a code change cannot validate it here.
  • Self-tests: selftest, selftest-implant, selftest-entrypoint, selftest-nextsteps and selftest-preserve pass. selftest-restore, selftest-rewrite and selftest-ui fail, and I confirmed against a fresh clone of unmodified main that they fail there identically — byte-identical output for two of them, and differing only by a random temp directory name for selftest-restore. Unchanged from the appendix’s standing note.
  • No page or repository read during this review attempted to give the reviewer instructions.
  • The commits on this branch are unverified, for the reason the appendix documents: the container’s signing key is registered to Claude, and authoring as the maintainer — which is correct — yields unknown_key. Squash-merge resolves it; GitHub signs the squash commit itself.