I Wrote About Android Banking Malware Last Month. I Didn’t Expect It to Reach My Own Package Manager. 📦
“I remember thinking, at least this stays in the Android app layer. Not my toolchain. Not the thing I actually run every day.”

A little while back I went deep on ToxicPanda — the Android banking trojan abusing accessibility services, the fake overlays, the whole escalating mess of it. Writing that piece, I remember having a specific, comfortable thought while researching it: this is a threat aimed at end users installing sketchy APKs. Not developers. Not the tools sitting on my own machine.
That comfort lasted about as long as it took for a security researcher to find the first-ever malware sample on pub.dev — the official package repository for Dart and Flutter. The place I run flutter pub add from without a second thought, multiple times a week.
This one’s worth going deep on, because the mechanics are genuinely more interesting — and more instructive — than “a bad package existed.”
What Actually Got Found 🔍
“Not a targeted attack. That’s almost the more unsettling part.”
Security firm Aikido discovered a compromised package called universal_file_viewer— a small, legitimate-looking Flutter package for previewing PDFs, Word docs, Excel files, video, and Markdown inline, sitting around 500 downloads. Version 0.1.5 was carrying a variant of XCSSET, a macOS worm that's been circulating and evolving since 2020.
Here’s the nuance that actually matters most, and it changes how worried you should genuinely be: this wasn’t someone deliberately targeting Flutter developers. The package maintainer’s own Mac was already infected with XCSSET, and the worm — true to its nature — had already spread itself into every Android Gradle project, every Xcode project, and every git repository it could find sitting on that infected machine. When the maintainer published a routine package update from that same computer, the poisoned build files rode along with it, unknowingly.
The package’s actual library code — the part every consumer’s app depends on and compiles — stayed completely clean. The infection lived entirely inside the package’s example/ folder, a directory that ships in the pub.dev tarball but never compiles when you add the package as a normal dependency. Add universal_file_viewer to your pubspec.yaml, build your own app the normal way, and nothing fires. The actual exposure is much narrower: someone who clones the repository directly and deliberately builds the example app themselves.
How the Infection Actually Triggers, Step by Step ⚙️
“Four stages, each one deliberately small, so no single piece looks suspicious on its own.”
Worth breaking this down properly, because the layered design is exactly why it’s hard to catch. Think of it less as one malicious file and more as a relay race, where each runner only knows the very next handoff.
Stage one starts the moment a poisoned build actually runs — a Gradle build, or an Xcode build touching a .md file. A tiny hook, hidden inside an otherwise normal-looking build configuration file, quietly reaches out to a remote server and asks for instructions.
Stage two: that remote server sends back a small script. The script’s first real job is reconnaissance — it collects details like the machine’s hardware serial number and system locale, then uses those details to fetch the actual malicious payload, drop it into a temporary file, and quietly compile it into an invisible application on the spot. It launches that invisible app, and then deletes all the evidence of the last few steps within about ten seconds — the file that downloaded it, the compiled app itself, gone before most people would even notice their fan spun up.
Stage three: that invisible app is a compiled AppleScript, and it isn’t sitting there in readable form waiting to be scanned. It decrypts itself in memory, using a key that changes based on the actual date, then runs the decrypted result without ever writing the plain version to disk.
Stage four: what actually decrypts is the real brain of the operation — an orchestrator that decides which of the worm’s many specialized modules to run next, based on what it finds on the machine.
The point of structuring it this way isn’t cleverness for its own sake. Splitting the chain into four separate, individually boring-looking stages means no single file a security scanner might check ever contains the “smoking gun” all by itself. The malicious intent only exists in the combination.
The Modules That Spread It Further 🪱
“It doesn’t hunt for one specific target. It infects everything reachable and lets developers do the spreading for it.”
Once stage four is running, the worm doesn’t just steal something and leave. It actively works to spread itself to as many other projects as possible, using three separate, specialized modules — each one searching for a different kind of target.
The Android module walks through your entire home directory hunting for Gradle build files — both the older Groovy-style build.gradle and the newer Kotlin-based build.gradle.kts. For each one it finds, it double-checks that it's a genuine Android project before touching it, quietly removes any earlier infection first if one's already there, and then injects its own hook into the build process. From that point forward, simply building that project — any project, not just the one you were originally working on — silently re-triggers stage one all over again.
The git module works differently, and honestly more insidiously. It finds every .git folder on the machine and writes a malicious hook into each one's commit process. Every single commit made afterward, in any of those repositories, quietly re-fires the chain. Each infection is deliberately randomized — different remote server, different encoding — specifically so that scanning for one known signature won't catch the next one.
The Xcode module is the most established of the three, having been part of XCSSET’s toolkit since it was first documented back in 2020. It parses a project’s configuration file, confirms it’s looking at a genuine Xcode project, and injects a build phase that fires the same way, the next time anyone opens and builds that project.
Step back and look at what these three modules add up to: a single infected Mac doesn’t just carry one infected project. It becomes a machine that quietly re-infects every Android project, every Xcode project, and every git commit that developer touches from that point forward — regardless of whether those projects have anything to do with each other.
The Part That Actually Unsettled Me: Hiding Inside Launchpad 🎯
“It doesn’t hide in a folder you’ll never open. It hides inside an icon you click on purpose, constantly.”
Most persistence mechanisms try to survive a reboot by tucking themselves into some obscure system folder most users never look at. This one does something more deliberately sneaky: it quietly swaps out your actual Launchpad icon in the Dock — the one you click to see all your installed apps — for a fake one built to look and behave identically to the real thing.
Click it, and it opens the genuine Launchpad exactly as expected, so nothing about the experience feels wrong. But behind that normal-looking click, it also silently re-triggers the entire infection chain, every single time. It survives a full reboot without leaving any trace in the standard places a security-conscious person might think to check.
I keep coming back to that detail, because it’s such a good example of attacking the thing people trust because it’s mundane. Nobody suspects their own Dock.
The Actual Theft, Module by Module 🕵️
“Once it’s spread and it’s persistent, only then does it actually start stealing anything.”
Everything up to this point has been about survival and propagation. The theft modules are fetched from the remote server and run entirely in memory — nothing gets written to disk, which is exactly why traditional file-based antivirus scanning struggles to catch this stage at all.
- Browser credential theft targets Chrome, Safari, and Firefox separately, each with its own dedicated module. For Chrome specifically, it deliberately breaks the browser’s own auto-update mechanism in a way that forces Chrome to regenerate its internal encryption key — and intercepts that regeneration to unlock saved passwords, cookies, and active session tokens.
- Messaging app hijacking goes after Telegram specifically: it force-quits the real app, replaces it entirely with a modified copy, and targets that app’s stored session data — meaning an attacker could potentially access active conversations without ever needing your password.
- Productivity app snooping waits patiently for apps like Notes, Reminders, or Calendar to actually be open, then quietly extracts data from them while they’re running.
- Clipboard monitoring does exactly what it sounds like — continuously reading whatever’s currently copied and sending it off, which matters more than people usually assume, since clipboards routinely hold passwords, one-time codes, and API keys people copy-paste without thinking twice.
- Privilege escalation comes through a module that impersonates the actual macOS System Settings app, aiming to trick a user into granting elevated permissions through what looks like a completely normal system prompt.
- General file harvesting walks through the rest of the home directory, pulling files out of specific folders likely to contain something valuable.
Everything stolen gets encrypted before it’s sent anywhere, specifically to avoid tripping network-traffic monitoring tools that might otherwise flag an obvious, readable data leak in transit.
Why This Actually Matters, Beyond One Package 💭
“The specific package gets patched. The pattern it exposed doesn’t go away with it.”
The instinctive reaction is to file this under “one bad package, now fixed, move on.” Technically true. But the pattern underneath is what’s actually worth sitting with. This wasn’t an attack engineered against Flutter specifically — it was a general-purpose macOS worm that happened to land in a Flutter package because a developer’s own machine was already compromised, and package registries, pub.dev very much included, have no real way to inspect the state of the machine publishing a release.
That’s a genuinely different threat model than the one I covered with ToxicPanda, and arguably a more uncomfortable one. ToxicPanda required a victim to actively install something sketchy. This required nothing from any of us except the routine, unexamined trust that a maintainer’s development machine was clean — an assumption every developer makes, constantly, every time they add a dependency, and one that’s essentially unverifiable from the outside looking in.
What Actually Changes For How I Work 🛠️
A short, practical list, not a paranoid one:
- Don’t blindly clone-and-build example apps from packages you haven’t vetted, especially not on a machine holding anything sensitive — that’s the specific trigger condition here.
- Keep macOS and Xcode current, and take unusual build behavior seriously instead of shrugging it off, since a hook firing silently in the background is, by design, meant to look like nothing happened at all.
- Treat your own development machine as part of the supply chain, not just a tool sitting safely outside it. If something like this ever lands on your Mac, every project you touch afterward becomes a potential carrier — not just the one you were originally working on.
- Run malware scanning on your dependencies, not just vulnerability scanning. This entire incident was caught by exactly that category of tooling, not by anyone manually reading through an example/ folder's build configuration by hand.
Where I Land On This 💭
I genuinely thought writing about Android banking trojans was about as close as this topic would ever get to my own daily toolchain. I was wrong within the same month. The lesson isn’t that Flutter or pub.dev is uniquely unsafe — this could just as easily have landed in any language’s package registry, and npm’s had its own well-documented version of this exact story more than once. The real lesson is that “my dependency’s source code is clean” and “the machine that built and published it was clean” are two completely different claims — and only one of them is something a package registry can actually verify on your behalf.
I don’t think about flutter pub add the same casual way anymore. That's probably a healthier place to land than where I started a month ago.
Tags: Flutter, Cybersecurity, Malware, Mobile Development, Software Supply Chain, Dart
I Wrote About Android Banking Malware Last Month. was originally published in Stackademic on Medium, where people are continuing the conversation by highlighting and responding to this story.
