PolinRider is still spreading, and the worm hides its own commits
We found 303 PolinRider-infected GitHub repos missing from the public lists, 101 active in October 2026, and a new variant of how the worm hides commits.
Nov 1, 2026 · 6 min read
PolinRider is a malware campaign that plants obfuscated JavaScript in the config files of GitHub repositories, steals credentials from developers who run those projects, and then uses the stolen access to push itself into more repositories. OpenSourceMalware's write-up documents it in detail and publishes lists of affected repositories, last updated on GitHub on 10 July 2026.
We wanted to know whether it had stopped. It hasn't. On 1 November 2026 we searched GitHub for the campaign's markers and found 303 infected repositories that aren't in those published lists. 101 of them were pushed to in October 2026, and when we fetched the infected file from each one's default branch, all 101 still contained the malware.
What we found
| Repositories | |
|---|---|
| Matched a PolinRider marker in GitHub code search | 1,044 |
| Already in the published lists | 642 |
| Not in the lists | 402 |
| Not in the lists, with the malware in a project file | 303 (139 owners) |
| Of those, pushed to in October 2026 | 101 (22 owners) |
| Of the 101, still infected on 1 November | 101 |
Most of the 99 we set aside were the opposite of infections: security tools, blocklists, detection rules and CI checks that contain the same markers as detection strings. Several projects now run a "PolinRider guard" workflow in their pipelines. We also dropped the few matches where we couldn't confirm the payload.
These numbers are a floor, not a total. GitHub code search caps how many results it returns, skips forks unless you ask for them, and doesn't index everything. OpenSourceMalware also tracks more reports on its own site than the lists on GitHub show.
Who gets hit
Almost entirely individual developers. Of the 303 repositories, 284 belong to personal accounts, 43 have a single star or more, and the most-starred has 29. Most were created in 2025 or 2026: portfolios, coursework, freelance client sites, backend APIs, starter templates.
That makes them easy to dismiss, but the damage isn't the repository. It's the machine behind it, and everything that machine can reach. Among the active cases were a web agency's client websites, a set of Symfony demo sites, and a company's NestJS boilerplate, the kind of repository other projects are started from.
One developer has 69 infected repositories, all pushed on the same day in late October. Another has 21. A count like that usually means the worm is still running on the developer's machine, rewriting each repository they push to.
We're writing privately to the owners we can reach. We're not naming anyone here: they're the victims.
How the payload hides
Three tricks keep it out of sight.
It hides off-screen. The malicious code is appended to the last line of a real config file, after hundreds of tab characters. Open postcss.config.js and you see a normal ten-line config. In the files we checked, the payload started after 300 to 500 tabs or spaces, far past the right edge of any editor window:
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}; ⟶ global['!']='…'; (obfuscated loader)
It lives in files that run. Most often that's a build tool's config, which is executed rather than read: postcss.config.mjs (94 repositories), eslint.config.mjs (15), tailwind.config.js (9), next.config.mjs and others. Running npm run dev, building, or opening the project in an editor with a linter can be enough. In 62 repositories it was appended to application code instead: Express route handlers, Mongoose models, database helpers, even a Symfony Stimulus controller. There it runs every time the app starts.
Some variants add a launcher. In several repositories an extra api.js holds the payload, and package.json runs it before the real command:
"dev": "node api.js && vite",
"build": "node api.js && vite build"
Once running, it fetches a second stage and goes after GitHub tokens, SSH keys, browser credentials and crypto wallets.
How the worm hides its commits
This is the part that makes PolinRider hard to notice. When it pushes itself into one of your repositories, nothing new shows up in the history.
At least 116 of the 303 repositories still contain the worm's propagation script, temp_auto_push.bat. It doesn't make a new commit. It reads the last commit's message, author name, author email and date, adds the infected files to that commit with git commit --amend, and force-pushes with --no-verify to skip any hooks.
The result looks exactly like the commit you made yesterday: same message, same author, same date. The commit hash changes, but almost nobody compares hashes.
The trick is keeping the date. We found two versions of the script doing it two different ways:
- The documented version (84 of the 116) changes the Windows system clock to the date of the last commit, amends, then sets the clock back.
- A newer version (32 of the 116), which isn't in the public write-up, leaves the clock alone and sets git's own
GIT_AUTHOR_DATEandGIT_COMMITTER_DATEenvironment variables instead. A comment left in the script explains why: the Windowsdateandtimecommands "prompt and require locale formats … which breaks bots". Someone is maintaining this, and fixing it so it fails less often.
Two side effects matter for anyone investigating:
- Don't read the commit date as the infection date. The worm keeps the original. In our sample, "last modified" dates on infected files go back to 2024.
- The person shown as the author probably didn't write it. The commit carries their name because the worm copied it from their previous commit.
How to check your repositories
From the root of a project (or a folder containing all of them):
# Known PolinRider markers
grep -rlE --exclude-dir=node_modules \
'global\[.(!|_V|\\u0021).\]|_\$_1e42|rmcej%otb%|Cot%3t=shtP' .
# Code hidden after a long run of whitespace, PolinRider's hiding place
grep -rlE --exclude-dir=node_modules --include='*.js' --include='*.mjs' --include='*.cjs' \
'[[:space:]]{200,}[^[:space:]]' .
# The worm's own files
git ls-files | grep -E 'temp_(auto|interactive)_push\.bat|\.(lfk|llf)$'
Also look at the scripts in package.json for anything that runs node api.js or a file under public/fonts/ before the real command, and at .vscode/tasks.json for tasks that run when a folder is opened.
On GitHub, a repository's Activity view lists force pushes. A force push to your default branch that you don't remember is worth a closer look.
If you find it
- Treat the machine as compromised. From a different, clean device, rotate GitHub tokens and SSH keys, cloud and hosting credentials,
.envsecrets and wallet keys, and sign out other GitHub sessions. Cleaning the repository first and rotating later lets the worm push itself back. - Clean or reinstall the machine before using it for code again.
- Remove the code after the real config in each affected file, delete
api.jslaunchers,temp_auto_push.batandtemp_interactive_push.bat, and restore thepackage.jsonscripts. - Check every repository the same accounts can push to, including organisation repositories. The worm doesn't stop at personal projects.
- Block force pushes to default branches with branch protection or a ruleset. It won't remove an infection, but it stops this worm from quietly rewriting the branch everyone builds from.
How Lockhound helps
Lockhound checks the repositories you connect for committed malware, including every PolinRider indicator above, and runs the same check on each pull request, so an infected config file fails the check before it's merged. Public repositories are free. Scan yours.
Method
On 1 November 2026 we ran GitHub code searches for the obfuscator markers (_$_1e42, rmcej%otb%, Cot%3t=shtP), the propagation script's filename, and the campaign's published command-and-control hostnames. We removed repositories already in OpenSourceMalware's published lists (polinrider-compromised-repos-july10.csv and polinrider-master-2026-04-11.csv). We kept a repository when a marker appeared in a build or app config file, an entry file such as index.js, or the propagation script, or when the loader itself (global['!']= followed by the obfuscated decoder) was present on the default branch of any other JavaScript file. Matches in security tooling, blocklists, detection rules, CI workflows, tests and documentation were removed. For the 101 repositories pushed to in October, we fetched the flagged files from the default branch to confirm the malware was still present, and checked several by hand.
Read next
- Malicious packages, how they work and how to catch themMalicious packages don't have bugs, they are the attack. How they get in, why install scripts matter, and how to check your lockfile for known bad versions.
- Know when a malicious package is installed anywhere you workFlags known malicious package versions from OpenSSF across npm, PyPI, Composer, RubyGems, Go, Cargo and Maven, plus malware committed into your repositories.