A first-hand incident writeup — October 2026
The pitch
It started the way these things usually do: a recruiter on LinkedIn, a real conversation, a real-sounding opportunity. The task was squarely in-genre for a blockchain developer — take an existing payroll/treasury platform called “ConfidentialPay” and add a cross-chain bridge feature. Not a toy exercise, a whole application: React frontend, Express backend, mock database layer, even Solidity contracts for a shielded pool and a ZK income prover. The kind of codebase that looks like eleven months of someone’s actual work, because that’s exactly what the attackers wanted it to look like.
It was hosted on Gitea, not GitHub — a small, easy-to-miss tell. The assignment was ordinary in every respect that mattered: fork the upstream repo, clone your fork, build the feature. The upstream project’s git log showed a clean, steady history: eleven commits from June through July, each with a plausible engineering-sounding message (“Employee & Payroll Backend: Designed APIs for employee management and payroll processing”), all authored by the same name. It read like a real project’s real commit history, because it probably was — likely a real developer’s real repository, quietly copied and repurposed as bait, seeded with the malicious code from the very start (traced to specific commits from mid-June), then handed out as a “technical assessment” to whoever the recruiter was targeting. The two commits that followed — the ones actually implementing the requested bridge feature — were the victim’s own honest work on their own fork, made after the trap was already fully in place. They didn’t write a single line of what follows.
The trap you don’t see
The instructions were simple: clone it, npm install, npm run dev, build the feature. Nothing about that should feel dangerous. There’s no email attachment to be suspicious of, no .exe to right-click-refuse, no “enable macros” dialog. It’s just… a Node.js project. The malicious code wasn’t bolted onto the outside of the app. It was written into the app, in the one place almost nobody reads carefully: error-handling boilerplate.
Every Express backend needs a way to turn a raw MongoDB duplicate-key error into something readable for a user. It’s copy-pasted from Stack Overflow or a previous project more often than it’s written from scratch, and it looks exactly as boring as it should. Buried in this project’s version of that file was one extra line, tacked onto the end:
const dbconnectorSVG = require("../../public/dbconnector.svg");
An SVG. A vector icon. The kind of file every frontend project has dozens of, sitting quietly in a public/ folder, never suspected of doing anything at all — because SVGs are pictures, not programs.
Except Node.js has a quirk: ask it to require() a file with an extension it doesn’t recognize, and it shrugs and runs the file as JavaScript anyway. There was no picture in that SVG. There was 24 kilobytes of code, padded out with a fake auto-generated-looking comment banner and thousands of blank lines so that scrolling past it in an editor reveals nothing but whitespace. And because that require() line lives at the top level of a file that gets loaded the moment the server boots — not inside some route handler that only fires when a user clicks something — the payload runs the instant npm run dev starts the backend. Before you’ve written a single line of the bridge feature you were asked to build. Before you’ve even looked at the code.
What actually happens next
That first payload calls out to jsonbin.io — a legitimate, free JSON-storage API, the kind of domain that’s never going to be on anyone’s blocklist — fetches a blob of JSON, and runs whatever JavaScript is sitting inside it. A second, independent copy of the same mechanism, discovered already sitting on disk in a ~/.vs_cache folder, does the same thing through a different path: a URL shortener that quietly redirects to the attacker’s own server. Two separate channels to the same destination, so that killing one doesn’t kill the operation.
What comes back and runs, when we captured and read it, does three things in order. First, it introduces itself to the attacker: hostname, computer name, network interface hardware addresses — a fingerprint, so whoever’s running this campaign knows which machine is which across however many victims they have running at once. Second, it makes itself permanent: a LaunchAgent tucked into ~/Library, configured to relaunch on every login, pointing at a fake application bundle named AppleAccountSync — official-sounding, no Dock icon, designed to never be looked at twice. Third, it drops a second, compiled program, hidden inside ~/.zsh_sessions of all places — a folder your terminal uses to remember command history, not somewhere anyone goes looking for malware.
Then it settles in. Its own internal log message put it plainly: “Initial backup scheduled in 300s.” Every five minutes, for as long as it’s running, it reaches for things. The evidence — deliberately decoded from the malware’s own obfuscated code, not guessed at — points at browser data across Chrome, Brave, Edge, and Opera; at crypto wallet software, Exodus by name; at SSH material; at documents matching .pdf, .xlsx, and .txt. And it ships whatever it finds out over HTTP, the same unremarkable protocol every web app on the planet uses, which is exactly why nothing about a node process making outbound web requests looks alarming on its own.
The part that actually cost money
A few days after all this — after the assignment was done, after attention had moved on — a small test wallet, the kind of thing you keep around with a few dollars in it for exactly this kind of experimentation, got drained. Not hacked in some dramatic sense. Just copied. A mnemonic phrase, pasted into a field the way you’d paste anything, on a machine that had, without anyone’s knowledge, been listening to the clipboard for eight days.
The compiled second-stage program is the likely author of that particular theft. Unlike the JavaScript layer, it resisted analysis — packed so thoroughly that a byte-level randomness measurement came back at 7.9975 out of a possible 8.0, functionally indistinguishable from noise. What could be determined is which pieces of the operating system it links against: the frameworks Apple ships specifically for reading the system clipboard and for controlling other applications’ interfaces. And, during the investigation itself, a live demonstration: mid-analysis, days after the original infection, that same process attempted — unprompted, unrelated to anything the investigator had done — to request control over macOS’s own UI-automation system. The kind of permission that, if granted, hands a program the ability to act as if it were the user: clicking, typing, reading whatever’s on screen. It was denied. It’s worth wondering how many times, on how many other machines, that same prompt was clicked through without a second thought.
Confirming it, without becoming a second victim
Reading code is one kind of confidence. Watching it happen is another. To be certain — not just reasonably sure, but certain — that this repository was the actual source rather than some unrelated coincidence, the investigation included deliberately re-running the project on the same machine, this time with instrumentation watching every process it spawned, every file it wrote, and — carefully, without ever letting the malicious logic actually execute unsupervised — the literal source text of the code it fetched from the internet, captured the instant before it ran and never actually needed to finish running past that point.
Every one of those deliberate re-infections was killed within seconds, long before the five-minute “backup” timer could fire for real. The goal was evidence, not re-victimization.
What eight days of hindsight looks like
By the time this investigation reached back out to the attacker’s own infrastructure — the same servers, the same URLs, hard-coded in the malware itself — most of it had already gone quiet. The JSON dead-drop had been deleted. The redirector now serves something unrelated to anyone who asks it directly, a decoy for exactly the kind of probing this investigation was doing, real payload reserved for requests that look enough like the actual malware’s. That’s not incompetence on the attacker’s part. It’s the same operational hygiene any well-run infrastructure practices: assume you’ll be looked at eventually, and make sure looking doesn’t tell anyone anything useful.
The unglamorous ending
The system is clean now. Every dropped file, every persistence mechanism, every running process — found, traced back to its exact origin, and removed, twice over, because the first cleanup pass happened before the decision to re-run the project for confirmation, and the second because that confirmation necessarily meant reinfecting the machine on purpose, briefly, on record, with someone watching.
Nothing about the rest of the system shows signs of compromise. No SSH keys were touched. No shell startup files were modified. No other project on the machine carries the same trick. The blast radius, as far as eight days of after-the-fact forensics can determine, was exactly what it looked like at the start: this one repository, and the one small wallet that got emptied because its owner did, once, what the assignment asked — cloned a project, ran it, and trusted that a coding test was just a coding test.
It rarely is, anymore.