BLOG

A Developer's Guide to Getting Rid of PolinRider

How you get infected with DPRK's PolinRider, why just rotating creds and wiping laptops doesn't stop it from coming back, and the field-tested steps that do.

By cb482791-4ef1-4762-96ad-b0ca4bdd538e ·

A Developer's Guide to Getting Rid of PolinRider

PolinRider is a campaign run by North Korea (DPRK) that targets software developers. We first documented PolinRider in early March 2026, which at the time had hit 300+ GitHub owners. As of our most recent hunt in July, that footprint has grown to 2000+ owners, with more than 4000 repos compromised.

This campaign overwhelmingly targets individual developers rather than organizations. Victims are infected via one of three vectors: a fake job interview with a booby-trapped coding test, a malicious package disguised as a legitimate developer tool, or a fellow developer’s repo/package/commit that’s been infected.

Once it’s on your machine, the malware steals things like browser credentials, cryptocurrency wallet data, SSH keys, AWS credentials, environment variables and API keys, git credentials, and tokens. Then it self-spreads in a worm-like manner, using your credentials to plant the same malicious code into your other projects, repositories, and accounts. After the initial infection, human attackers don’t have to do anything.

We often hear from infected developers who did everything “right”. They rotated every credential, wiped the laptop, etc. but the malware came back anyway, sometimes several months later. Because this campaign is both hard to detect and difficult to remediate, it’s provided DPRK with persistence on thousands of developer machines, effectively forming what we’re calling a “human botnet.” We don’t know what they plan to do with this infrastructure, but given their history of cybercrime, it’s nothing good!

This post covers the ways people actually get infected, why the standard remediation playbook keeps failing against this specific campaign, and a cleanup guide built and field-tested by developers who lived through it themselves.

Three ways to become a PolinRider victim

Vector 1. Forking a DPRK-owned repo

The Contagious Interview campaign, where DPRK personas have posed as recruiters since at least 2023, uses social engineering to convince targets they’re interviewing for a high-paying software development job. The lure hasn’t changed much over the years, but the payload increasingly leans on a malicious .vscode/tasks.json file to do the actual infecting. If you were hit through a fake interview, a poisoned tasks.json file is the most likely reason why.

StakingGame, a fake Web3/blockchain-deployment assessment, illustrates how it works. Its .vscode/tasks.json contains a "runOn": "folderOpen" task that pipes curl from an attacker Vercel subdomain straight into bash/sh. The machine is compromised the moment the candidate opens the folder in VS Code, before you even write any code.

Vector 2. Installing a DPRK-owned malicious package

PolinRider also ships trojanized packages directly to public registries in the form of typosquats of popular packages. The flagship example is the npm package tailwindcss-style-animate (and sibling names like tailwind-mainanimation, tailwind-autoanimation). Installing it, directly or transitively as a dependency, injects the PolinRider JS loader during the build.

Vector 3. Trusting a PR, fork, or package from another victim

When a developer is infected with PolinRider, it acts a little bit like an account takeover (compromise of legitimate account). But instead of locking them out and pushing poisoned versions, PolinRider quietly self-propagates using the victim’s own legitimate access:

The malware’s infostealer stage silently harvests any local credentials that developer has on their machine… If the malware finds GitHub credentials, it can use those to automatically create new malicious GitHub repos, etc, but even if the malware doesn’t find a PAT, it can still poison GitHub. Here’s how: The malware finds any local git repos on the compromised workstation, and it looks for one of these JS config files: postcss.config.mjs, eslint.config.mjs, tailwind.config.js, vite.config.js. If it finds one, it appends some white space to the end of the file, and then tacks on a malicious payload. If it doesn’t find one of the files in the list, it just creates one in the root of the git repo, and moves on to the next repo. The threat actors use a neat little trick to hide this payload: they overwrite the last git commit with the new changes using a git commit –amend –no-verify command. Then the threat actor pushes the changes to origin, or if they don’t have GitHub credentials, they just wait for the victim to create new changes, and their malicious payload will ride along If the developer is also a maintainer, the malware can get pushed into npm, Go, Packagist (PHP), and PyPI packages they own.

Why PolinRider keeps coming back

The propagation script runs locally, not from some server the attacker controls

The mechanism behind PolinRider’s persistence is a script called temp_auto_push.bat, and it’s not a remote operation. It runs on the infected device itself, using whatever git credentials are already cached there. It isn’t a second party logging in from somewhere new. Rather, it’s your same device, your SSH key, the same account behavior GitHub sees from you every day. The script resets the local clock, amends a commit so the timestamp matches the one it’s replacing, and force-pushes. A casual git log shows nothing wrong. None of that requires a credential to leave the machine. It runs, does its work, and runs again whenever whatever triggers it fires again, a scheduled task, a detached background process, or a developer simply opening an infected project a second time.

That’s enough on its own to explain most of what looks like reinfection, without needing to imagine an attacker manually reusing an old credential from somewhere else:

  • The persistence artifact from the original infection was never actually removed. If a detached process, a scheduled task, or a launch agent from the initial infection is still alive, it doesn’t need a human to tell it to push again. It just does, automatically, using whatever credentials are currently valid on that machine, including ones that were just rotated in.

  • A “new” machine gets infected the same ordinary way the first one did. If the original poisoned repos or forks were cleaned locally but never fully scrubbed from GitHub itself, cloning or opening one of them again on the replacement machine restarts the exact same local, automated cycle from scratch. This also explains a branch that was never checked out locally still showing up force-pushed. This is because pushing to a named remote branch doesn’t require having that branch checked out first, only a valid credential and a repo that already knows about the remote.

A rotation is only as complete as the list of things you rotate

The stealer takes more than the credentials a checklist usually covers. Standard remediation advice, including our own, says to rotate GitHub password, PATs, SSH keys, and API keys, the things a person thinks of as “my dev access.” That’s not the full list of what got taken, and anything left unrotated, a password manager vault, a browser-saved login, an old cloud key sitting in an .env file, is still usable by whoever took it.

GitHub gives out more kinds of access than “password, PAT, SSH key”

A standard rotation checklist covers the credentials a developer thinks about day-to-day. It’s worth also checking the ones that are easy to forget exist at all. This includes deploy keys added directly to a single repository, OAuth Apps or GitHub Apps authorized once and never revisited, and CI/CD platforms like Vercel, Netlify, or GitHub Actions holding their own separate copy of push or publish credentials. None of these show up when you generate a new SSH key pair or revoke a PAT.

The payload keeps changing shape, so “I don’t see the old marker” doesn’t mean clean

We’ve seen PolinRider rotating its own signatures, which is common for threat actors looking to avoid detection. The original rmcej%otb% marker and _$_1e42 decoder function gave way to Cot%3t=shtP and MDy. That’s consistent with an operator retooling against published detection rules rather than abandoning the campaign.

The command-and-control (C2) side rotates too. Instead of a domain or server, PolinRider fetches its payloads from encrypted blockchain transactions, TRON primarily, Aptos as a fallback, infrastructure that can’t be seized or taken down the way a server can. We recently discovered DPRK’s new NullReceiver technique, which replaces the fixed, publicly-known EtherHiding burn address with a brand new, disposable destination address on every lookup. A payload that no longer matches last month’s grep patterns, and that resolves its C2 through Ethereum instead of TRON or Aptos, reads less like a new, unrelated malware family and more like the third generation of the same one.

Some of it, you can’t fully undo

Even a perfect rotation doesn’t reach everywhere the campaign has already spread. Go’s module proxy, proxy.golang.org, caches the first version it ever sees of a module permanently, by design, so builds stay reproducible. That means none of it is remediated by taking the repo down. A malicious commit that got tagged and fetched once is downloadable through go get forever, regardless of what happens to the source repository afterward.

And because propagation follows the shape of a developer’s actual working relationships, not just their own account boundary, cleaning your own side of a collaboration doesn’t close the loop if the other side is still infected. A collaborator invite, an organization membership, or a shared CI pipeline all extend the automation’s reach past any one person’s credentials. If the account on the other end of that relationship is still running the same resident propagation, repositories you didn’t personally touch can still pick up the payload through access that was granted long before either of you knew anything was wrong.

How to actually get over PolinRider

On March 6, 2026, a developer named robellorin opened a GitHub Community discussion asking why obfuscated code kept appearing in next.config.js and postcss.config.js across repos they hadn’t touched, code that had first shown up the previous November. Over the next five months the thread grew to 17 participants, developers comparing partial pictures from different repos and different machines until they added up to a full one. A developer posting as semitha-dev posted a genuinely useful and clear write-up of how the payload hides inside a config file, a table of indicators of compromise, exact byte-size thresholds for spotting an infected file, a cleanup sequence organized by what to do first, and a section on what not to waste time on.

What follows combines that guide that combines our own PolinRider research with the great work from semitha-dev. If you’ve been infected, be sure to work through it in order. Skipping ahead to credential rotation before the earlier steps just means you’ll have rework in your future.

1. Turn off automatic task execution before you touch anything else

If a .vscode/tasks.json with a folderOpen trigger shows up in a clone you didn’t write, that’s not a productivity script. Before you open the project again for any reason, including to inspect or clean it, go to VS Code settings, search “task,” and set Task: Allow Automatic Tasks In Folder to off or prompt. Do this globally, not just in the one project. It costs nothing and it means every remaining step below can’t accidentally re-trigger the thing you’re trying to remove.

2. Run our free scanner as a first pass

The polinrider-scanner.sh (find in our PolinRider dossier repo) 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. It exits 0 clean, 1 infected, 2 error, so it drops easily into a CI check.

Run this against your projects:

git clone https://github.com/OpenSourceMalware/PolinRider
cd PolinRider
./polinrider-scanner.sh /path/to/your/projects

One limitation worth knowing before you rely on it: it’s built against the original March signature (rmcej%otb%), not the rotated one 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, not a clean bill of health, then work through the rest of this list by hand regardless of what it reports.

3. Kill the live process

On Windows, list running node processes and their command lines with Get-CimInstance Win32_Process -Filter "Name='node.exe'", then Stop-Process on anything running node -e against obfuscated content. On macOS and Linux, ps aux | grep -E "(python|node).*-(c|e)" | grep -v grep catches the equivalent.

This matters, but it’s not the fix on its own. The payload isn’t a resident infection with a foothold that killing a process removes for good. It re-fetches and re-executes from the blockchain dead-drop the next time the infected config file loads, whether that’s the next npm run dev, the next build, or the next time CI pulls the repo. Killing the process buys you a clean moment. It doesn’t buy you a clean repo.

4. Clean every infected file, on GitHub, not just on your machine

The payload gets appended to the export default config; line in files like postcss.config.mjs after roughly 280 spaces, invisible in an editor or a normal diff unless you scroll right. A clean-looking config that’s 5,000 bytes instead of the usual 80 to 200 is the tell. Check .woff2 files under public/fonts/ and similar asset directories too: the campaign also hides its loader inside a file made to look like a font (fa-solid-400.woff2 is the name to search for specifically), which reads as binary and gets skipped by most manual review entirely.

Every time the payload runs, it scans the machine for other JavaScript projects, injects itself into those too, and pushes to GitHub using whatever git credentials are already cached locally, whether or not you’ve opened those other repos yourself recently. That’s why what’s sitting on GitHub can differ from what’s on disk: the process reaches further than your own editor does, sometimes into repos you haven’t opened locally in months. Clean infected files through GitHub’s own web editor, repo by repo, rather than trusting a local git pull to surface everything.

5. Rotate every credential the malware could reach

The stolen credential behind a compromise isn’t always the obvious one. Some victims see no foreign logins in their GitHub security log at all because the credential taken wasn’t a GitHub password or PAT, it was a token for a connected CI/CD platform like Vercel, Netlify, Railway, or Render. Those platforms hold their own GitHub App installation with push access, so a poisoned commit never has to touch GitHub’s login page at all. Rotating GitHub credentials alone leaves that path open.

The full list, combined with our own stage breakdown of the stealer:

  • GitHub password, PATs, SSH keys, and authorized OAuth Apps and GitHub App installations (not just the keys, the app grants too)

  • Cached git credentials (cmdkey /delete:git:https://github.com on Windows; on macOS, open Keychain Access, search “github,” and remove any internet password entries, then re-add access only via SSH key).

  • If you use the GitHub CLI, it likes to stash credentials in ~/.config/gh/hosts.yml.

  • CI/CD platform tokens for anything connected to your repos: Vercel, Netlify, Railway, Render, and equivalents

  • Every secret in every .env file the malware’s build-time process could read through process.env

  • Cloud credentials (AWS, GCP, or equivalent) and database credentials

  • Browser-saved passwords and, if you use one, your password manager vault. If the vault itself was harvested at the time of infection, treat everything stored in it as compromised, not just the credentials you happen to remember were in there

  • Your npm registry token, if you publish packages, even before you have confirmation it was used. Waiting for proof doesn’t help: by the time a poisoned version shows up on the registry, it’s already out

What you’re protecting 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, so hardware-backed MFA on GitHub and clean SSH key hygiene matter more here than anything registry-side ever could.

6. Check for a resident second stage

Repeated macOS Keychain prompts asking for GitHub credential access, continuing even after being denied, are a sign of a detached process looping to re-acquire cached credentials. Denying a Keychain prompt doesn’t kill the process asking for it, it just retries. Run these before assuming you’re done:

# suspicious interpreter processes
ps aux | grep -E "(python|node).*-(c|e)" | grep -v grep

# amended commits that don't match what you remember committing
git reflog

# launch agents and daemons the malware may have dropped (macOS)
ls -la ~/Library/LaunchAgents/
ls -la /Library/LaunchAgents/
ls -la /Library/LaunchDaemons/

# shell startup files for injected commands
grep -E "(curl|wget|eval|base64)" ~/.zshrc ~/.bashrc ~/.profile 2>/dev/null

# scheduled jobs
crontab -l

7. Audit every repo the account can reach, and read the force-push trail, not just the commit log

Every repository an infected account has write access to can get force-pushed in a single pass, same commit author, same dates, nothing visibly edited. The only local tell is an “updated x hours ago” marker on GitHub and branches coming back force-pushed on the next pull, because PolinRider rewrites git history and back-dates commit metadata specifically so a normal git log shows nothing wrong.

There’s one place that spoofing doesn’t reach. Check the pull request activity log for a line reading “[username] force pushed the branch from [commit] to [commit].” That’s a GitHub-generated audit entry tied to the real pushing identity, not to whatever author name got written into the commit. If you’re trying to figure out which credential is still live, that line is more trustworthy than anything in the commit itself.

8. Skip the wasted effort, but don’t skip the antivirus scan entirely

You don’t need to factory reset a machine over this specifically, the payload doesn’t persist as a file on disk between runs, and you don’t need to reinstall Node, since Node itself isn’t what’s compromised. Running more antivirus scans as your main strategy is also a bad use of time, since the payload is decrypted from a blockchain transaction into memory and never touches disk as an executable in most variants.

The exception: at least one variant has been flagged by Windows Defender as Trojan:JS/PolinRider.DB!MTB. So “antivirus won’t see it” isn’t a rule you can rely on across every variant. Run a scan anyway. Just don’t treat a clean result as proof you’re clear.

9. Block known indicators at the network layer while you work

Malicious C2 IPs worth an interim block at the firewall level while you work through the steps above: 166.88.54.158, 198.105.127.210, 23.27.202.27, 154.91.0.103, 136.0.9.8, 166.88.4.2, 23.27.120.142, 202.155.8.173, 166.88.134.82, 188.43.33.249, 23.27.13.43. If you publish to npm and want to check for a specific, newer variant, also watch for outbound traffic to a Telegram bot API, one documented sample exfiltrates over both HTTP and a Telegram channel in addition to the usual blockchain C2. Treat this list as a snapshot, not a permanent one; indicators keep rotating, for the reasons covered earlier. We maintain a current set in the PolinRider GitHub repository.

The takeaway

Nothing on this list works in isolation. Kill the process without rotating the CI/CD token, and the next build re-infects the file you just cleaned. Rotate everything except a password manager vault that was harvested at the same time, and the credentials sitting behind that one lock are still exactly as exposed as they were on day one. Clean the local repo without checking what’s actually on GitHub, and the version everyone else pulls is still poisoned. Run the scanner and stop there, and you’ll miss anything from after March. It took 17 people across five months, comparing partial pictures from different repos and different machines, to add up to a complete list. The only way any of them actually got over it was by doing all of it, not the parts that were obvious first.

Learn more