Reactime: the font that wasn’t a font
A North Korean implant in a popular open-source developer tool. Runs on folder open, resolves its C2 from the Ethereum blockchain, rewrites VS Code and Discord to persist. Zero detections across every engine.
Supply chain · Malware · On-chain C2
Key findings
- Reactime, an open-source React debugging tool under Open Source Labs, was compromised. 20 of 22 branches carry the implant. Only masteranddevare clean.
- The malicious commit is backdated to 2020 and presented as a one-file change. It rewrites the whole tree: 387 files, over 160,000 lines.
- Opening the repository in VS Code is enough. A .vscode/tasks.jsontask runs onfolderOpenand executes a file disguised as a Font Awesome font. The file is JavaScript.
- The implant does not hardcode a command server. It reads the C2 IP out of an Ethereum transaction’s address bytes (the NullReceiver technique).
- On Windows it injects itself into installed Electron apps, VS Code and Discord among them, and re-resolves its C2 on every launch. This persistence is not in the public reporting on the campaign.
- The implant profiles its victim before it acts. On an Ubuntu VM the C2 answered Blocked (BOT)and stopped. The full execution chain was captured on a Windows decoy built to pass.
- Five samples submitted to VirusTotal: two loader variants, the backdoor, the trojanized Discord core, the modified VS Code entry. Zero detections on all five. Sandboxes recorded no behavior.
- Delivery and C2 match the PolinRider campaign and the NullReceiver technique, both publicly attributed to North Korea. We treat attribution as consistent with public reporting, not as our own determination.
The warning
At 11:31 PM on September 30, 2026, Michael Novati, co-founder of Formation, posted a public warning that Reactime appeared to be compromised and that people should stop opening the repository. He did not have the technical details. He had enough to say stop.
Three questions followed: is it real, how does it work, who is behind it. By October 6 the first answer was solid enough to file the formal advisory, Reactime issue #368: twenty branches contain malware, do not open in VS Code or build.
The target
Reactime is a Chrome extension for time-travel debugging and performance monitoring in React applications. About 2,200 stars on GitHub, 210 forks, listed in the Chrome Web Store.
It lives under Open Source Labs, the nonprofit open-source accelerator attached to Codesmith, the software engineering residency. Codesmith residents build and iterate OSLabs tools as part of the program, and Reactime has passed from cohort to cohort for years; its README credits around ninety authors. That is the profile an attacker wants: a trusted name, a long history, many hands in the repository, and a steady stream of new developers cloning it to work on it.
Confirming the compromise
Nothing was run on a real machine. We pulled a full mirror of the repository into an isolated VM, 80,537 objects, and read history before touching a file.
Everything that follows comes out of that one commit.
The forged commit
Open f04e99a on GitHub and it looks like nothing. The message is Update StateRoute.tsx. The author is a real GitHub account, with that account’s avatar. The date is September 2, 2020. On a branch with years of history, a one-file touch from five years ago is the least interesting object on the page.
Every one of those signals is free text. Git does not verify who wrote a commit or when; the author name, email, and timestamp are fields the committer fills in, and GitHub renders whatever it is given. Put a known contributor’s email in the author field and GitHub draws their face next to it. Set the clock to 2020 and GitHub files it under 2020. There is no signature on the commit and no verification badge, and nobody looks for the absence of one.
The number that gives it away is on the same screen: 387 files changed, 165,612 lines added, 156,243 removed. That is not a one-file update. The add and remove counts are nearly equal because the tree was replaced wholesale, not edited; the implant is a 24-line task file and one “font,” carried inside 300,000 lines of churn that no reviewer will read. GitHub also shows a failing check on the commit, one of three. Nobody acted on it.
Because the commit is dated 2020, the branch it sits on reads as dormant. Nothing in the repository’s recent activity points at it. A developer who pulls staging and runs git log sees a five-year-old commit at the top, which is normal for a branch nobody has touched in a while. Rewritten history that looks old and routine is this campaign’s documented signature; it is how it hid in packages before it hid in a repository.
Twenty branches
It is not one poisoned branch. We checked every branch for the implant’s signature, the presence of .vscode/tasks.json. Of 22 branches, 20 carry it. Feature branches, personal patch branches, staging, website. Only dev and master came back clean.
The loader is byte-identical on all twenty branches: one git blob, referenced from twenty branch tips. One implant, planted twenty times.
That choice is the operator’s read of how people use GitHub. The two branches that stayed clean are the two a maintainer looks at and the two the repository page shows by default. The twenty that were hit are the ones a contributor checks out to pick up a piece of work, and the ones nobody reviews. Infect the quiet branches and the default view stays honest.
The trigger
The implant does not wait to be run. .vscode/tasks.json defines a task labeled eslint-check, a name that belongs in any JavaScript repository, with runOn: folderOpen. Open the folder in VS Code and the task runs.
The task is built to run anywhere and show nothing. The command tries command -v node for macOS and Linux, then where node for Windows, and falls through to echo '' so it never fails loudly. It is marked isBackground and hide; its presentation is reveal: never, echo: false, close: true. The terminal panel never opens. The task runs node against a file nine directories deep, inside a test fixture, in a folder called fonts.
The only gate between a developer and this task is VS Code’s Workspace Trust prompt, which asks whether you trust the authors of the folder. Anyone who cloned Reactime to work on it answers yes. That is what the prompt is for.
The disguise
The file is fa-solid-900.woff2, at tests/automated-tests/hooks-redux-router/Frontend/src/redux/actions/public/fonts/. The filename is Font Awesome’s. The directory is a test fixture nobody opens. In a file tree it is an icon font, and font binaries are the one kind of file no reviewer reads.
It is JavaScript. 37,717 bytes, no line breaks, carrying the campaign’s 9-0476-3 build marker. A second variant of the same loader ships as babel.config.js.
Nobody sees it
We submitted the loader to VirusTotal. 0 of 60 engines.
The reason is in the file’s properties: HTML and ASCII text, very long lines, no line terminators. There is no plaintext malware for a signature to match, and a text file wearing a font extension does not trip heuristics.
Unpacking it
The loader is obfuscated. We beautified it and ran it through webcrack to unwind the string-array encoding, control-flow flattening, and decoder wrappers.
Stage 1 fetches a larger stage 2. Rather than let it execute, we hooked the decoder and the dangerous calls, stubbed anything that writes to disk, spawns a process, or touches the network, and let it resolve its strings. Stage 2 came out as inert text.
The command channel
There is no C2 address in the code. The implant reads it from the Ethereum blockchain.
The configuration has a hardcoded attacker wallet, a nonce to look for, a public blockchain indexer, and a list of legitimate Ethereum RPC providers.
It queries the chain for the attacker’s transactions and selects one by sender and nonce.
Then it takes that transaction’s destination address and reads the C2 IP out of the address bytes. First four bytes, primary. Next four, backup. The command server never appears in code or config, and the lookup looks like ordinary blockchain traffic.
Independent researchers had already logged the same C2 address and the same wallet, attributed to North Korea.
Execution and persistence
With a C2, stage 2 fetches the next payload and runs it two ways: in-process with eval, and as a detached child process with the window hidden and output suppressed. The detached process survives the editor closing.
The decoy
The loader checks its host before it does anything. Our first run, on October 6 in an isolated Ubuntu VM, got as far as the C2 and no further: the server fingerprinted the machine as an analysis box, answered Blocked (BOT), and withheld everything past stage 1. So we built a Windows machine that looks like a developer’s, a default PC name, a plausible user, Chrome, a wallpaper, no VM tells, and detonated the implant there on October 10. It passed the profiler, ran the full operation, and we captured the complete execution chain from the host side.
What the Windows run showed
The full chain, in the order it happened on the decoy:
- VS Code opens the folder. The hidden folderOpentask runsnodeagainst the fake font, the stage-1 loader.
- The loader resolves its C2 from the Ethereum blockchain and connects to the command server over socket.io, upgrading to a WebSocket.
- It rewrites installed Electron applications. VS Code gets a modified main.jsand a dropped module,main.inz.cjs. Discord gets a trojanizeddiscord_desktop_core/index.js. An attempt to do the same to npm failed on permissions.
- It downloads a private Python 3.12.7 runtime served by the C2 and installs it. The victim never contacts python.org.
- The Python stage harvests the machine and exfiltrates the take as encrypted multipart uploads, tagged with the machine’s identity.
Steps 3 and 4 are the parts not in the public reporting. PolinRider was documented as a package-delivered stealer; on this machine it also became a persistent implant that reinfects itself from two applications a developer opens every day.
Inside the injected module
main.inz.cjs is the backdoor. It labels itself by the host application it was injected into. On every launch of VS Code or Discord it independently re-resolves the command server from the blockchain and reconnects, so the implant survives the original clone being deleted and the C2 address changing. Its strings are hidden with a cipher of its own, a stream cipher stepped by the golden ratio, distinct from the seeded character shuffle the loader uses; the two were built to be unwound separately. From its decoded string table we recovered the full list of Ethereum RPC providers it rotates through, including two that had not been logged in public reporting at the time.
We submitted the backdoor, the trojanized Discord core, and the modified VS Code entry.
Five samples. Not one detection on any engine. Signature antivirus is blind to this campaign. Detection has to come from behavior and from rules, which is why we published both.
Timeline
Attribution
Consistent with public reporting. Not an independent determination.
PolinRider is the name Socket gave, in July 2026, to a North Korea-linked supply-chain campaign against open-source developers: by Socket’s count, 162 malicious release artifacts across 108 packages, starting in npm and spreading to Go modules, Packagist, and a Chrome extension. Its documented signature is compromised maintainer access and rewritten history, so the change looks old and routine. Reactime matches that signature. What is different is the delivery: no package was poisoned, and the published npm package stayed clean. The repository itself was the vector, and the editor was the trigger.
The blockchain dead-drop is the NullReceiver technique, already named and documented. The C2 address and attacker wallet recovered from the sample match what other researchers have published.
Observed in the sample and not in the public reporting at the time of writing:
- Electron-application injection for persistence. VS Code and Discord rewritten so the implant re-runs on every launch. Electron injection is a known technique in general; its use by this campaign appears undocumented.
- Per-launch C2 re-resolution inside the injected module, independent of the loader.
- A custom stream cipher for string obfuscation in the injected module, distinct from the loader’s scheme.
- A C2 that serves its own Python runtime to the victim rather than letting the victim fetch one.
Reported as observations, not as a new technique.
Public references:
- Socket, PolinRider: North Korea-Linked Supply Chain Campaign Expands
- OpenSourceMalware, PolinRider dossier
- The Hacker News, Trojanized npm Packages Employ NullReceiver Tactic
- Sonatype, Six npm Packages Use Ethereum Transactions to Retrieve Malicious Payloads
- SOC Prime, NullReceiver Hides C2 in Blank Ethereum Transfers
Are you affected
If you cloned Reactime from any branch other than master or dev since late September 2026 and opened it in VS Code, treat the machine as compromised. The published npm package, reactime 3.1.1, was clean; the exposure is the repository, not the install.
- Rotate every credential that machine could reach.
- Check VS Code and Discord installations for modified entry points. Hashes are in the IOC set.
- Match main.inz.cjsand the loader hashes against your telemetry.
- Audit .vscode/tasks.jsonforrunOn: folderOpenacross cloned repositories. It is a legitimate feature. It should not be pointed at a font.
Method
Isolated lab throughout. Full mirror clone, read before run. A controlled run of stage 1 on an isolated Linux VM with the TLS session keys logged, then a full detonation on a purpose-built Windows decoy once the Linux run was refused. Stage 2 was recovered separately in a neutered harness with the decoder hooked and every disk, process, and network call stubbed. Every artifact is hashed; all five samples were submitted to VirusTotal and are reported here as returned. Attribution is deferred to public reporting, and our own observations are labeled as such.
Why it matters
Cloning a repository is now execution. A folderOpen task means the trust decision happens at git clone, not at npm start. Signature detection returned zero on all five samples; if your controls stop at antivirus, this campaign is invisible to you. A public blockchain is a C2 channel that cannot be taken down and reads as normal traffic; blocking the IP does nothing, the next transaction moves it. And the implant decides whether you are worth infecting before it does anything, so a generic sandbox sees a clean file. The behavior report has to come from a machine the malware believes.
Evidence and disclosure
Indicators, YARA and Sigma rules, the deobfuscated backdoor, and all hashes:
Public advisory: Reactime issue #368, filed by Deep Woods Security on October 6, 2026.
Reported to GitHub, CISA, and the FBI.
Michael Novati, co-founder of Formation, raised the first public alarm. This analysis exists because he spoke up before anyone had the details.