BLOG
PolinRider Jumps the Fence to Go, Packagist, npm, PyPI
DPRK’s most successful campaign spread from GitHub into several package ecosystems without any changes to the malware.
By c0a15726-c5b1-4b0d-85e6-fe15553df9e2 ·
PolinRider is a DPRK-linked supply-chain campaign, part of the Lazarus Group / Contagious Interview activity cluster, that I first identified and named earlier this year. Rather than Contagious Interview's classic playbook of conducting fake interviews accompanied by typosquatting or publishing throwaway malicious packages, PolinRider takes over legitimate GitHub accounts and quietly injects an obfuscated JavaScript loader into repositories the maintainer already owns.
When I first documented PolinRider earlier this spring, it was 100% a GitHub problem. A North Korean actor was quietly appending obfuscated JavaScript to config files inside legitimate repositories. The payload hides in config files, fake .woff2 font files, and .vscode/tasks.json triggers, then uses blockchain dead-drops to download the Lazarus stealer toolkit. A Windows script rewrites git history to hide the tampering, making it look like a normal commit. Our first report caught 675 compromised repositories. Our follow-up a few weeks later put the count at 1,951 repos across 1,047 owners.
Then a few weeks ago, the OpenSourceMalware and Socket teams observed PolinRider publishing payloads to multiple package registries including Packagist and Go. Almost 200 malicious release artifacts tied to 111 unique packages and extensions: more than 80 Go modules, 10 Packagist packages, a cluster of NPM libraries, and a couple of Chrome extensions thrown in for good measure. The campaign didn’t have to get more sophisticated to make this jump. It got the jump for free, because of how these ecosystems are built.
Why ecosystem jumping was inevitable
There are two ways a package manager can turn a name like github.com/user/thing into code on your disk.
Model A — the registry hosts its own artifact
NPM and PyPI host their own artifacts. To publish, a maintainer builds a specific artifact file, typically a tarball or a wheel, and uploads it to the registry’s storage using a publishing token. The registry keeps its own copy. When you install, you pull that copy from the registry — not from GitHub. GitHub is, at most, where the source happens to live. The registry credential is a completely separate key from the GitHub credential.
Model B — the registry resolves from the Git repo
Go modules and Packagist resolve from the Git repo: There is no separate “upload” step and, in Go’s case, no artifact-hosting registry at all.
Go: A module’s identity is its repository path, and a version is a git tag.
go get github.com/user/thing@v1.4.0asks the module proxy (proxy.golang.org) for that tag, and the proxy fetches whatever is in that git commit. There is no publish token. If you can push a tag to the repo, you have published a Go module version.Packagist: 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 requiredownloads 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.
For PolinRider, this collapses two attacks into one. The moment the actor takes over a GitHub account and pushes a malicious commit — which is exactly the capability the campaign already had at scale — every Go module and every Composer package backed by that repo is automatically compromised too. Repository access and publishing access are the same thing in these ecosystems. No stolen NPM token required. No PyPI credential required. The registry does the distribution for you.
The evidence, ecosystem-by-ecosystem
Packagist / Composer
The PHP ecosystem is particularly vulnerable to these force push style attacks. So far we have identified 10 packages in the Packagist registry that are part of the PolinRider campaign. Each is distributed through Composer’s normal require flow, because Composer trusted the linked GitHub repo.
thiio/kubernetes-php-sdk - GitHub compromised commit
arsl/optima-class - GitHub compromised commit
olc/olc-php - GitHub compromised commit
sevenspan/laravel-whatsapp - GitHub compromised commit
adxio/twig-hmvc - GitHub compromised commit
sevenspan/code-generator - GitHub compromised commit
lambda-platform/moqup - GitHub compromised commit
roberts/leads - GitHub compromised commit
sevenspan/laravel-chat - GitHub compromised commit
plusinfolab/logstation - GitHub compromised commit
The sevenspan Packagist namespace shows how this worked in practice. The affected Composer packages were backed by GitHub repositories maintained by the 7span organization. Once the attacker had write access to those repos, they could add the PolinRider payload directly to the source code. In the example below from 7span/react-list, the malicious JavaScript was appended to vite.config.js. The commit timeline shows the campaign’s signature evasion: malicious payload is added to the vite.config.js file via a force push on commit 1c1d61b.
Embedding a JavaScript loader into a PHP package
Wait, is that JavaScript in a Packagist package? Yes.
This is a classic DPRK tactic where the threat actors will leverage VS Code to run JavaScript payloads regardless of what language the host repository uses.
First, the threat actor pushes two new directories to the target source code repository.
One directory holds VSCode configuration files with names like extension.json, launch.json, spellright.dict and tasks.json. Each has specific use cases for VSCode but in this case, DPRK threat actors will use these different files in unique ways. Most of the time, the payload will be in the tasks.json file.
The second is a public/fonts folder that holds what look like font files. However, one or more of those fonts is actually malicious JavaScript.
If you look at line 16 of the tasks.json file, you’ll see a command that runs uses node to run one of the files pretending to be font files: fa-solid-400.woff2. In that woff2 file is the JavaScript loader that bootstraps the first stage of a DPRK Beavertail or Ottercookie attack chain.
This use of VSCode tasks files allows the threat actor to embed their standard JavaScript loaders into a PHP package. It’s pretty ingenious, honestly.
Go modules
Go is where the repo-as-registry model bites hardest, and it accounts for the bulk of the cross-ecosystem damage: Our research found compromise traces in more than 80 Go modules (roughly 61 of them confirmed as distinct malicious module versions). There is no separate publish action to point at, because in Go there is no separate publish action. A module’s identity is its repository path and a version is a git tag, so the instant the actor pushed a tagged malicious commit to a compromised repo, proxy.golang.org was ready to serve it to anyone running go get — no token, no upload, no registry account required.
That same design is what makes the Go compromises hard to see after the fact. Because the PolinRider campaign rewrites git history — force-pushing anti-dated commits to preserve the original timestamps — the GitHub landing page and visible commit log are unreliable indicators. This means defenders should ignore the rendered history and instead review repository activity logs, package release metadata, and the config files themselves. In a repo-sourced ecosystem there is no pristine registry copy to diff against; the tampered repo is the published artifact.
NPM and the PyPI are less susceptible to PolinRider methods
GitHub users that maintain NPM packages were also hit, but as far as we can tell none of the malicious force pushes made their way into the NPM registry. However, just to clarify, its possible for PolinRider style attacks to compromise NPM packages just like with Packagist and Go package, but the threat actors would have to wait for the victim to publish the package themselves at some later point. Sometimes the victim notices the compromise and doesn’t publish their NPM changes.
Same thing with PyPI. Take the Xpos587 GitHub account, seized on June 23, 2026 around 10:00 UTC: multiple repositories under it, including the Python projects Xpos587/git2md and Xpos587/markfetch, were rewritten in one near-simultaneous burst.
The actor had full write access to those Python repos on GitHub. And yet we saw no malicious releases on the maintainer’s PyPI account. Our assessment: the actor either lacked the PyPI publishing credentials, was blocked by PyPI controls, or otherwise couldn’t push a package version.
That synchronized, bulk-modification pattern is the fingerprint of an account-level breach, not routine maintenance. If you look at the activity log you don’t see any commit on June 23rd. That’s because the threat actor force pushed existing commit 79bdb26 from May 3rd.
The point we're making: Where the ecosystem required a separate credential to upload an artifact (NPM, PyPI), the GitHub compromise stalled at the registry boundary. Where the ecosystem resolved straight from the repo (Go, Packagist), there was no boundary to stall at.
It’s important to note here that PolinRider has definitely published malicious NPM packages, and continues to do so weekly. However, they have typically been “malicious from birth” rather than account takeover style compromises. Ie., DPRK threat actors publish their own NPM malware and just append their standard JavaScript payload to the package, or in some cases they will use the VSCode trick to try and compromise unsuspecting NPM users.
The PolinRider payload hasn’t evolved because it didn't need to
The malware did not need to change for this expansion to happen. The same four infection methods and the same loader we documented on GitHub carried over into Go and Packagist:
Obfuscated JavaScript appended to config files (
postcss.config.mjs,tailwind.config.js,eslint.config.mjs,vite.config.js,next.config.mjs), padded with whitespace so the executable code sits off the right edge of a default editor view..vscode/tasks.jsonwith arunOn: folderOpentrigger that curl-pipes a shell command the instant a victim opens the project.Fake
.woff2font files hiding JavaScript, so scanners skip them as binary assets.Malicious npm dependencies (typosquats of
tailwindcss-animateand friends) whosepostinstallhooks do the config-file injection.
The loader still resolves its second stage from blockchain dead-drops across TRON, Aptos, and BNB Smart Chain, decrypts it with embedded XOR keys, and runs it through eval(). The unpacked chain is the familiar Lazarus toolkit: a Beavertail loader that fingerprints the host OS and selects an InvisibleFerret implant (also reported as DEV#POPPER RAT and OmniStealer) for credential theft, browser-data theft, wallet exfiltration, and socket.io-based C2. And on the repo side, temp_auto_push.bat still rewrites the latest commit to preserve its original timestamp, bypasses pre-commit hooks with --no-verify, and force-pushes to erase the evidence.
The campaign’s reach tripled from GitHub to four ecosystems on the strength of infrastructure it already had. The new surface area was a property of the ecosystems, not an achievement of the malware.
Timeline
Date
Event
Jan 8, 2026
Anti-dated malicious commits later found in 7span/Packagist repos
Mar 8, 2026
Initial PolinRider disclosure — 675 compromised GitHub repos
Apr 12, 2026
Campaign expands to 1,951 repos / 1,047 owners; merges with TasksJacker
May 16, 2026
Maintainers begin removing .woff2 payloads from affected repos
Jun 23, 2026
Xpos587 account takeover; bulk Go-module compromise (~10:00 UTC)
Jul 1, 2026
Cross-ecosystem spread confirmed: 162 artifacts across Go, Packagist, npm, Chrome
Indicators of Compromise (IOCs)
Compromised accounts and repos
You can find all the GitHub, NPM, PyPI, Packagist and Go packages, and associated accounts, on OpenSourceMalware by using the #polinrider tag. Here’s a direct link: https://opensourcemalware.com/?search=%23polinrider
Blockchain C2 dead-drops
PolinRider continues to use the same TRON and Aptos resources since at least February 2026:
TRON: TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP
TRON: TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG
Aptos: 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e
Aptos: 0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3
XOR decryption keys
2[gWfGj;<:-93Z^C
m6:tTh^D)cBz?NM]
Detection markers
Loader signatures: rmcej%otb% | _$_1e42 | global['!']
New variant: Cot%3t=shtP | function MDy(f) | global['_V']
Propagation script: temp_auto_push.bat (LAST_COMMIT_DATE / LAST_COMMIT_TIME)
Hidden payloads: JavaScript inside .woff / .woff2 under public/ static/ assets/
VS Code trigger: .vscode/tasks.json with runOn: folderOpen + curl|bash
What maintainers should do
The remediation for a repo-sourced ecosystem is different from npm or PyPI, because there’s no registry copy to pull down and inspect separately — the repo is the package.
Audit registry publication history against repo modification. Any Go tag or Composer release that landed right after an unexpected push deserves scrutiny.
Treat repo access as registry access. Require hardware-backed MFA on GitHub, revoke stale tokens, and check whether an abandoned domain can still recover your maintainer account — domain-takeover-based account recovery is a plausible entry path here.
Grep before you trust. Look for content after
export default/module.exportsin config files, JavaScript inside.woff2files,runOn: folderOpenin.vscode/tasks.json, andtemp_auto_push.batat the repo root.If you find any of it, treat the machine as compromised. Rotate every secret from a clean host, rebuild from a known-good lockfile, and force-push verified-clean history with commit signing enabled.
Conclusion
PolinRider didn’t need a new exploit to become a four-ecosystem campaign. It needed the Go and Packagist model, where a Git commit is a published release, and where the credential that lets you write code is the same credential that ships it to the world. That design is convenient — it’s a big part of why Go modules and Composer are pleasant to use — but it means a GitHub account takeover is a supply-chain publish. The ecosystems that made the actor work for a second credential, npm and PyPI, are the ones that slowed them down.
If you maintain a Go module or a Composer package backed by a public repo, your GitHub account security is your registry security. There is no second door.
If you encounter packages or repositories matching these indicators, please report them to OpenSourceMalware.com.
Stay safe out there.