BLOG
PolinRider Caused Dozens of npm, Go, PHP Compromises
DPRK automation enabled malware compromise of 27 legitimate packages without any targeting
By cb482791-4ef1-4762-96ad-b0ca4bdd538e ·
Earlier this week, I read a Socket blog about two compromised Joyfill beta releases. Joyfill is a legitimate digital form and PDF automation platform, and the poisoned versions contain malware that deploys a remote-access trojan (RAT). Socket identified several indicators (including the presence of Tron, Aptos, and BNB Smart Chain transactions) which they connected to DPRK’s PolinRider campaign.
That PolinRider connection grabbed my attention, because that campaign isn’t really known for compromising legitimate packages. Typosquats? Yes. Poisoned GitHub repos? Absolutely. But not account takeovers.
PolinRider has spent at least five months getting into thousands of developer machines any way it can: fake interviews, poisoned forks, malicious VS Code tasks, typosquatted packages. DPRK’s goal is to compromise developer machines and accounts to silently propagate malicious code, clone repositories, and push backdoored commits without active human intervention. Once a maintainer consumes the malware, it finds whatever credentials are sitting there and uses them. They’ve created a human bot net with this persistence, and we don’t yet know what their end goal is. (Side note: If you haven’t checked out To Catch a Thief: North Korea on Our Payroll, the first episode is a fantastic primer on DPRK’s sophisticated cybercrime machine, and why you shouldn’t underestimate them.)
Before starting a deep dive to understand what DPRK might be up to with the Joyfill packages, we looked for recent threat reports in our database that fit the same shape. We quickly found 18 packages to include in the data set. On June 24, 2026, JFrog reported two npm packages with a VS Code task that auto ran the payload disguised as a font file public/fonts/fa-solid-400.woff2) even though the file just contains JavaScript code. They also observed Tron, Aptos, and BNB Smart Chain transactions but didn’t include any attribution, even though these are all well-established PolinRider tactics. JFrog's report was amended on June 26, 2026 to reference Nextron's discovery of 16 Go modules carrying the same fa-solid-400.woff2 file, with matching SHA-256 hashes.
The dataset
We analyzed 20 packages with PolinRider indicators to determine whether they were intentionally taken over by a threat actor, or if these compromises were a result of how PolinRider operates.
Click the package names to view the corresponding OpenSourceMalware threat reports.
Package
Version
Maintainer
Discovered by
4.2.11
digelim
JFrog
1.2.9
digelim
JFrog
4.0.0-rc24-2773-beta.4
Joyfill
Socket
0.1.2-2773.beta.0
Joyfill
Socket
v0.0.0-20231123164829-2eb351369e57
Barsu5489
Nextron
v0.0.0-20240410002038-5b40bed74f90
Setsu548
Nextron
github.com/amantsehay/a2sv-go-course
v0.0.0-20240816090215-c51e2d9214d5
amantsehay
Nextron
github.com/anatoli-derese/a2sv-excercise
v0.0.0-20240805074755-5adbbc600635
anatoli-derese
Nextron
v0.0.0-20241216030053-8573b6044fba
bm-197
Nextron
github.com/dexbotsdev/uniswap-v2-v3-arbitrage
v0.0.0-20231007040503-7b0a4d1c503d
dexbotsdev
Nextron
github.com/glacialspring/go-winsparkle
v0.0.0-20250402002608-ba5501b8ba90
glacialspring
Nextron
github.com/glacialspring/static
v0.0.0-20181015024211-023dc73bc332
glacialspring
Nextron
github.com/hngi/Team-Fierce-Backend-Golang
v0.0.0-20200612135333-4f82269a0a14
hngi
Nextron
github.com/lambda-platform/dan
v0.0.0-20221011015638-695b34fb98d4
lambda-platform
Nextron
github.com/lambda-platform/ebarimt-rest-api
v0.0.0-20230429075241-30dbb04b67f7
lambda-platform
Nextron
github.com/lambda-platform/lambda
v0.9.19-...0cf995e71697, v0.9.20-...12b5a6e0c244
lambda-platform
Nextron
github.com/naol7/dist-task-scheduler
v0.0.0-20241120175214-0365b36af82f
naol7
Nextron
v0.0.0-20260213144826-0c7c4a5b5859
reauheau
Nextron
github.com/rickt/slack-weather-bot
v0.0.0-20180704165649-55def291ce83
rickt
Nextron
github.com/zainirfan13/graphql-client
v0.0.0-20220912215956-d304e79da123
zainirfan13
Nextron
TL;DR yes it’s PolinRider and no, they aren’t really ATOs
With a total of 20 compromised packages across npm and Go, we had a diverse sampling for analysis. Here’s a summary of what we found:
PolinRider attribution confirmed. One XOR key and two TRON wallet addresses are identical across our PolinRider research and all 20 packages.
These aren't true account takeovers. Based on campaign behavior, this is the auto-propagation pattern documented in PolinRider Jumps the Fence to Go, Packagist, npm, PyPI.
Low-profile packages impacted. Our most recent threat hunt showed 93.5% of PolinRider victims were individual developers, and these packages align to that pattern.
This analysis focuses on how these compromises happened, and how they’re the predictable consequence of PolinRider getting persistence on thousands of developer machines.
When an ATO isn’t an ATO
The researchers who discovered these compromises describe what happened as a compromised maintainer account. Technically, that's correct. In each case, a malicious version was published under the package owner’s identity but without their consent. But "account takeover" implies a decision by the threat actor. That they picked a target, found a way to gain publishing access, and published malicious versions. That’s not what happened with these packages.
Our July hunt found that 93.5% of PolinRider's confirmed victims are individual developer accounts. The twenty packages in this piece fit that pattern.
html-to-gutenbergandfetch-page-assetsare published by npm userdigelim, and together pull down less than 2k downloads/week - indicating they’re useful to a niche audience, but nowhere near the download counts that would make the maintainer a deliberately-chosen target.The sixteen Go modules read the same way: a Slack weather bot, an arbitrage script, someone's a2sv coursework, a handful of personal utility repos. A few, like the
lambda-platformmodules, sit under a small team's GitHub organization rather than a lone maintainer, but none are the kind of high-traffic dependency a threat actor would deliberately select if they were working down a target list.And while Joyfill has a big following, the malicious versions were beta releases that historically have very low consumption of 2-4 downloads per release.
And when a worm isn’t a worm
To understand how PolinRider has been so successful, it’s useful to compare and contrast this behavior with worms.
A worm is a self-replicating piece of malware that steals credentials, uses the credentials to publish new package versions containing the malware, and in doing so seeks out new, unrelated packages and maintainers on its own. Shai-Hulud, Mini Shai-Hulud, and Miasma are notorious examples. Typically “patient zero” of a worm attack is selected by the threat actor because they have access to high-popularity packages, ensuring the most potential victims.
PolinRider doesn't work that way, but the outcome is similar. A human still has to get infected first, through any of PolinRider's attack vectors: fake take home assignments, lure repos, typosquatted packages. From there, the malware exfiltrates credentials and uses them to push malware into whatever the maintainer has access to. Typically that begins with their GitHub repo, and more recently it's been spreading out into their packages. Anyone who uses that developer's poisoned repos or packages becomes affected.
That handoff, from infected machine to poisoned package, is invisible because nothing about it looks like an intrusion. temp_auto_push.bat, the script behind the GitHub and Go compromises, runs locally on the infected machine using the developer's own already-authorized git credentials. It isn't a second party logging in from somewhere new; it's the same device, the same SSH key, the same account behavior GitHub sees from that developer every day. The script even resets the local system clock before it amends a commit, so the timestamp matches what it replaced and a casual git log shows nothing out of place.
npm packages get the same treatment through a different credential. Once the malware has harvested a maintainer's ~/.npmrc token, it either publishes directly, or it simply waits and lets the maintainer's own next npm publish carry the payload along. Either way, the registry sees a normal, authenticated publish from the account that's always owned that package.
None of this required a human decision at any point after the initial infection. The automation chose these 20 packages the same way it chooses every package it touches: because they were there.
Go modules
Go modules don't have a separate publishing step. A module's identity is its repository path, and a version is a git tag. That means the moment PolinRider force-pushed a maliciously tagged commit to a compromised repo, proxy.golang.org served it to anyone running go get. No token, no upload, no registry account required.
We analyzed the 16 Go modules, and confirmed the payload dropped into each module is DPRK's obfuscated JavaScript loader. It resolves a second-stage payload from blockchain RPC infrastructure (TRON, Aptos, BNB Smart Chain), decrypts it with an embedded XOR key, and executes it with eval() for remote code execution and persistence through detached background processes. The follow-on payloads are the familiar Lazarus toolkit: DEV#POPPER, OmniStealer, and InvisibleFerret, covering credential theft, browser-data theft, crypto-wallet exfiltration, and keylogging.
Propagation runs through temp_auto_push.bat, the same git history-rewriting script we first documented in March. It changes the local system clock to match a commit's original timestamp, amends the commit with the malicious payload, force-pushes, and restores the clock, so a casual git log review shows nothing out of place.
The code signatures have rotated since March rmcej%otb% became Cot%3t=shtP; the decoder function $1e42 became MDy), consistent with an operator retooling against published detection rules rather than abandoning the campaign.
The pseudo-version timestamps are forged
There's a detail buried in those version strings worth pulling out on its own. A Go pseudo-version like v0.0.0-20241216030053-8573b6044fba looks like it's telling you when the code was written. Immediately after the - in this example are the digits 20241216. It’s a timestamp, telling you the version was pushed on December 16, 2024. That timestamp is the commit's committer date, and the committer date is set by whoever made the commit, not by any server. But GIT_COMMITTER_DATE is a normal environment variable and anyone with push access can set it to any date they want before committing.
Paul McCarty, our head researcher, didn't want to take these dates at face value, so he checked them. For each pseudo-version's claimed timestamp, he pulled the corresponding hour from GH Archive, which records every GitHub push event server-side and can't be altered. He looked for the claimed commit inside that hour's actual push activity. Here’s how it’s interpreted:
If it's there, the timestamp is honest.
If the repository demonstrably pushed something that hour and the claimed commit is nowhere in it, the timestamp is forged.
If nothing was archived for that repository in that hour at all, the result is inconclusive.
Of the 16 Go modules in this piece, 11 came back forged. Four were inconclusive, with no archived push in the claimed hour to check against. Only lambda-platform/lambda checked out mostly honest, with timestamps landing inside the campaign's known active window rather than years in the past.
One case, bm-197/chill, we can date exactly. The pseudo-version claims a December 2024 commit. GH Archive shows the actual push happened on June 8, 2026, eighteen months later, appended cleanly onto the developer's genuine final 2024 commit with no force push and no rewritten history. It's a completely ordinary-looking fast-forward push, except that the commit it delivered carries a timestamp claiming to be a year and a half older than the push that created it.
Paul ran the same check against a set of OpenSourceMalware's broader Go-ecosystem PolinRider tracking, 64 modules total, not just the 16 tied to this incident, and the pattern held: 43 forged, 5 honest, 16 inconclusive. Every honest timestamp fell inside a seven-week window in May-June 2026. Every claimed date from 2018-2025 that could be checked came back forged, 42 for 42. A developer's ordinary commit-then-push habits don't sort themselves neatly by year. Deliberate backdating does.
None of this trips force-push detection, because none of it is a force push. The malicious commit's parent is genuinely the developer's own last commit; nothing was rewritten or overwritten. The only thing wrong is the date it claims to have been made, and that's not a field any of the standard integrity checks look at.
None of it is remediated by taking the repo down, either. proxy.golang.org is a write-once cache. By design, the first time anyone requests a module version, the proxy fetches and stores it permanently. This ensures Go builds stay reproducible even after the source disappears. Eight of the thirteen accounts behind these modules are gone now, whether suspended by GitHub or deleted by the developer once they learned what happened. None of that mattered to the cache. Every forged module is still downloadable right now, through go get, regardless of what's happened on the GitHub side.
A benefit of adding all these threat reports to our database is we can immediately see when a maintainer has been compromised beyond a single package. We found that a single account's footprint often runs larger than what any one report captures. Pivoting on the hngi account showed six more malicious Go module threat records in our database:
digelim packages
npm works differently from Go, where publishing requires a credential separate from git access. PolinRider's toolkit has to go get it specifically. Generally this requires the victim to consume malware through one of PolinRider's upstream tactics, such as forking a poisoned repo or consuming a typosquatted package. We saw the mechanism for that step in the ChainVeil packages Checkmarx analyzed in June, where DPRK's malware credential harvester runs harvest("~/.npmrc", ".npmrc") right alongside the code stealing SSH keys and browser passwords. Once that token is on hand, publishing a poisoned release doesn't need the maintainer's cooperation at all.
html-to-gutenberg and fetch-page-assets show that same family, one step further along. The blockchain resolution structure, Tron primary, Aptos fallback, BSC as a third RPC, is identical to what we catalogued in March, down to reusing one of the original two XOR keys. The second key here, ThZG+0jfXE6VAGOJ, is new to our own dossier, but it isn't new to the campaign: it shows up in the ChainVeil and Joyfill packages too, another rotation shared across incidents rather than something unique to these two packages. What is actually novel is the entry point. Where March's campaign reached GitHub users directly through git commits, here the same .vscode/tasks.json folderOpen trick reaches through a compromised npm package instead, which only works if someone has already cloned or installed the maintainer's poisoned repo or package.
The full chain JFrog documented runs in five stages:
Autorun. A committed
.vscode/tasks.jsonwithrunOptions.runOn: 'folderOpen'executes the moment the project folder opens in VS Code, Cursor, Antigravity, or GitHub Desktop, bypassing npm v12's lifecycle-script protections entirely.The loader.
public/fonts/fa-solid-400.woff2isn't a font. It's JavaScript that reads XOR-encrypted payloads from on-chain transactions on Tron (viaapi.trongrid.io) and Aptos (viafullnode.mainnet.aptoslabs.com), withbsc-dataseed.binance.organdbsc-rpc.publicnode.comas additional RPC infrastructure.The backdoor. A socket.io channel gives the actor interactive command execution, file upload and download, and clipboard access.
The bootstrap. The malware installs Node dependencies and a portable Python interpreter, pulled from
bootstrap.pypa.io, into paths including%LOCALAPPDATA%\Programs\Python\Python3127.The stealer. A reversed-base64, zlib-wrapped Python infostealer harvests browser credentials, crypto wallets, password managers, developer tool configs, and OS keystores, exfiltrating over both the HTTP C2 and a Telegram bot.
Joyfill compromise
Joyfill's chain is the same family as the packages above, run through a different entry point. There's no git commit or repo fork here at all: the malicious code is appended directly after the legitimate package code and executes at require() time, which sidesteps npm install --ignore-scripts entirely since there's no install script to skip. It also checks the hostname before doing anything, staying quiet on github-runner, buildbot, buildkitsandbox, and microsoft-standard-WSL2, an evasion aimed squarely at CI systems, something neither html-to-gutenberg nor fetch-page-assets bothered with.
From there it's the familiar chain: Tron and Aptos addresses resolve a BSC transaction containing an XOR-encrypted payload, using the same two keys documented in OpenSourceMalware's original PolinRider infrastructure. That spawns a detached process pulling a payload from hxxp://23[.]27[.]13[.]43/$/boot with a Sec-V: A9-0135-3 marker header. What lands is a 77KB build of InvisibleFerret (also tracked as DEV#POPPER RAT), with a Python infostealer component assessed as OmniStealer. Persistence comes from injecting into VS Code, Cursor, Discord Desktop, GitHub Desktop, and the global npm CLI.
The status column below is the whole argument in one table. Two indicators unique to Joyfill; the rest are wallets and C2 infrastructure we and three other research teams have now watched this same actor reuse for five months.
Indicator
Value
Status
166.88.134.62:443 / :80
primary C2
novel to this incident
23.27.13.43
stage 2 loader
novel to this incident
198.105.127.210:443 / :80
C2
shared with PolinRider infrastructure
23.27.202.27:443 / :27017
C2
shared with PolinRider infrastructure
TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP
Tron wallet
shared with PolinRider infrastructure
TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG
Tron wallet
shared with PolinRider infrastructure
TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v
Tron wallet
shared with PolinRider infrastructure
Crossover into PHP
Just as we pivoted on a Go maintainer identity to find more affected Go modules, our platform quickly showed us that 23.27.202.27:443 / :27017 is shared infrastructure with another known compromised package: roberts/leads. In yet another example of an account takeover that wasn’t really an ATO, this package was discovered in May by Socket with many of the same PolinRider indicators. They hypothesized, and we agree, that the maintainer was likely compromised through a fake interview lure.
Packagist operates similarly to Go, where the registry stores metadata that points at your GitHub repo. A GitHub webhook tells Packagist to re-read the repo whenever you push, and composer require downloads the code as a zipball of the tagged commit straight from GitHub. Again: no separate artifact, no separate upload credential. The repo is the source of truth.

PolinRider attribution
JFrog's investigation into html-to-gutenberg and fetch-page-assets is detailed work: a five-stage chain from VS Code autorun through a fake font-file loader to a Python infostealer, with a full C2 path and Telegram exfiltration channel mapped out. They cited Nextron's discovery of 16 Go modules carrying the identical payload.
Socket's Joyfill writeup matched the loader's multi-chain resolution order, decryption keys, and marker structure against PolinRider-family indicators from its own prior research.
Here's what lines up against our own PolinRider infrastructure:
XOR key
2[gWfGj;<:-93Z^Cappears across this campaign's infrastructure, and in our own March 8 report on the original GitHub-based campaign.TRON wallets
TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAPandTXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcGare the same dead-drop addresses we published in March, reused across every incident in this piece.Th
Sec-Vmarker header carries anA9-0135-3value in Joyfill and anA8-**prefix in JFrog's findings. We've tracked this as a versioning convention rather than separate operators (the ChainVeil typosquat packages surfacedA6-markers back in June).The fake font-file and
.vscode/tasks.jsoncombination (a JavaScript loader disguised asfa-solid-400.woff2, triggered by afolderOpentask) is PolinRider's signature delivery mechanism. JFrog documented it independently, without naming the actor. Nextron's Go module discovery is built on the same file: their scanner found matching SHA-256 hashes53abf37710d6f2e35694fbe7cfaf1108127cbc001ce3e6bf994d0486cae5a0e8,13e9a3c41e038bf9d8fcb0831305819819e4f7f4452bc20a04b9bf2756ee22e8) forfa-solid-400.woff2inside the Go modules, which is almost certainly what JFrog's "same payload" credit is based on, even though neither report says so in prose.temp_auto_push.bat, the propagation script behind the 16 Go modules, is the same git history-rewriting tool we first documented in March, now running rotated code signaturesrmcej%otb%toCot%3t=shtP) consistent with the retooling we've tracked since.
Put together, this isn't overlapping tools with multiple hands on them. The infrastructure, the XOR key, the wallets, the marker convention, predates all three of these incidents by months, so it can't be coincidental reuse discovered after the fact. The file match ties the Go modules to the exact npm loader JFrog documented, not just a similar one. And the backdating pattern, the multi-repo bursts, and the lateral movement into another developer's repository are the kind of precise, repeated, cross-account behavior one operator running one toolkit produces, not what sixteen independent compromises coincidentally reusing a public sample would look like. That's what makes this PolinRider, and not just PolinRider-shaped.
Full IOC set
Everything relevant across all three compromises, consolidated in one place.
XOR keys
2[gWfGj;<:-93Z^C
ThZG+0jfXE6VAGOJ
TRON wallets
TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP
TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG
TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v
Aptos addresses
0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e
0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3
0x533b2dbcaeff19cd1f799234a27b578d713d8fcaa341b7501e4526106483e0b1
File hashes
fa-solid-400.woff2 — SHA-256 53abf37710d6f2e35694fbe7cfaf1108127cbc001ce3e6bf994d0486cae5a0e8, 13e9a3c41e038bf9d8fcb0831305819819e4f7f4452bc20a04b9bf2756ee22e8
C2 infrastructure
166.88.134.62:443 / :80 — novel to Joyfill
23.27.13.43 — novel to Joyfill
198.105.127.210:443 / :80 — shared PolinRider infrastructure
23.27.202.27:443 / :27017 — shared PolinRider infrastructure
RPC endpoints
api.trongrid.io, fullnode.mainnet.aptoslabs.com, bsc-dataseed.binance.org, bsc-rpc.publicnode.com
C2 paths
/$/boot, /$/{id}, /verify-human/{channel}, /snv, /u/e, /u/f, /d/python.zip, /d/python.7z, /d/7zr.exe
Marker convention
Sec-V HTTP header, A#- prefix versioning: A8- (JFrog), A9-0135-3 (Socket/Joyfill). Same scheme, not separate operators.
Propagation artifacts
temp_auto_push.bat, config.bat — local git history rewriting and force-push
Code signature: rmcej%otb% (original, March) rotated to Cot%3t=shtP (since June); decoder $1e42 rotated to MDy
Telegram exfiltration
Bot token prefix 7870147428:AAGbYG..., chat ID 7699029999
Malware family naming
InvisibleFerret, DEV#POPPER RAT, OmniStealer (Python infostealer component). Same or overlapping payload family across reports.
What this means for maintainers
If you maintain a package in any of these ecosystems, the fastest way to find out where you stand is to check your own machine and repositories, not wait for a registry notice.
Paul McCarty, OpenSourceMalware's head researcher, wrote a scanner for exactly this: polinrider-scanner.sh, in the PolinRider dossier repo. Run it against your own projects with ./polinrider-scanner.sh /path/to/projects, and it checks your config files postcss.config.mjs, tailwind.config.js, eslint.config.mjs, and similar) for the obfuscated payload signature, looks for the temp_auto_push.bat and config.bat propagation artifacts, and checks your git reflog for amended commits: the same mechanism behind the Go module compromises above. It exits 0 clean, 1 infected, 2 error, so it drops easily into a CI check.
One limitation worth knowing before you rely on it: it's built against the original March signature rmcej%otb%), not the rotated one we documented in July Cot%3t=shtP), and it doesn't check for this campaign's blockchain-resolved or npm-token indicators. Treat it as a first pass, then check your own projects against the IOC tables above by hand, especially the .woff2 loader pattern and any sign your ~/.npmrc was touched if you publish to npm.
Beyond the scan itself, what you should protect differs by ecosystem. If you maintain a Go module, there's no separate publish credential to protect, because there isn't one. Git push access to your repository is the entire control surface. Hardware-backed MFA on GitHub and clean SSH key hygiene do more for you here than anything registry-side ever could.
If you maintain an npm package, rotate your registry token as a matter of course any time the scanner, or anything else, suggests your machine was compromised, even before you know how. Waiting for confirmation that your token was actually used doesn't help. By the time you'd see that, the package is already out.
And regardless of ecosystem: if a .vscode/tasks.json with a folderOpen trigger shows up in a clone you didn't write, that's not a productivity script. Turn off automatic task execution Task: Allow Automatic Tasks In Folder set to off or prompt) before you open anything you didn't build yourself.
Learn more
JFrog Security Research: Hijacked npm Packages Use Novel VSCode Autorun and Blockchain Dead Drops to Deploy a Credential/Crypto Stealer
Checkmarx Zero: ChainVeil: A Malicious npm Supply Chain Attack by SuccessKey
OpenSourceMalware: PolinRider: DPRK Threat Actor Implants Malware in Hundreds of GitHub Repos
OpenSourceMalware: TasksJacker DPRK Attack Compromises GitHub Users Via VS Code Tasks
OpenSourceMalware: PolinRider Rides Again: North Korean Attack Expands Across GitHub
OpenSourceMalware: PolinRider Jumps the Fence to Go, Packagist, npm, PyPI
OpenSourceMalware: PolinRider Confirmed Footprint Grows 6.5x Since March
OpenSourceMalware: ChainVeil and ViteVenom are DPRK's PolinRider Campaign
OpenSourceMalware: How Malware Abuses NPM Lifecycle Scripts and VS Code Tasks