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.
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.
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.
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.
Line 5 is 9,135 characters long. Scroll it sideways โ or reveal the whitespace.
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.
Step by step
- Query public Ethereum RPCsFalls through 1rpc.io, drpc.org, publicnode and blastapi, so cutting off any one endpoint changes nothing.
- Read the wallet's newest transactionThrough the Blockscout API, sorted newest first.
- Decode an IP from the destination addressThe first eight bytes are read as two IPv4 addresses โ a primary and a fallback.
- Fetch stage two over plain HTTPFrom
/0x/clb,/0x/clsor/0x/ls, on port 443 to look like TLS to a port-based filter. - Decode and spawn it detachedXOR-decoded and evaluated in memory โ nothing is written to disk โ then
spawn(โฆ, {detached:true})andunref(), so it outlives the editor that started it.
Address โ endpoints
The payload observed in the wild stole clipboard contents โ passwords, access tokens and wallet seed phrases โ from the moment the editor opened.
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?
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?
grep -rn "0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a" .
git log --all -S "0xa322E5f3" --pickaxe-regex --oneline
3 ยท Is a task set to run when you open the folder?
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?
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.
awk 'length > 1500 {print FILENAME": line "FNR" ("length" chars)"}' \
$(find . -name "*.js" -not -path "*/node_modules/*")snare does all of this, everywhere you keep code
Four commands. It uses your own GitHub credentials and ships no token of its own.
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.
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.
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.
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.
Pick your machine
Free and open source under the MIT licence. Requires bash,
git, python3 and the GitHub CLI.
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.
# Debian / Ubuntu sudo apt install -y git python3 # gh: github.com/cli/cli/blob/trunk/docs/install_linux.md # # Fedora: sudo dnf install -y git python3 gh # Arch: sudo pacman -S git python github-cli git clone https://github.com/AviOfLagos/snare ~/snare cd ~/snare && ./install.sh gh auth login snare doctor
The guard runs as a systemd --user unit.
No systemd? Run snare guard run --interval 1 yourself under your own supervisor.
winget install Git.Git GitHub.cli Python.Python.3.12 # then, from Git Bash โ not cmd or PowerShell git clone https://github.com/AviOfLagos/snare ~/snare cd ~/snare && ./install.sh gh auth login snare doctor
snare is written in bash, so it needs Git Bash or WSL.
snare guard install registers a logon Scheduled Task.
sudo apt install -y git python3 git clone https://github.com/AviOfLagos/snare ~/snare cd ~/snare && ./install.sh gh auth login snare doctor
The guard only sees processes inside WSL, not Windows itself. If you develop on Windows proper, run snare from Git Bash instead.
| Requirement | Why it is needed |
|---|---|
bash git python3 | Core. Already present on macOS and most Linux distributions. |
gh | All GitHub access uses your credentials. snare ships no token and stores none. |
git-filter-repo | Optional โ only for snare fix --purge-history. |
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.