Indicator review, 2026-09-14
| Outcome | 19 indicators added |
| Pull request | #32 |
| Indicator set | ioc/bad-packages.txt, ioc/strong.txt, ioc/network.txt |
Sources
Section titled “Sources”| 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.
Upstream state
Section titled “Upstream state”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.
Verified against fixtures
Section titled “Verified against fixtures”Scanned outside the repository working tree, with ci/scan-workspace.sh:
- a
package.jsondepending on@vite-tab/tabandtailwindcss-merge→INFECTED(exit 2) - a
.vscode/tasks.jsoncarrying the font-execution command →INFECTED(exit 2) - a file containing
A10-npm3!and the wallet →INFECTED(exit 2) package.jsonfiles depending on the realsass-formatter,typeorm-encryptedandclsx-tailwind-merge→ zero findings (exit 0)- an HTML file merely referencing
/public/fonts/fa-solid-400.woff2in 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.
Considered and not added
Section titled “Considered and not added”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.
- 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. Everynpm,npxornpm execcall 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 nextnpm i. A check would comparenpm/lib/cli.jsagainst a size threshold, following symlinks. It is not anioc/entry — the path is legitimate and only the size and content are wrong — so it needs a new local check. - 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/.mjsfile 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. - 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.
- 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.
Open gaps
Section titled “Open gaps”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-encryptandclsx-tailwindare 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.
shellcheckis not installed in this container.bash -npasses on every script. CI enforces shellcheck, and this change touches onlyioc/*.txtdata 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-nextstepsandselftest-preservepass.selftest-restore,selftest-rewriteandselftest-uifail, and I confirmed against a fresh clone of unmodifiedmainthat they fail there identically — byte-identical output for two of them, and differing only by a random temp directory name forselftest-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.