Skip to content
PublishedSoftware Supply Chain Security · v1.0 · 21 min

PolinRider: A Supply-Chain Loader in 40,136 GitHub Repositories

Pinned-commit proof, hidden loaders, propagation and the blockchain-based payload chain.

Cristian Chiriac

Published:
2 November 2026
Version:
1.0
Last observed:
30 October 2026
Dataset frozen:
1 November 2026
Collection cut-off:
31 October 2026, 17:59 UTC
Tracked items rechecked:
1 November 2026, 18:23 UTC
Time zone
All dates UTC.
Contents

PolinRider loaders are present in 40,136 public repositories at pinned default-branch commits.

The research in 30 seconds

What I found
PolinRider loaders are present in 40,136 public repositories at pinned default-branch commits.
What this adds
The owner-wide expansion documents scale beyond the largest public count found in the reviewed sources, together with rewritten-history and pull-request evidence.
Verified scope
40,136 repositories under 3,334 accounts on 30–31 October; default branches form the counting boundary.
What remains unknown
Code presence does not establish execution. Shared owners or infrastructure do not independently establish an operator.

The numbers, with their boundaries

Current count

40,136

Proven repositories

30–31 Oct 2026 · UTC

Counts:
Unique repository ids passing the fixed code and task-target rules; 3,334 owners.
De-duplicated by:
Unique GitHub repository id
Excludes:
Repositories whose task points at an empty file (806 on 30 October 2026)

Earlier snapshot or subset

7,332

Active first-pass repositories

29 Oct 2026 · UTC

Counts:
Earlier code-search set before owner-wide expansion; not a like-for-like growth measure.

Context · separate measure

806

Empty-target tasks excluded

30 Oct 2026 · UTC

Counts:
Repositories whose task target was empty; these cannot execute a target payload and are excluded.
Technical detail:Long tables, code and the evidence register are folded; nothing is left out.

PolinRider is already public. What was missing was its size. The largest public count I found was 1,951 repositories. On 30–31 October 2026, 40,136 public repositories under 3,334 accounts carried the malicious code on their default branch. For each one I recorded the commit, the file and its git object id.

  • The count started from 8,219 code-search candidates and grew as I scanned every public repository of each confirmed owner. That round was repeated until it found no new owners.
  • Every repository was then proven again at a pinned commit. A random sample of 555 was re-read through a different API, and all 555 matched.
  • In 3,281 of the accounts at least one repository was changed in place. In the other 53, the only infected repositories are forks carrying their parent's identical file.
  • The code hides in two places. One is a file named like a font that is really JavaScript, started by a hidden VS Code task. The other is a JavaScript payload pushed off-screen by whitespace in a build configuration file.
  • Most infections were folded into rewritten, back-dated history and force-pushed in September and October 2026. In 93 of 151 cases I could check, the account that pushed was not the credited author.
  • 13 open pull requests carry the payload into other projects.
  • The loader fetches its next stage from TRON, Aptos and BNB Smart Chain (BSC). I decrypted 179 of 182 payloads stored on BSC. Most of the addresses involved are already public, some under other campaign names. Two TRON accounts and two command-and-control addresses were not in any public source I checked.

I did not execute anything, contact command-and-control servers, or open victims' data. Public reporting (OpenSourceMalware, Socket) links PolinRider to North Korean operators [1, 2]; I did not make that attribution myself.

Where this stands (1 November 2026)

#

The proven set was closed on 1 November 2026 with what had been found up to 31 October 2026, 17:59 UTC; repositories found after that are tracked separately and are not in these figures. The full proof pass at pinned commits ran on 30–31 October. On 1 November 2026, 18:23 UTC a read-only GitHub API recheck covered the 162 repositories, 73 accounts and 12 issues that are tracked one by one (operator repositories, Shai-Hulud accounts, phishing issues), not the whole set.

  • Frozen: 40,136 repositories under 3,334 accounts; 3,281 accounts had a repository changed in place.
  • Removal, estimated: on 31 October a random sample of 600 proven repositories found 5 cleaned and 2 deleted (about 1%). The full set has not been re-read since 31 October.
  • Not counted: 22,870 infected non-default branches; seven fork-collector accounts.

Key findings

#

Scope and method

#
Expand technical detail · complete record
Full data table · 10 rows
Data source What was collected Scope Date (UTC) Coverage limit
GitHub code search 12 queries for task, fake-font and config-payload patterns, sliced by file size 32,237 hits in 8,219 repositories 28 Oct 2026 Default branches only; no forks; identical files de-duplicated
Owner-wide expansion Every public repository of each confirmed owner, repeated for newly found owners until a round found none; forks checked against their parent 3,457 owners in the final record 29 Oct 2026 Seven accounts holding 7,500–89,800 repositories each, all infected ones forks, were recorded but not scanned
Proof pass Default-branch commit, matched file and task target, each by git object id Every candidate; 39,998 proven (3 added on 31 Oct), then 138 from the 31 Oct extended search 30–31 Oct 2026 Default branches; other branches only state-checked
GitHub GraphQL object reads The exact matched file of every candidate, by git object ID, plus the task's target file at the default branch 8,219 repositories 29 Oct 2026, before 08:00 A file deleted from all history cannot be read
Fork snapshots Presence or absence of infection commits in forks taken after the claimed date 137 repositories with forks 28 Oct 2026 Only repositories that have forks
GitHub public events Push and force-push events with their actors 478 infected repositories 28 Oct 2026 About 90 days or 300 events per repository
Pull-request search PRs by the credited authors of infection commits 5,799 PRs into 790 repositories 28 Oct 2026 Authors found through commits only
TRON, Aptos, BSC, Ethereum explorers and public RPC Transactions and memos of the dead-drop accounts 7 TRON accounts, 1 BSC sender, 1 Ethereum address 28–29 Oct 2026 Funder graph capped at 80 recipients each
Static decoding Python re-implementations of each obfuscation and decryption layer, in an isolated virtual machine 182 BSC payloads, 6 font-variant samples, config variants 29 Oct 2026 Nothing executed; 3 payloads not decrypted
Registry data (RDAP) and public reporting Address-space holders; prior publications All resolved command-and-control addresses 29 Oct 2026 Origin AS not checked per prefix

Decoded payload text never left the virtual machine. Early in this work some of it was printed on my workstation, and Microsoft Defender quarantined it as Trojan:NPM/PolinRider.DB!MTB, which also confirms the family. Only counts, hashes, addresses and dates were brought out. Repository owners are treated as victims and are not named.

How it started

#

I found PolinRider while investigating a different campaign, the Contagious Interview fake job tests described in Contagious Interview. Seventeen repositories carrying that campaign's errorHandler.js loader also had a VS Code task that ran Node.js on a file named fa-brands-regular.woff2. A real web font is binary. These files were text.

A first search on 28 October found 832 such repositories under 314 owners. 734 of them were not in OpenSourceMalware's public lists, whose last update was dated 10 July 2026. A wider search the same day returned 32,237 raw hits in 8,219 repositories under 3,060 owners. On 29 October I verified every one.

Code search misses most of the campaign, because it indexes only some repositories and no forks. So I then listed every public repository of each confirmed owner and checked them all, and repeated that for each owner the round turned up. The fourth round found nothing new.

The proof pass: 39,998 repositories

#

On 30 October every candidate from both passes was proven again from scratch:

  1. read the repository by numeric id;
  2. pin its default-branch commit;
  3. read the matched file at that commit;
  4. apply a fixed rule.

On 31 October three more repositories passed the same rule. A wider read of lure and tool repositories found them, each with the hidden config payload in an ordinary build config (vite.config.js, postcss.config.js, astro.config.mjs). None is a fork and all three owners are new. They are included in the table, the chart and the account counts below.

Later on 31 October an extended search added 138 more under the same rule, for 40,136 repositories under 3,334 accounts. I listed every public repository of the owners of newly proven repositories in all activities, and ran 36 code searches scoped by file name and extension. Listing owners found 136 of them; the scoped searches added 2. 122 carry the VS Code task and 16 the config payload. 18 are forks, none an identical copy of its parent, and 11 owners are new. Two more matched but sit in a test-fixture or documentation path, so the dataset rule leaves them out. The verdict table and the chart below describe the 39,998; the account table includes the 138.

A task file counts only if:

  • it runs on folder open;
  • it starts node on a file that is not a script;
  • that target exists at the same commit, is not empty and looks like JavaScript.

A config file counts only with the campaign's global marker plus a long whitespace run or a line over 1,500 characters.

Full data table · 10 rows
Verdict Repositories Note
Proven 39,998 Under 3,323 owner accounts; 3 of them added on 31 October
…VS Code task + JavaScript disguised as a font 30,005
…hidden config payload 12,355 2,362 repositories carry both
…forks 6,866 719 of them only carry their parent's identical file
Task points at an empty file 806 Cannot run code; not counted
No evidence at the current commit 360 Cleaned or moved; not counted
Task target deleted 131 Not counted
Security datasets and research copies 78 Not counted
Repository deleted 25 Not counted
Needs manual review 3 Not counted
How the PolinRider code is hidden (39,998-repository snapshot)

Snapshot at a date: the 39,998 repositories proven by the morning of 31 October 2026. The 138 added later that day are not split by form here. Each repository counted once; 2,362 carry both forms.

Table view
How the PolinRider code is hidden (39,998-repository snapshot)
ItemValueNote
VS Code task + fake font only27,643
Hidden build-config payload only9,993
Both2,362
Accounts Count Definition
Owners with a non-inherited infected repository 3,281 At least one proven repository that is not an identical copy of a parent. This shows the file was changed in the owner's own repository; it does not show how the account was compromised
Inherited only 53 Only forks that carry the parent's identical infected file
Dataset only 75 Only security datasets; not counted as victims
Nothing proven now 62 Earlier evidence no longer present

The sample check re-read 555 proven repositories, stratified by rule and account type, through the REST contents API at the pinned commit. All 555 matched. An earlier sample had found seven task files whose target was empty, so the rule was tightened before the final count. That removed 806 repositories.

22,870 non-default branches in 6,530 repositories also carried the payload on 29 October. They are not added to the repository count.

First pass: code-search candidates (29 October)

#
Expand technical detail · complete record

The tables in this section describe the first pass only. Its figures are kept because the later sections analyse that set.

Full data table · 11 rows
Verdict Repositories Accounts Rule
Candidates from code search 8,219 3,060 At least one file matching a PolinRider pattern
Confirmed malicious 7,479 2,651 The matched file contains the malicious pattern (91%)
…active: VS Code task + fake font 5,552 1,845 Task runs node on a font-named file that is text
…active: hidden config payload 1,780 936 Marker + long whitespace run or a line over 1,500 characters
…task present, target file deleted 124 84 Malicious, but not runnable as found
…task and target both removed later 23 7 Read from history
Still active on the default branch 7,332 2,606 249 organizations among the owners
Pattern match only 665 423 Not counted
Research or defensive copies 62 49 Not counted
Neutralized (the "font" is a real font) 10 5 Not counted
Deleted or private 3 2 Not counted

How each number was produced:

Number How I counted it
8,219 / 3,060 Unique repositories and owners among 32,237 hits for twelve code-search queries. A floor: code search covers default branches only and excludes forks (none of the 8,219 is a fork).
7,479 The exact file the search matched, fetched by git object ID. A task file counts only if it runs on folder open and starts node on a non-script file. A config file counts only with the campaign's global-variable marker plus a long whitespace run or an over-long line.
7,332 The same file still at the same path on the default branch, read on 29 October before 08:00 UTC.
5,552 For each task, the target "font" was read at the default branch: 3,870 begin like JavaScript, 1,663 with a short glob header, 20 are other text.
62 research A strict path rule: test data, fixtures, documentation, detection rules or scanners. An earlier loose rule matched substrings inside unrelated names and was discarded.
249 organizations Organization owners among the 7,332 active repositories (255 among the 7,479 confirmed).

When the infected repositories were last pushed (platform pushedAt, which is the latest push, not the infection date):

Last push Config-payload repositories (1,780) All 8,219 candidates
Up to November 2025 40 —
December 2025 – August 2026 656 —
September 2026 221 1,314
October 2026 863 5,888

A separate pass on 28 October read 7,569 candidates and found 5,245 still infected then. The 29 October verification superseded it; together the two sets cover 7,739 repositories under 2,712 accounts.

Where the code hides

#

1. A VS Code task and a fake font

#
  • In the tasks I sampled, all used "runOn": "folderOpen". In 156 of 178 the label was a lint- or build-like name, and the task was hidden from the terminal.
  • In 138 of 178 the command checks for Node.js on macOS, Linux and Windows and fails silently; 13 call node directly. The target has a font extension: .woff2, .woff, .ttf, .otf, .eot, and in a few cases .orc.
  • The "font" sits deep in an unrelated folder, under a public/fonts/ path, so it looks like a bundled asset. Every one of 248 resolved targets in an earlier sample was text.

The task needs the user to open the folder in VS Code and trust the workspace. No dependency install is needed.

2. A payload behind whitespace in a build config

#

Obfuscated JavaScript is appended to a configuration file that build tools load automatically: tailwind.config.js, postcss.config.mjs, and later Next.js and Vite configs. It starts after hundreds of spaces. In one infected repository, a PostCSS configuration that shows 12 normal lines is 5.4 KB on disk. The code sets a global marker (_V, later !) so it runs once per process, then starts the loader.

Config variant (by payload size) First server-side push Repositories in the early set
about 5,087 characters 20 March 2025 1
about 3,300 characters 7 June 2025 10
about 4,111 characters 18 June 2025 8
about 5,009 characters 2 August 2025 1
! marker, about 4,782 characters 22 October 2025 2

Statically, the config payload has three layers: a character shuffle, a dictionary decoder and a string table. Decoded, it is the same TRON → BSC dead-drop loader described below, with an Aptos fallback. Two early samples still have one layer left.

How the forms changed over time

#
Technique sequence (server-side dates)
  1. 01Config payload behind whitespaceT1027First confirmed 20 March 2025; still the form of 1,780 active repositories in October 2026
  2. 02Fake fa-brands-regular.woff2 on some repositoriesT1036.008October – November 2025
  3. 03VS Code folder-open task + JavaScript disguised as a fontT1204.002Mass scale in 2026 — 897 pushes of infected repositories in September and 4,137 in October

How it spreads

#

Rewritten history

#
Measure Value
Infection commits examined 620
…pushed in September or October 2026 620
…claiming an author date before July 2026 536
Repositories where a fork snapshot disproves the date 36 (32 proven, 4 strong)
Oldest disproved date 9 February 2020
Other disproved dates 25 Jul 2021, 26 Oct 2023, 9 Mar 2024, 3 Jan 2025, 2 Feb 2025, 27 Aug 2025, 9 Jun 2026
Repositories with 2016–2024 infection commits and no fork to test 62

Commit dates in this campaign are attacker-controlled. Only push events, pull-request force-push events, repository creation and fork snapshots date anything.

Who pushed

#
Full data table · 11 rows
Measure Value
Infected repositories whose events were read 478
…infecting push found 151 (24 in September, 127 in October 2026)
…outside the event window 205
…no events 122
Pusher different from the credited author 93 of 151
Distinct pushing accounts 99
…long-standing developer accounts (created before mid-2025, 50+ contributions) 64
…no longer accessible 22
…new or thin, unclassified 13
Accounts that infected 3 or more repositories 13
Accounts that pushed to more than one owner 6

The shape is one account sweeping several repositories of one organization within minutes. One account touched five repositories of three owners in six minutes. Another touched six repositories of one organization in 84 minutes. Two accounts each infected five repositories within the same minute. In one organization, five pull requests credited to one developer were force-pushed within four minutes by a different account with write access.

The 46 operator-side accounts I could identify had no commits or pull requests in anyone else's repositories.

Pull requests

#
Group PRs Open Merged Closed How the payload arrived
Later branch rewrite 12 10 0 2 Clean when opened; head force-pushed 6–29 Sep 2026
Present from the start 14 3 11 0 Mostly the config form, into small agency and student organizations
Total 26 13 11 2

The 12 rewritten pull requests target, among others, a widely used data-privacy SDK (closed unmerged on 9 October), an AI framework's Dart SDK (closed unmerged on 28 October) and open-source hospital software (still open). A broader pass found 296 repositories with merged pull requests touching the abused paths. 125 of them, in 34 organizations, are infected now; for those the route is not proven.

The payload chain

#
From an opened folder to a remote-access tool
  1. 01Loader runsT1059.007Folder-open task runs the fake font, or the build loads the poisoned config
  2. 02TRON dead dropT1102.001Latest memo of a TRON account = a BSC transaction hash written backwards
  3. 03Aptos fallbackT1102.001The same pointer read from an Aptos account if TRON fails
  4. 04BSC transaction inputT1027.013Encrypted next stage written by one sender to the burn address
  5. 05Decrypt and runShort XOR key; eval in-process and in a detached node -e process
  6. 06Remote-access toolT1071.001Socket.IO control, command execution, secret-file sweep, clipboard; most builds name an Evoxt address

The font form also uses a fourth lookup: it reads the latest transaction from an Ethereum address to learn the current command-and-control IP address, then fetches the next stage from that server on port 443. That address published eight IP addresses through 609 transactions between 25 July and 19 October 2026.

The mechanism, the first TRON accounts, the earliest Aptos fallbacks and the loader keys are already public [1, 2, 4]. I do not reproduce keys here.

Dead-drop accounts and what they point to

#
TRON dead drop Payloads in the BSC corpus Public before this work
TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v 61 Yes [8, 9]
TLmj13VL4p6NQ7jpxz8d9uYY6FUKCYatSe 53 Yes [7, 10]
TCqf6ZkaQD84vYsC2cuu1jRwB6JveTaRrF 27 Yes [4]
TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP 19 Yes [1]
TFMryB9m6d4kBMRjEVyFRbqKSV1cV2NcpH 14 Yes [4]
TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG 7 Yes [1]
TPDBqDKAYfRVvVYDNZVxY8CRfovkwDaSgW 1 (10 Mar 2026) Not found
Total 182

One TRON funder, not found in public sources, paid four of these accounts, including TLmj13 with 53 payloads. That account sent 58 transactions; 57 carry reversed BSC hashes, pointing to 53 distinct payloads from 12 June 2025 to 27 January 2026, none of them among the 128 found through the other accounts.

The BSC corpus

#
Encrypted PolinRider payloads written to BSC, per month

All 182 distinct payload transactions from the one BSC sender, by block timestamp (UTC). Enumerated 28–29 October 2026.

Table view
Encrypted PolinRider payloads written to BSC, per month
ItemValueNote
Jun 2539
Jul 254
Aug 256
Sep 250
Oct 2510
Nov 255
Dec 2515
Jan 2611
Feb 2638
Mar 269
Apr 260
May 2618
Jun 2611
Jul 260
Aug 260
Sep 269
Oct 267
Measure Value
Distinct encrypted payloads enumerated 182 (12 Jun 2025 – 7 Oct 2026)
Sender transactions in total (nonce) about 347; about 165 not enumerated
Decrypted with keys taken from loaders 99
Decrypted with a key recovered by known-plaintext attack 80
Decrypted in total 179 (3 failed)
Family 1: chained loaders 86
Family 2: final remote-access/stealer builds 93 (30–92 KB, June 2025 – March 2026)
Final builds with strings recovered 79 (60,716 of 70,861 strings)

The 86 loaders use the same three-layer obfuscation and point onward to a TRON account; 48 start a detached node -e process and 16 use eval. Their three Aptos fallback accounts are all in public reporting [4, 8, 10]. The 93 final builds are packed with obfuscator.io, and every one is distinct. Their embedded build tags run from 250602 to 260206; the authors wrote these strings, so they are not verified dates.

Capabilities present in the 79 builds whose strings were recovered:

Capability Builds (of 79)
Socket.IO command-and-control channel 75
Command execution through child_process 74
Home-directory and secret-file sweep 73
Clipboard access 34
Telegram channel 24
Python second stage 1

Public reports already describe similar capabilities [2, 4, 5], so none of this is presented as new. It is read from code, not observed in operation.

Command-and-control addresses

#
Address Role Dates Builds Public before this work
136.0.9[.]8 Final payload 17 Jun 2025 – 6 Jan 2026 18 Yes [7, 9, 10]
166.88.4[.]2 Final payload 17 Jun 2025 – 7 Oct 2025 14 Yes [7, 9, 10]
23.27.202[.]27 Final payload 20 Jun 2025 – 19 Dec 2025 10 Yes [7, 8, 9, 10]
198.105.127[.]210 One chained loader 10 Mar 2026 1 Yes [4, 9, 10]
166.88.134[.]62, 23.27.13[.]135, 166.88.73[.]46, 193.247.144[.]38 Ethereum-published 25 Jul – 19 Oct 2026 — Yes [3, 4, 5]
166.88.134[.]75, 91.218.183[.]174 Ethereum-published same — Yes [4]
23.27.13[.]72, 103.179.142[.]155 Ethereum-published same — Not found

Fifty of the 79 builds whose strings I recovered name no IP address.

Infrastructure: one hosting provider

#
Address block Holder per registry Abuse contact
23.27.x, 166.88.x Evoxt (UK), assigned within Ace Data Centers ranges (ARIN) Evoxt, optionally Ace Data Centers
136.0.9[.]0/24, 166.88.4[.]0/24 Evoxt (UK) Evoxt
193.247.144.x, 91.218.183.x Evoxt Enterprise (RIPE) Evoxt
103.179.142.x Evoxt Enterprise US (APNIC) Evoxt
198.105.127.x SYN LTD, assigned to Evoxt (RIPE) SYN LTD

Evoxt (AS149440) is a self-service VPS provider based in Kuala Lumpur. It sells plans from about $2.99, accepts cryptocurrency, and asks for identification only from clients it considers suspicious. Its acceptable-use policy bans malware and botnet control. It is not on Spamhaus's ASN drop list. Public reports also tie it to unrelated state-linked actors [6, 7].

My assessment, with moderate-to-high confidence: an abused budget host, not a "bulletproof" one. Four Ethereum-published addresses were public from 17 September 2026 [3], and SafeDep named Evoxt on 30 September [4]. I found no sign that the provider acted; the actor kept rotating between Evoxt addresses.

ATT&CK mapping

#
Expand technical detail · complete record
MITRE ATT&CK v20 (content v20.0), checked on attack.mitre.org on 29 October 2026. Only behaviour observed in this research is mapped.
TechniqueNameTacticEvidence
T1195.002 Compromise Software Supply ChainInitial AccessMalicious code committed into thousands of legitimate repositories and pull requests
T1078 Valid AccountsInitial AccessInfections pushed by long-standing developer accounts other than the credited authors (inferred compromise)
T1204.002 Malicious FileExecutionFolder-open task requires the user to open and trust the project
T1059.007 JavaScriptExecutionLoader, chained loaders and final builds are JavaScript run by Node.js
T1036.008 Masquerade File TypeStealthJavaScript stored with .woff2, .woff, .ttf, .otf and .eot extensions
T1027 Obfuscated Files or InformationStealthWhitespace-hidden config payload; three-layer shuffle obfuscation; obfuscator.io packing
T1027.013 Encrypted/Encoded FileStealthXOR-encrypted next stages stored in BSC transaction input
T1102.001 Dead Drop ResolverCommand and ControlTRON memos, Aptos accounts and an Ethereum address point to the next stage or the current server
T1071.001 Web ProtocolsCommand and ControlNext stage fetched over HTTP on port 443; Socket.IO control channel in final builds
T1552.001 Credentials In FilesCredential AccessHome-directory and secret-file sweep in 73 of 79 builds (read from code)
T1115 Clipboard DataCollectionClipboard access in 34 of 79 builds (read from code)

Evidence register

#
Expand technical detail · complete record
E-PR-PROOFPinned-commit proof rows

One row per proven repository with repository id, owner id, default-branch commit, path, git object id, size, rule and task-target checks, plus a commit permalink; 555-repository independent sample re-read through REST. No file contents stored.

Source: GitHub GraphQL and REST, read from an isolated virtual machine · Collected 30 October 2026

E-PR-VERIFYFile-by-file verification of 8,219 candidates

Exact matched objects read by git object ID; per-repository verdict with mechanism, owner type and default-branch state. The list of 7,332 active repositories was provided to GitHub.

Source: GitHub code search and GraphQL object reads · Collected 29 October 2026

E-PR-FORKSFork snapshots disproving commit dates

Forks created after a claimed infection date that do not contain the infection commit; 36 repositories, oldest disproved date 9 February 2020.

Source: GitHub repository and fork metadata · Collected 28 October 2026

E-PR-PUSHPush-event actors versus credited authors

Push events for 478 infected repositories; 151 infecting pushes found; actor differs from credited author in 93.

Source: GitHub public events · Collected 28 October 2026

E-PR-PRSInfected pull requests

26 pull requests confirmed to carry the payload, with head-branch force-push events dated 6–29 September 2026 for 12 of them.

Source: GitHub pull-request search and timeline events · Collected 28 October 2026

E-PR-BSCBSC payload corpus and static decryption

182 distinct encrypted transaction inputs from one sender, linked from 7 TRON accounts; 179 decrypted by re-implementation in an isolated VM; families, build tags and recovered strings recorded as metadata only.

Source: Public TRON, Aptos and BSC explorers and RPC · Collected 29 October 2026

E-PR-ETHEthereum dead-drop history

609 transactions from 25 July to 19 October 2026 publishing eight command-and-control IP addresses.

Source: Public Ethereum explorer · Collected 28 October 2026

E-PR-RDAPAddress-space holders

RDAP records for every resolved command-and-control address, plus the provider's public terms and reputation data.

Source: ARIN, RIPE and APNIC RDAP; provider website; Spamhaus · Collected 29 October 2026

Confidence and alternative explanations

#
Inference Confidence Alternative explanation Why it is less likely
Pushing accounts are compromised developers Moderate The operators own those accounts 64 have years of unrelated activity; operator-side accounts show none
Back-dated commits are deliberate High Clock errors on developers' machines Hundreds of dates, all disproved or pushed in the same two months
The final builds are one actor's tooling Moderate Several actors share the code One BSC sender, one funder graph and one hosting provider
Evoxt is abused rather than complicit Moderate–high A deliberately permissive host Ordinary terms and pricing; unrelated actors use it; not on drop lists
North Korean attribution Not assessed here — Taken from public reporting [1, 2]

Timeline

#

Exact platform and on-chain dates:

Research timeline
  1. Earliest server-confirmed infected repository

    Config payload in a Tailwind configuration; repository created and pushed the same day

  2. First encrypted payload written to BSC

  3. First final builds naming 136.0.9[.]8 and 166.88.4[.]2

  4. First push of the "!" marker config variant

  5. Last transaction of the TLmj13 TRON dead drop

  6. OpenSourceMalware publishes its PolinRider dossier

  7. Last final-build transaction found

  8. Socket's 108-package report covered by The Hacker News

  9. Ethereum address begins publishing command-and-control addresses

    609 transactions, eight addresses, until 19 October 2026

  10. First pull-request branch force-pushed with the payload

    The wave runs until 29 September 2026

  11. Socket publishes the Ethereum dead drop and four addresses

  12. SafeDep names Evoxt and six of the eight Ethereum-published addresses

  13. Last BSC payload transaction found

  14. 8,219 candidates verified, 7,332 of them active infections

  15. Proof pass completes at 39,995 repositories under 3,320 accounts

  16. Three more config-payload repositories added, 39,998 under 3,323 accounts

  17. Extended search adds 138 more, 40,136 under 3,334 accounts

Periods that cannot be pinned to a day: config-payload variants appear from June to October 2025; fake fa-brands-regular.woff2 files appear in October–November 2025; infected repositories received 897 pushes in September 2026 and 4,137 in October 2026.

Data notes and reconciliation

#
Expand technical detail · complete record
Full data table · 11 rows
Item Values seen Explanation
Infected at HEAD before full verification 5,245 and 5,249 Two snapshots of the 28 October file-read set; superseded
7,332 vs 39,998 First pass vs proof pass 7,332 is the code-search set on 29 October; 39,998 adds the owner-wide expansion and forks
41,369 vs 39,995 29 Oct full run vs 30 Oct proof pass The proof pass removed 806 empty-target tasks and counts repositories by id, so renamed repositories are not counted twice
39,995 vs 39,998 30 Oct vs 31 Oct Three config-payload repositories found by a wider read of lure and tool repositories
39,998 vs 40,136 31 Oct, before and after the extended search 138 repositories, found mostly by listing every repository of newly proven owners
Organizations 249 vs 255 249 among active repositories, 255 among all confirmed
Capability counts 75/74/73/34/24 vs 63/62/61/22/17 Two counting passes; the higher set is the stored per-build metadata and is used here
Final command-and-control ports Not in metadata Left out of this article
166.88.134[.]75, 91.218.183[.]174 Earlier marked unpublished Both appear in SafeDep [4]; corrected here
23.27.13[.]72, 103.179.142[.]155 Not found Rechecked on 2 November 2026 in sources 1–5, 7 and 9–11 (live pages, the text files of the two cited GitHub repositories, and the Internet Archive captures of SafeDep from 27 October and Socket from 24 September): neither appears. Socket [3] and Deep Woods [5] list four Ethereum-published addresses; SafeDep [4] lists those four and the two in the row above. Sources 6 and 8 were not rechecked
PolinRider total including pull-request and first-round sets 7,809 Derivation not fully documented; not used

Defensive checks

#
  1. Open unfamiliar projects in restricted mode. VS Code's workspace trust stops folder-open tasks. Before trusting a folder, read .vscode/tasks.json for "runOn": "folderOpen".
  2. Check that fonts are fonts. A .woff2, .woff, .ttf, .otf or .eot file that opens as text is a strong signal.
  3. Look past the right edge of build configs. Search tailwind, postcss, next and vite configurations for long whitespace runs, over-long lines and global['_V'] or global['!'].
  4. Do not trust commit dates. Use push events, pull-request force-push events and fork snapshots.
  5. Re-review pull requests whose head branch was force-pushed after review.
  6. Treat an infected repository as an infected machine or token of the account that pushed, not only of the credited author.
  7. Scan all branches. Code search sees only default branches.
  8. Network: developer machines and CI rarely need to query TRON, Aptos, BSC or Ethereum RPC from node. Alert on that, and on the addresses below.
What you can check in your environment
  1. 01

    Search every branch for .vscode/tasks.json tasks with runOn: folderOpen that start node on a font-named file.

    Data needed: A clone of each repository with all branches; code search sees only default branches.

    What a result does not show: A match shows the loader is present, not that it ran.

  2. 02

    Open .woff2, .woff, .ttf, .otf and .eot files as text; a font file that reads as text is a strong signal.

    Data needed: Repository files.

    What a result does not show: Covers the font-carrier form only; the second form hides in build configuration files.

  3. 03

    Search tailwind, postcss, next and vite configurations for long whitespace runs, over-long lines and global['_V'] or global['!'].

    Data needed: Repository files, all branches.

    What a result does not show: Matches the forms recorded on this page; the forms changed over time.

  4. 04

    Compare commit dates with push events, pull-request force-push events and fork snapshots.

    Data needed: Repository events from GitHub, and a fork or backup to compare against.

    What a result does not show: GitHub's event window is limited; for that reason the pusher analysis here covers 151 of 478 repositories.

  5. 05

    Alert when node on developer machines or CI queries TRON, Aptos, BSC or Ethereum RPC, and on the addresses under Indicators.

    Data needed: Network or DNS logs from developer machines and CI runners, attributed to the process.

    What a result does not show: Such queries are rare from node on these machines, not proof on their own; ports of the final command-and-control servers are not confirmed.

Detection rules

#

Written from the inclusion rules above, for source files rather than binaries.

VS Code task that runs node on a disguised file when the folder opens· yaraDownload

Matches .vscode/tasks.json files with the PolinRider trigger: a folderOpen task whose command runs node on a file with a font, image or made-up extension (.woff2, .ttf, .orc, .lkf and others). Proof additionally requires the target to be a non-empty JavaScript file.

Status
experimental
Family
PolinRider
Platforms
none recorded
Engines tested
YARA 4.5.0
Known benign cases
none recorded
Testing
Rewritten on 2 November: the first version matched none of 199 proven tasks, because it expected node and the target as separate JSON strings and knew only font extensions. The proven tasks run node inside one shell command string, and many target made-up extensions such as .orc. This version looks for node followed by a path with a font, image or made-up extension in any folderOpen task. It matched all 199 files it was written against, all 300 held-out files, and none of 88 benign tasks.json files. The proof pass chose the proven files with a similar check (a folderOpen task running node on a non-JavaScript file), so these runs show that the rule finds the proven set; they do not show that it finds variants without those markers.

Check results

DateEngineCorpusResult
2026-11-02YARA 4.5.0Proven PolinRider .vscode/tasks.json files the rule was written against (199)199 of 199 matched
2026-11-02YARA 4.5.0Proven tasks.json files not used to write the rule, random sample (300)300 of 300 matched
2026-11-02YARA 4.5.0The other proven sets of this investigation (1,194 files)0 matches
2026-11-02YARA 4.5.0Benign source code: shallow clones of next.js, microsoft/vscode and vscode-extension-samples (56,864 files, 88 of them tasks.json)0 hits
2026-11-02YARA 4.5.0Benign files in the analysis VM: npm tools (19,474), Python standard library and packages (29,360), git sample hooks (14)0 hits
rule CMPC_PolinRider_VSCode_Task_Autorun
{
    meta:
        author = "Cristian Chiriac / CMPC Research"
        description = "PolinRider trigger: VS Code folderOpen task that runs node on a file with a font, image or made-up extension"
        status = "experimental"
        date = "2026-11-02"
        scope = "Run on .vscode/tasks.json; confirm the target file is JavaScript before acting"
        targets = "woff2 woff ttf otf eot orc lkf lfk llf glb fml svg png jpeg sol: every extension in the proven tasks"
    strings:
        $open = "folderOpen" ascii
        $node = /\bnode(\.exe)?\s+(\\?["'])?(\.{0,2}[\/\\]{1,2})?[\w.\-\/\\\[\]()@]+\.(woff2?|ttf|otf|eot|orc|lkf|lfk|llf|glb|fml|sol|svg|png|jpe?g|gif|ico|bmp|webp)\b/ ascii
    condition:
        filesize < 200KB and $open and $node
}
Obfuscated loader appended to a build configuration file· yaraDownload

Matches files that carry the PolinRider loader on one long line: the global['_V'] or global['!'] marker, or an _$_xxxx obfuscator name, followed by at least 300 more characters on the same line. In build configurations the line is usually pushed off-screen by spaces or tabs; the rule does not depend on that padding.

Status
experimental
Family
PolinRider
Platforms
none recorded
Engines tested
YARA 4.5.0
Known benign cases
none recorded
Testing
Rewritten on 2 November: the first version found 8 of 200, because it required files over 20 KB and a 400-character run of spaces, and it hit 13 benign files. This version drops both conditions and requires the marker to be followed by at least 300 characters on the same line. It matched all 200 files it was written against, all 297 held-out files, and no benign file, 1,685 of them configuration files. The proof pass chose the proven files with the same markers plus a long line or a long run of spaces, so these runs show that the rule finds the proven set; they do not show that it finds variants without those markers.

Check results

DateEngineCorpusResult
2026-11-02YARA 4.5.0Proven PolinRider files carrying the appended loader, mostly build configurations (200; the files the rule was written against)200 of 200 matched
2026-11-02YARA 4.5.0Proven files of the same set not used to write the rule, random sample (297; 3 no longer available)297 of 297 matched
2026-11-02YARA 4.5.0The other proven sets of this investigation (1,196 files)0 matches
2026-11-02YARA 4.5.0Benign source code: shallow clones of next.js, microsoft/vscode and vscode-extension-samples (56,864 files, 1,673 of them *.config.* files)0 hits
2026-11-02YARA 4.5.0Benign files in the analysis VM: npm tools (19,474), Python standard library and packages (29,360), git sample hooks (14)0 hits
rule CMPC_PolinRider_BuildConfig_Appended_Loader
{
    meta:
        author = "Cristian Chiriac / CMPC Research"
        description = "PolinRider loader appended to a file: global['_V'] / global['!'] or an _$_xxxx name, then the rest of the loader on the same line"
        status = "experimental"
        date = "2026-11-02"
        scope = "Run on *.config.(js|ts|mjs|cjs), postcss/tailwind/vite/next/eslint configurations and font-named files that read as text"
    strings:
        $g = /global\[\s*['"](_V|!)['"]\s*\][^\n\r]{300}/ ascii
        $s = /_\$_[0-9a-f]{4}[^\n\r]{300}/ ascii
    condition:
        filesize < 5MB and any of them
}

If you find it in your repository

#
  • Remove the task, the fake font and any config payload; check every branch and open pull request.
  • Assume the machine of the account that pushed the change is compromised. Rebuild it, then rotate GitHub, npm and cloud tokens, SSH keys and passwords saved in browsers, from a clean device.
  • Rewritten history means the bad commit may look old. Compare against a fork or a backup.

Indicators

#
Expand technical detail · complete record
Command-and-control addresses (defanged; dates are on-chain configuration dates)
0 selected · 11 of 11 shown
TypeValueContextCopy
ip136.0.9[.]8Final builds, Jun 2025 – Jan 2026; public [7, 9, 10]
Provenance: Previously documented
ip166.88.4[.]2Final builds, Jun – Oct 2025; public [7, 9, 10]
Provenance: Previously documented
ip23.27.202[.]27Final builds, Jun – Dec 2025; public [7, 8, 9, 10]
Provenance: Previously documented
ip23.27.13[.]72Ethereum-published, Jul – Oct 2026; not found in public sources checked
Confidence: Confirmed malicious
ip103.179.142[.]155Ethereum-published, Jul – Oct 2026; not found in public sources checked
Confidence: Confirmed malicious
ip166.88.134[.]62Ethereum-published; public
Provenance: Previously documented
ip23.27.13[.]135Ethereum-published; public
Provenance: Previously documented
ip166.88.73[.]46Ethereum-published; public
Provenance: Previously documented
ip193.247.144[.]38Ethereum-published; public
Provenance: Previously documented
ip166.88.134[.]75Ethereum-published; public
Provenance: Previously documented
ip91.218.183[.]174Ethereum-published; public
Provenance: Previously documented
Blockchain dead drops and sender
0 selected · 8 of 8 shown
TypeValueContextCopy
other0x9bc1355344b54dedf3e44296916ed15653844509BSC sender of all 182 encrypted payloads; public [10, 11]
Provenance: Previously documented
otherTLmj13VL4p6NQ7jpxz8d9uYY6FUKCYatSeTRON dead drop, 53 payloads; public [10]
Provenance: Previously documented
otherTPDBqDKAYfRVvVYDNZVxY8CRfovkwDaSgWTRON dead drop, 1 payload; not found in public sources checked
Confidence: Confirmed malicious
otherTGJ1YJJNkFYLZKU4ZGqJfS6dU2cvGac7pSTRON funder of four dead-drop accounts; not found in public sources checked
Confidence: High-confidence suspicious
otherTQdwohPCWqqfCUaCispyV1NaUZ1HgiJPUyTRON funder of the single-use dead drop; public [10]
Provenance: Previously documented
other0x3414a658f13b652f24301e986f9e0079ef506992472c1d5224180340d8105837Aptos fallback in chained loaders; public [10]
Provenance: Previously documented
other0x533b2dbcaeff19cd1f799234a27b578d713d8fcaa341b7501e4526106483e0b1Aptos fallback in chained loaders; public [8, 9]
Provenance: Previously documented
other0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1aEthereum dead drop for command-and-control addresses; public
Provenance: Previously documented

Host markers: a .vscode/tasks.json that runs node on a font file on folder open; font files that are text; build configs with long whitespace runs followed by global['_V'] or global['!']. Antivirus name seen: Trojan:NPM/PolinRider.DB!MTB (Microsoft Defender).

What was already public

#
Expand technical detail · complete record

Sources were read on or before 30 October 2026. Each one also links to its Internet Archive record, which lists the captures that exist, in case the original moves or changes.

  1. OpenSourceMalware, "PolinRider: DPRK Threat Actor Implants Malware in Hundreds of GitHub Repos" (7 March 2026, updated 11 April 2026: 1,951 repositories / 1,047 owners): mechanisms, history rewrite, TRON and Aptos dead drops, loader keys. https://github.com/OpenSourceMalware/PolinRider (Internet Archive lookup)
  2. The Hacker News, "North Korean Hackers Publish 108 Malicious Packages and Extensions in PolinRider Campaign" (4 July 2026), on Socket's research. https://thehackernews.com/2026/07/north-korean-hackers-publish-108.html (Internet Archive lookup)
  3. Socket, "PolinRider Spreads Through Compromised GitHub Accounts and Packagist" (17 September 2026): the Ethereum dead drop and four of its addresses. https://socket.dev/blog/polinrider-github-packagist (Internet Archive lookup)
  4. SafeDep, "PolinRider Switches to Ethereum C2 in 30+ Repositories" (30 September 2026): eleven server addresses, all on Evoxt (six of them Ethereum-published), stealer capabilities, spread from infected machines. https://safedep.io/polinrider-ethereum-c2-github-repositories/ (Internet Archive lookup)
  5. Deep Woods Security, PolinRider analysis of one open-source project (lab run 10 October 2026). https://github.com/deepwoodssec/polinrider-reactime-analysis (Internet Archive lookup)
  6. Proofpoint on TA416 favoring Evoxt (1 April 2026), https://www.proofpoint.com/us/blog/threat-insight/id-come-running-back-eu-again-ta416-resumes-european-government-espionage (Internet Archive lookup) ; and cyberandramen on PlugX staging infrastructure (2 March 2026), https://cyberandramen.net/2026/03/02/before-the-proxy-uncovering-active-plugx-staging-infrastructure-linked-to-three-prc-actors/ (Internet Archive lookup)
  7. Ransom-ISAC, cross-chain TxDataHiding crypto heist, part 3 (13 November 2025). https://ransom-isac.org/blog/cross-chain-txdatahiding-crypto-heist-part-3/ (Internet Archive lookup)
  8. JFrog Security Research, "Hijacked npm Packages Use Novel VSCode Autorun and Blockchain Dead Drops to Deploy a Credential/Crypto Stealer" (24 June 2026), source in the github.com/jfrog/research repository.
  9. Trend Micro, Void Dokkaebi fake job interview lure, with indicator list (21 April 2026). https://www.trendmicro.com/en_us/research/26/d/void-dokkaebi-uses-fake-job-interview-lure-to-spread-malware-via-code-repositories.html (Internet Archive lookup)
  10. Ransom-ISAC, cross-chain TxDataHiding crypto heist, parts 2 and 4 (27 October and 8 December 2025). https://ransom-isac.org/blog/cross-chain-txdatahiding-crypto-heist-part-2/ (Internet Archive lookup) and https://ransom-isac.org/blog/cross-chain-txdatahiding-crypto-heist-part-4/ (Internet Archive lookup)
  11. Google Threat Intelligence Group, "DPRK Adopts EtherHiding" (16 October 2025). https://cloud.google.com/blog/topics/threat-intelligence/dprk-adopts-etherhiding (Internet Archive lookup)

Several of these indicators are public under other names: Joyfill, Void Dokkaebi, cross-chain TxDataHiding or EtherHiding, rather than PolinRider.

Not found in the public sources I checked, as of 30 October 2026:

  • the scale (40,136 proven repositories under 3,334 accounts; the largest public count was 1,951);
  • the pull-request counts;
  • the pusher-versus-author analysis;
  • fork-based proof of back-dating and the 20 March 2025 date;
  • the BSC payload corpus and its decryption;
  • one TRON dead drop (TPDB…), one TRON funder (TGJ1…) and two Ethereum-published addresses (23.27.13[.]72, 103.179.142[.]155).

The Wiz and Gridinsoft pages were read; they give no counts or indicators.

Limitations

#
  • Static analysis only. Capabilities are read from recovered strings, not observed.
  • The first pass used code search, which sees only default branches and no forks. The owner-wide expansion closed most of that gap, but seven very large fork-collector accounts were not scanned. The counts are a floor.
  • pushedAt is the last push, not the infection date.
  • The pusher analysis covers 151 of 478 repositories; the rest fell outside GitHub's event window.
  • About 165 BSC payload transactions were not enumerated. Three payloads and two early config variants were not fully decoded. The known-plaintext decryption leaves about 0.1% token errors.
  • Ports of the final command-and-control servers are not confirmed.
  • Hosting ownership was not checked prefix by prefix.
  • Attribution to North Korea comes from public reporting, not from this work.

Investigation context

#

This article is part of the GitHub threat investigation. Counts are a snapshot, not evidence that the code ran on a victim's machine. The shared methodology and proof rules explain inclusion, exclusions and the independent re-check.

Claims, evidence and limits

ClaimAssessmentEvidenceLimit
40,136 repositories proven at a pinned commitConfidence: Confirmed malicious
E-PR-PROOF, E-PR-VERIFYNot stated in the finding
Infections arrive through rewritten history, usually pushed by someone other than the credited authorConfidence: Confirmed malicious
E-PR-FORKS, E-PR-PUSHNot stated in the finding
The payload travels into other projects through pull requestsConfidence: Confirmed malicious
E-PR-PRSNot stated in the finding
A 182-payload corpus on BNB Smart Chain, 179 decryptedConfidence: Confirmed malicious
E-PR-BSCNot stated in the finding
Every resolved command-and-control address is on one budget VPS providerConfidence: High-confidence suspicious
E-PR-ETH, E-PR-RDAPNot stated in the finding

Already public, and what this adds

Already publicAdded hereWhere to check
Up to 1,951 repositories under 1,047 owners (OpenSourceMalware, updated 11 April 2026).40,136 repositories under 3,334 accounts, each proven at a pinned default-branch commit on 30–31 October 2026.The proof pass: 39,998 repositories
Mechanisms, history rewriting, TRON and Aptos dead drops and loader keys (OpenSourceMalware).Fork-based proof of back-dating, including the 20 March 2025 date, and a pusher-versus-author analysis.Rewritten history
Spread through compromised GitHub accounts and Packagist (Socket, 17 September 2026).Pull-request counts.Pull requests
The Ethereum dead drop and four of its addresses (Socket, 17 September 2026); eleven server addresses on Evoxt, six of them Ethereum-published (SafeDep, 30 September 2026).The BSC payload corpus and its decryption.The BSC corpus

Terms used on this page

pinned commit
A specific commit, identified by its hash, at which a repository was read. Every counted repository was proven at the default-branch commit recorded in the proof pass, so a later change to the repository does not change what was proven.
git object id
The hash Git assigns to a file's exact contents. Recording it identifies the precise file that matched, independently of its path or of later edits.
default branch
The branch a repository shows by default. Counts are taken on default branches only; infected non-default branches were recorded separately and are not counted.
inherited fork
A fork whose malicious file is byte-identical to its parent's and was not modified in the fork. Such forks are counted and flagged, because removing the parent does not clean them; an account whose only infected repositories are such forks is listed as inherited, not compromised.
commit author vs pusher
The credited author is the name and date written inside a commit; whoever creates the commit sets both, and rewritten history can back-date them. The pusher is the account the platform's push events record as having pushed the change. In PolinRider the two differ in 93 of 151 traceable infecting pushes.
dead drop
A public record the loader reads to learn its next step. In PolinRider, the latest memo of a TRON account (an Aptos account as fallback) points to a BNB Smart Chain transaction whose input holds the encrypted next stage.
fork-collector account
An account holding thousands of forks. Seven such accounts, holding 7,500 to 89,800 repositories each, all of whose infected repositories were forks, were recorded but not scanned; the counts are a floor.
proof pass
The 30–31 October 2026 re-read of every candidate at a pinned commit under fixed inclusion rules, followed by an independent re-read of a random sample of 555 repositories through a different API.
owner-wide expansion
Listing and checking every public repository of each confirmed owner, repeated for each newly found owner until a round found none. It reaches repositories that code search does not index, including forks.
folder-open task
A VS Code task in .vscode/tasks.json with "runOn": "folderOpen", which runs when the folder is opened in a trusted workspace.
workspace trust
VS Code's setting for unfamiliar folders: a folder that is not trusted opens in restricted mode, which stops folder-open tasks.

All terms: glossary.

Corrections & discussion

Found an error, or have evidence that supports or contradicts a finding? Send a correction. Name the section and what you observed.

← All research