Two malicious packages have been published to pub.dev, this is the first time a supply chain attack has hit the platform. It began with a maintainer's account getting compromised, and from August 12th, they spent the next four weeks quietly pushing out malicious releases of universal_file_viewer and surveyjs_flutter. The malicious actor was targeting developer credentials by injecting malicious code into the build scripts of the packages. This looks to be an XCSSET variant, a macOS worm that hides in build steps, which more recently has been targeting Android through Gradle build files.
This is the first appearance of trusted packages compromised on this registry.
The research team at Ossprey reached out to the maintainer to let them know and get the packages taken down.

The download count is small, (~818 between the two packages), this however, does not mean the impact is low. As we have seen in previous platforms and registries, this is only the beginning. According to Flutter "The pub.dev ecosystem is more popular than ever, with over 1.3 billion package downloads in the last 30 days alone". The impact is huge, the customer needs to be prioritised, so we must be proactive in identifying and blocking threats in our software supply chain. This goes to show trust can no longer be inferred, it must be earned.
What happened?
We believe this to be the first supply chain attack on pub.dev.
On 8th September 2026, security researcher Charlie Eriksen reported that version 0.1.5 of universal_file_viewer (a legitimate Flutter package for previewing images, video, PDFs and Office documents) had been published with malicious code added to build files inside the package's bundled example app - a Gradle file (Gradle being Android's build system).
This then triggered us to start an investigation, where we identified that additionally there was malicious code in the the iOS and macOS Xcode project files and another package surveyjs_flutter which actually was first compromised on August 12th.
To be clear, this is not the first time the Flutter framework has been weaponised - https://www.jamf.com/blog/3cx-supply-chain-attack/ - it is simply the first time a legitimate package has been weaponised on the pub.dev platform.

Who is at risk?
Execution requires someone to build the example project specifically: contributors and repository cloners (the payload is committed to public master), CI that builds example apps, or a developer opening the example to evaluate the package. These files are shipped to everyone who installs the package but executed by a narrower group. While this means just downloading the package might not imply you are compromised. It is still recommended you trigger a security incident.
Timeline

surveyjs_flutter0.1.0, built on 28 July, is clean.Version 0.1.1, published fifteen days later on 12th August, carries the first payload.
That fifteen-day gap suggests this is when injection began, not necessarily the intrusion itself, since dormant malware could leave the same trace.
Three infected files in 0.1.2 on 4th September
Four infected files in 0.1.3 on 7th September.
The malicious actor was adding vectors throughout the month rather than deploying everything all at once.
universal_file_viewerwas untouched during this period. When the maintainer did a release on 8th September, committing at 10:42 and publishing at 10:44, it inherited the full three-vector payload.Roughly an hour later, at 11:45, they published 0.1.6 and retracted 0.1.5. That 'remediation' removed the Gradle hook and deleted the leftover
.bakfile, but left both Xcode payloads completely intact. The two project files are identical between 0.1.5 and 0.1.6.The practical effect is worth sitting with: because retraction removes a version from dependency resolution but does not delete it, the visible cleanup step moved users off a three-payload version and onto a two-payload version, while implying that the incident was handled.
Impact
For anyone with universal_file_viewer or surveyjs_flutter in a Flutter project, they should check which version is being used and ensure they have a clean release. The total download was relatively low, suggesting this was a more targeted attack. This is not the extent of the impact, this shows that threat actors are changing their techniques to avoid detection, using a platform where historically scanners and researchers are not focusing (the main focus being npm, pypi) to aid evasion.
Additionally the payload was found in the native build files bundled inside a cross-platform package, executing during the build rather than at runtime or install - this delayed execution is an attempt to make the attacks harder to detect. New attack vectors must be analysed so we can protect against the inevitable future instances. It also tells us that the threat actors are not simply changing their methods or the payload, they are seeking alternative, overlooked platforms.
Malicious packages stopped being a niche problem a long time ago. There were 1.233 million malicious packages found in 2025 alone, by Q2 of this year, this total was 1.8 million.
Npm still dominates the raw counts, but the non-npm activity in Q2 2026 was concentrated in PyPI (48.5%) and NuGet (38.0%), with Hugging Face models at 6.8% and RubyGems at 3.0%. More and more ecosystems are being impacted, this can not continue to be an NPM problem, its a cross platform crisis, safety by obscurity is no longer an option.

The technical bit
Flutter is Google's cross-platform app framework, Dart is the language it uses. A Flutter package isn't just Dart code, building an app means using Gradle on Android and Xcode on Apple platforms, and the configuration those tools need (build scripts, podspecs, Xcode project files) all ship alongside the Dart source code. Those files are also code, and they run on your machine, or your CI runner, during the build.
Put malicious code in the Dart source and a user reading lib/ has a decent chance of spotting it. Put it in bundled build tooling and three things work in the attacker's favour:
It is outside where people look. The reasonable mental model of "the package" is the Dart code. This attack is what it looks like when that assumption is inverted
It executes at build time, on a developer or CI machine. Build machines hold signing keys, cloud credentials, CI secrets and publish tokens. They are a better target.
Build pipelines are trusted and automated. A Flutter build is a thing that happens on a runner, at speed, with credentials, and nobody typically reads the output.
What to do now
If you build Flutter or Dart apps:
Check
See if
universal_file_viewer0.1.5 or 0.1.6, orsurveyjs_flutter0.1.1, 0.1.2, 0.1.3 appear in anypubspec.lock, pub cache or CI cache. Presence means the malicious files were written to that machine, not that they executed. Treat credentials on that machine as exposed.Clean release now exists - 0.1.4 for
surveyjs_flutterand 0.1.7 foruniversal_file_viewerThis check finds affected packages regardless of whether anything ever built, and uses the injector's own UUID fingerprint rather than its rotating C2 domains:
Review
What does your dependency scanning actually read. If it only parses Dart source, or only checks CVE databases, it will not see this compromise.
If you own a build pipeline in any ecosystem:
Treat native build files (
build.gradle,*.podspec,project.pbxproj,binding.gyp,build.rs,setup.py, proc-macro crates) as executable code that arrives with your dependencies, because that is what they are. Xcode project files deserve particular attention: aPBXBuildRulecan claim any file pattern and run a script instead of compiling it.Scope build-runner credentials to what the build needs and nothing more. Build-time execution is only as valuable as the secrets sitting next to it.
The wider lesson
NPM and Pypi are popular open source ecosystems making them attractive for hackers to target. They can hide in the noise and target any company already using them. However, targeting smaller ecosystems like flutter still allows you to focus and target large organisations and avoid the growing investment in detecting malware in the more popular ecosystems. Flutter is used to build production mobile apps for banks, retailers and healthcare providers. The registry behind it just proved it can carry a compromised package into an Android build with credentials attached. That the first one was small tells you about the attacker's testing, not the potential.
Technical breakdown of the attack
Summary
The malicious code lives in the packages' example-app build tooling, a Gradle task hook and, a pair of injected Xcode build rules, where it fetches a remote shell script from a rotating set of .ru domains and pipes it directly to sh. See below for the affected packages.
At the time of writing,
Malicious versions have been retracted, but can still be downloaded
There is no advisory in OSV for either package.
Clean versions are listed above.
Importantly, this does not appear to be a malicious publisher. The malicious files are committed to the maintainer's own GitHub repository under their own commits. The injection pattern, build scaffolding rewritten in place, with a stray .bak left behind, does seem to be characteristic of malware operating on a developer's machine. This is how the XCSSET worm operates. The maintainer is a victim here, and the evidence suggests their development environment was compromised, then quietly re-signed everything they built for the next four weeks.
Affected packages and versions
Package | Version | Published (UTC) | Status |
|---|---|---|---|
| 0.1.1 | 2026-08-12 11:08 | Infected — 1 payload |
| 0.1.2 | 2026-09-04 11:57 | Infected — 3 payloads |
| 0.1.3 | 2026-09-07 13:18 | Infected — 4 payloads |
| 0.1.5 | 2026-09-08 10:44 | Infected — 3 payloads |
| 0.1.6 | 2026-09-08 11:45 | Infected — 2 payloads |
What is malicious, and why
Vector 1 — the Gradle task hook
Appended to the end of example/android/app/build.gradle.kts:
Two layers of obfuscation.. printf xAxd | tr -d A evaluates to xxd, so the decoder's name never appears literally in the file; the sibling variant uses printf bdase64 | tr -d d for base64. The hex blob is then decoded and piped to sh. Every variant decodes to the same one-liner:
Vector 2 — the Xcode build rule
The injector adds a custom PBXBuildRule to example/ios/Runner.xcodeproj/project.pbxproj:
This abuses a legitimate Xcode feature. It claims every *.md file in the target and runs a script instead of compiling it. The cp and the declared output are not incidental: they satisfy Xcode's dependency tracker so the rule is marked fulfilled and does not re-run, while making it look like a file copy.
The payload itself is a build setting buried in an alphabetised list:
Splitting them is an attempt to make the code look benign, however, when looking at both its clear this is not the case.
Finally, the rule needs something to fire on. Version 0.1.5 added zero-byte README.md files to example/ios/ and example/macos/, wired in as build inputs. A real README is never empty, and READMEs do not belong in platform subdirectories; this project's actual README is at the repository root.
The tampering is purely additive, eight insertions, no modifications or deletions, adding the six object-graph entries Xcode requires (PBXBuildFile, PBXFileReference, a group children entry, a target buildRules entry, the rule itself, and a Resources phase entry) plus the build setting in two configurations. That is a structure-aware pbxproj editor, not a blind text append.
A third variant, tagged p=xcode_phase, uses a PBXShellScriptBuildPhase instead and appears in surveyjs_flutter.
Indicators of compromise
Command-and-control- All 6 accept a POST to path /a with a p=<vector> body:
Host and file artifacts:
Build setting name
A3EA261Build rule with
filePatterns = "*.md"andcompilerSpec = com.apple.compilers.proxy.scriptOutput path
/tmp/${INPUT_FILE_BASE}Zero-byte
example/ios/README.mdandexample/macos/README.mdVector tags
android_kotlin,xcode_rule,xcode_phaseDecoder idiom
printf <str> | tr -d <char>resolving toxxdorbase64
The most durable indicator is an artefact of the injector's own UUID generator: every Xcode object it creates ends in 162AE3.
File hashes (universal_file_viewer):
File hashes (surveyjs_flutter):
Clean baselines:
A Note From Ossprey
This campaign is the first if its kind on the pub.dev platform and so we expect more information to come as this develops and as more threat actors utilise this platform.
We would recommend that anyone currently pushing code or responsible for package updates lock down their releases processes and ensure that active dependency and package scanning is in place.
If you are interested in how Ossprey Security can help secure you from these risks, please get in touch.

Real World Attacks

Blankets, worms, and RATs: New worm targets NPM and AUR

Ellie Jevon
29 Sept 2026
Real World Attacks

How Ossprey protects from novel AI-accelerated malware development, and cross-ecosystem supply-chain attacks.

Ossprey Research Team
24 Sept 2026
Real World Attacks

Shai-Hulud Again: Ten Signed Releases and the State of npm 12 Adoption

Valentino Duval
28 Aug 2026
