ACTIVE TECHNIQUE ยท SEEN IN PRIVATE AND PUBLIC REPOS

Malware that runs the moment you open the project.

A dropper family is being committed directly into developer repositories โ€” buried inside ordinary config files and disguised as font assets. It executes when you start the dev server, or the instant your editor opens the folder. No npm install required. One sample sat in a repository for five months before anyone noticed, and developers have since reported it across student cohorts, agencies and production teams โ€” their accounts are here.

snare is a free, open-source command-line tool that watches your machine for the loader and kills it, then scans every Git repository you can reach, cleans out what it finds, and helps you warn your collaborators.

What it doesThe two ways it gets executed, and why an IP blocklist is useless against it.
How to checkFour commands you can run right now on any clone. No tools needed.
How to stay cleanInstall snare on macOS, Linux, Windows or WSL in about a minute.
The threat

Two routes in, neither of which you'd think to check

The same payload ships two ways. Both fire from a completely ordinary developer action โ€” which is exactly why this family works.

Opening the folder in your editor

.vscode/tasks.json

A task labelled eslint-check, marked "hide": true and "reveal": "never" so it never appears in the terminal panel. It runs node ./public/fonts/fa-solid-400.woff2.

โ–ธ fires on "runOn": "folderOpen"

Starting the dev server

postcss.config.js

The payload is appended to the last line after thousands of spaces. The file reads as four correct lines of config, because everything else is far off the right edge of the viewport.

โ–ธ fires on next dev / next build

The font is not a font

A genuine .woff2 file begins with the magic bytes wOF2. This one begins with 507 spaces, then JavaScript. Disguising a payload as a binary asset keeps it out of code review entirely โ€” nobody opens a font in a diff.

Reports from the field

This is not a one-off

The accounts below are what developers have described first-hand โ€” an internship cohort of several thousand, engineers nearly dismissed over a compromise that was not their doing, and disrupted operations at several companies. These are reports, not audited figures. They are collected in the open precisely so you can read them and judge for yourself.

If it reached your team, say so

You do not have to name an employer or client โ€” "a fintech I contract for" is a perfectly good data point. Redact tokens, keys and internal hostnames first. The reason to post is simple: the developers who lost work to this mostly had never heard of the technique, and the ones who caught it early had heard about it from someone else.

Why nobody spotted it

The file looks completely normal

Here is the actual shape of an infected postcss.config.js. Four ordinary lines, then a gap wide enough to push the payload past any screen. Reveal the whitespace to see what your editor was not showing you.

postcss.config.js โ€” as committed

Line 5 is 9,135 characters long. Scroll it sideways โ€” or reveal the whitespace.

How it works

The address it phones home to is not in the code

This is why blocking an IP achieves nothing. The loader ships with no server address at all โ€” it derives a fresh one every run by reading the public Ethereum blockchain, a documented technique known as EtherHiding. The operator moves their infrastructure by sending a single transaction.

Ethereum ยท Blockscout API latest transaction from 0xa322โ€ฆ Attacker sends one tx to move the C2 โ‘  ask for the latest tx โ‘ก decode bytes 0โ€“3 โ†’ IPv4 YOUR MACHINE postcss.config.js or .vscode/tasks.json node loader detached, reparented C2 server address known only at runtime clipboard stealer polls every 200 ms runs โ‘ข GET โ‘ฃ eval'd in memory
The loader carries no server address. It reads one off the blockchain at run time, which is why an IP blocklist cannot keep up: the operator repoints every infected machine at once by sending a single transaction.

Step by step

  1. Query public Ethereum RPCsFalls through 1rpc.io, drpc.org, publicnode and blastapi, so cutting off any one endpoint changes nothing.
  2. Read the wallet's newest transactionThrough the Blockscout API, sorted newest first.
  3. Decode an IP from the destination addressThe first eight bytes are read as two IPv4 addresses โ€” a primary and a fallback.
  4. Fetch stage two over plain HTTPFrom /0x/clb, /0x/cls or /0x/ls, on port 443 to look like TLS to a port-based filter.
  5. Decode and spawn it detachedXOR-decoded and evaluated in memory โ€” nothing is written to disk โ€” then spawn(โ€ฆ, {detached:true}) and unref(), so it outlives the editor that started it.

Address โ†’ endpoints

20-byte destination address
a322e5f3 d311d308 e6f01210 63e9aDC2 490Ef1aโ€ฆ
bytes 0โ€“3 โ†’ primary C2 bytes 4โ€“7 โ†’ fallback remainder unused

The payload observed in the wild stole clipboard contents โ€” passwords, access tokens and wallet seed phrases โ€” from the moment the editor opened.

Check by hand

Four checks you can run right now

No tools required. Run these against any clone on your machine โ€” the whole thing takes about a minute.

1 ยท Is a loader running right now?

macOS ยท Linux
ps -eo pid,args | grep -E "node .*-e .*global\[" | grep -v grep

No output means nothing is running. Any output is a live process โ€” kill it.

2 ยท Does any file carry the operator's wallet?

working tree
grep -rn "0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a" .
and the full history โ€” a deleted file still lives in Git
git log --all -S "0xa322E5f3" --pickaxe-regex --oneline

3 ยท Is a task set to run when you open the folder?

editor auto-run
grep -rn "folderOpen" .vscode/tasks.json

Any hit executes the moment the folder opens. Treat it as hostile until proven otherwise.

4 ยท Are your font files actually fonts?

magic bytes
head -c 4 public/fonts/*.woff2

Every real font prints wOF2. Spaces mean you are looking at a script.

Look for the long line

The most reliable giveaway is geometry, not content. This finds any file padded out to hide something past the edge of the screen โ€” a hand-written config has no business being 9,000 characters wide.

hidden-payload heuristic
awk 'length > 1500 {print FILENAME": line "FNR" ("length" chars)"}' \
  $(find . -name "*.js" -not -path "*/node_modules/*")
The tool

snare does all of this, everywhere you keep code

Four commands. It uses your own GitHub credentials and ships no token of its own.

snare guard install

Watch this machine

Detects the loader and kills it โ€” process and children โ€” within about a second, then saves the evidence. It only ever kills interpreters, so a shell that merely mentions an indicator is left alone.

snare scan github

Scan every repo

Checks every repository you can reach through the API without cloning any of them. snare scan repo . goes deeper on a clone: working tree, every branch, full history.

snare fix owner/repo

Clean it out

Writes a backup bundle first, always. Cleans branch tips, or purges the payload from every commit in history. Dry run unless you explicitly say otherwise.

snare notify owner/repo

Warn your team

Files a GitHub issue mentioning your collaborators and opens a pre-filled mail draft for each one. It never sends anything โ€” you send from your own account.

Install

Pick your machine

Free and open source under the MIT licence. Requires bash, git, python3 and the GitHub CLI.

macOS ยท Homebrew
brew install git gh python3 git-filter-repo

git clone https://github.com/AviOfLagos/snare ~/snare
cd ~/snare && ./install.sh
gh auth login
snare doctor

The guard runs as a launchd user agent, started automatically at login.

RequirementWhy it is needed
bash git python3Core. Already present on macOS and most Linux distributions.
ghAll GitHub access uses your credentials. snare ships no token and stores none.
git-filter-repoOptional โ€” only for snare fix --purge-history.
Limits

What snare does not do

A security tool that oversells itself is worse than no tool at all.

The guard polls once per second, so a payload can still act before it is caught. It cannot see code already running inside another process, and it will miss variants built on different infrastructure. Removing the malware from the repository is the actual fix โ€” a running guard is never permission to open a repo you do not trust.

Scanning through the GitHub API sees branch tips only. A payload that was committed and later deleted survives in history; use snare scan repo on a clone to catch that.

Rewriting history does not touch forks, pull-request refs, or objects still reachable by SHA. Ask GitHub Support to garbage-collect after a purge.