All I Know about the Security Risk of Flutter Packages

I am a freelance mobile app developer, mainly working on Flutter. I'm also developing a Flutter package named "crop_your_image", which enables Flutter app developers to build cropping images functionality in their app with their own designed UI. https://pub.dev/packages/crop_your_image
Have you ever counted how many packages your project depends on? If not, run the command below in your terminal and check the number it outputs.
flutter pub deps --json | jq '.packages | length'
The number is probably much larger than you expected.
This is because your application does not depend only on the packages you explicitly list in pubspec.yaml. It also depends on transitive dependencies brought in by those packages.
Every single one of these packages becomes part of your application, which means each of them can potentially become a security risk.
This doesn't mean you should avoid using packages. Packages are essential to Flutter development. However, we should understand what kinds of risks they may introduce.
In this article, I will explore how package dependencies can become security risks by looking at several concrete scenarios and discuss how we can reduce or mitigate those risks in practice.
Before discussing the concrete risks, how do you imagine packages are developed and published?
In many cases, Dart/Flutter packages are developed and published by individual developers as side projects. They write source code, manage repositories, fix bugs, or publish releases, just like we do in our own application projects.
Of course, some packages are maintained by companies, organizations, or the Flutter team, with experienced developers. However, many packages in the ecosystem are not created by special security experts or large teams with dedicated review processes. They are often created by developers like us, using the same tools, workflows, and habits that we use every day.
This is not a bad thing. Rather, it is the nature of the open-source ecosystem. However, it also means we should not assume that every package has gone through a strict security review before we add it to our applications.
With that image in mind, let’s look at some concrete scenarios where package dependencies can become security risks.
Scenarios
Scenario 1: publishing account is compromised
According to the Dart documentation, pub.dev uses Google Accounts to manage package upload permissions. To publish a package, the author writes the source code and runs the command below from their development environment:
dart pub publish
During this process, pub verifies that the authenticated account has permission to publish the package.
In other words, publishing a package is done by a developer from their own development environment using their own Google Account.
But what if the author's Google Account is compromised?
If a Google Account that has permission to publish a package is compromised, an attacker may be able to publish another version of their package, which possibly contains malicious behavior, in a surprisingly simple process.
The attacker can check out the original source code from the public GitHub repository, add malicious code to it, and publish it as a new version of the package using the compromised account.
From the users’ point of view, the update may look like a normal package update with the same package name from the same author.
We will discuss how to address this risk later. For now, let’s move on to another scenario.
Scenario 2: local pub credentials are stolen
Technically, compromising the author’s Google Account is not the only way to publish a malicious version.
Once a package developer publishes a package from their local machine, pub creates and stores pub-credentials.json on the machine so that the developer does not have to authenticate manually again for future releases.
This means that if an attacker could steal the credentials from the author's machine, they may also be able to publish a new version without directly accessing the Google Account.
Dart Blog also points out that leaking pub-credentials.json can lead to a severe security vulnerability.
Scenario 3: automated publishing is abused through GitHub Actions
There is another way to publish a package without directly using the author’s Google Account or local pub credentials.
pub supports automated publishing from GitHub Actions. This is useful for maintainers because they can publish a new package version automatically when they create a release tag.
However, this also means that the GitHub repository can become part of the publishing path.
If an attacker compromises a GitHub account with enough permission to push matching release tags, they may be able to trigger the publishing workflow and publish a new version of the package through CI.
These compromising scenarios explained so far are not exaggerated.
We have already seen similar incidents in other ecosystems such as npm, PyPI, and GitHub Actions.
Although the details differ from pub, the underlying pattern is similar: when the account, credential, repository, or automation that controls publishing is compromised, trusted software can deliver malicious code.
Besides those publishing-path scenarios, there are other ways package dependencies can become security risks.
Scenario 4: an abandoned package is maliciously forked
Sometimes, a package developer stops maintaining a package for various reasons.
Unfortunately, Flutter projects can't always keep using old packages forever because the Flutter SDK, Android, iOS, and other underlying platforms continue to evolve. They may introduce new APIs, deprecate old ones, or make breaking changes that require packages to be updated.
In that case, another developer may fork the package repository and provide a migrated version of the package. Then users can depend directly on the forked repository like this:
dependencies:
some_package:
git:
url: https://github.com/someone/some_package.git
ref: main
Although this can be a practical workaround, it also changes the trust boundary. Your application no longer depends on the original package and pub release process, but the forked repository and the person who controls it.
Imagine that the fork maintainer intentionally puts malicious code into the repository.
Because the repository is not controlled by the original author, the fork maintainer can change the code at any time without compromising the original author’s account, machine, or publishing credentials.
From the application developer’s point of view, this may look like a reasonable workaround for an abandoned package. However, we have to keep in mind that nobody guarantees that the forked repository will remain safe to depend on forever.
There may be more possible scenarios, such as the case where a package developer simply puts bugs or vulnerabilities in a package. However, in this article, let me focus mainly on scenarios involving malicious actors, such as compromised accounts, stolen credentials, abused publishing workflows, and malicious forks.
What happens if we install a malicious package?
So far, we have looked at several scenarios where malicious code can enter a package dependency.
Now, let’s think about what can happen if we actually install such a package.
If a malicious package is included in the released app, the incident may happen on the user’s device. For example, the package may collect sensitive data that appears on users' devices, send unexpected network requests, or modify behavior in a way that the application developer did not intend.
Some may think that an incident doesn't happen until a new version of the application, that contains a malicious package, is released. However, the risk is not limited to the released application.
The developer’s environment can also be affected. A malicious package may run during development, testing, code generation, or the build process. In that case, the developer’s machine, the CI environment, or the release infrastructure also can be a target.
In other words, when we talk about package security, we should not think only about “what happens after the app is released.", but also about what can happen before the release, while the app is being developed and built.
Although Dart packages do not commonly execute arbitrary scripts immediately when you install dependencies, unlike npm, we should consider that malicious code still has a chance to run right after installation during development and build time.
How can we reduce the risk?
First of all, we can't remove all package risks. No one may disagree that completely avoiding packages is not realistic for most Flutter projects.
So the goal is not to stop using packages, but to understand the risk and reduce it to a reasonable level.
Review the package before adding it
The first step is simple: look at the package before adding it.
Of course, we can't deeply audit every line of every dependency. However, for packages that touch sensitive areas, we should at least check whether the implementation roughly matches what the README.md and other documentation say.
For example, if a small UI package contains unexpected network access, file access, build-related logic, or other irrelevant behaviors or permissions, that deserves extra attention.
It is also worth paying attention to the package’s own dependencies.
Even if the package you directly added looks safe, it may depend on other packages. Ideally, those transitive dependencies should also be checked with the same level of care as direct dependencies.
Review updates before accepting them
Every dependency update is also a moment when we accept new code into our project.
If an update is published after a maintainer account is compromised, the malicious code may come through a normal version update. The package name may be the same. The maintainer may look the same. The version number may simply be newer.
This is why dependency updates should not be treated as a mechanical task. It is worth checking the changes between the latest version and the current version before accepting the update.
In addition, because a malicious version can be published from an attacker’s local environment, the code published to the pub server does not always match the code in the GitHub repository.
Therefore, when reviewing or comparing package versions, it is better to inspect the source code downloaded from the pub server itself, not only the source code on GitHub.
Fortunately, pub provides a handy command dart pub unpack to download and extract a package's resources from the pub server as below:
dart pub unpack some_package:1.2.3 --output ./packages_to_review
Use AI to support package review
Of course, reading all the incoming source code manually for every package is not always realistic.
This is one area where AI can be useful.
Although AI can't be treated as a perfect security auditor, using AI to review package code is often much better than doing nothing.
For example, we can ask AI to download the package into a working directory, inspect the source code, compare the current version with the previous version, summarize the diff, and point out suspicious changes.
We can tell AI that it should take a look at not only Dart files but also Android, iOS, web, or other native code when relevant.
If this comparison process works well in your environment, you may want to prepare a reusable skill or checklist for it. This makes it easier to review package updates and newly introduced packages continuously, instead of treating each review as a one-off task.
Again, this does not guarantee safety. But it can make suspicious changes easier to notice.
Pin versions or delay updates carefully
Another practical mitigation is to avoid updating too quickly.
In some supply chain incidents, malicious versions are discovered and removed relatively quickly after publication. If your project does not automatically update to the latest version immediately, you may avoid many dangerous situations.
Thus, the techniques of version pinning, lock files, and delayed dependency updates can help here.
However, this is a trade-off.
Delaying updates can also delay important bug fixes or security fixes. If a serious vulnerability is fixed in a new version, staying on an old version for too long can become another risk.
So the point is not simply to say “never update quickly.”
The point is to control when and how updates are accepted. We can delay ordinary updates, but still need a process to quickly review and apply urgent security fixes when needed.
Reduce unnecessary dependencies
One of the simplest ways to reduce package risk is to reduce unnecessary dependencies.
Every dependency expands the amount of code we rely on. It also expands the number of maintainers, repositories, credentials, release processes, and transitive dependencies that can affect our application.
This does not mean we should reimplement everything ourselves.
It is still relevant to depend on packages whose core functionalities exactly fit our project.
On the other hand, if a package solves a very small problem, or if we use only a tiny part of a large package, it may be worth asking whether we really need it.
Recently, AI has made it easier to implement small utilities ourselves or migrate away from packages that we use only lightly. This does not mean that AI-generated code is automatically safer, but it can reduce the cost of comparing “using a package” with “maintaining a small implementation ourselves.”
For example, we can inspect how we use a package in the current project, check which features we actually depend on, and estimate how difficult it would be to replace it with our own implementation.
This kind of shift can also be supported by AI.
Be careful with Git dependencies and forks
Git dependencies and fork dependencies deserve special care.
They are useful when we need a fix that has not been released to pub.dev, or when the original package is no longer maintained.
However, they also change the trust boundary.
When we depend on a Git repository directly, we depend on the person who controls that repository. If we point to a moving branch such as main, the code we receive may change over time.
If we use a Git dependency, pinning it to a specific commit is safer than depending on a moving branch.
dependencies:
some_package:
git:
url: https://github.com/someone/some_package.git
ref: 1a2b3c4d5e6f7890abcdef1234567890abcdef12
If you need to use a fork, we should consider forking it ourselves instead of depending on someone else's fork repository, applying the minimum necessary changes and clearly documenting why the fork exists.
Prepare for compromise
Finally, we should also prepare for the case where prevention fails.
Even if we review packages carefully, pin versions, and reduce dependencies, we can't guarantee that we will never be affected by a compromised package.
So we should also think about incident response.
For example, if our GitHub repository or developer environment is compromised because of a compromised package, what secrets may be exposed? Which tokens are stored in CI? Which credentials exist on developer machines? Which API keys are embedded in the app? Which services need to be rotated?
Thinking about these questions before an incident happens can reduce damage when something actually goes wrong.
For mobile apps, this is especially important because rotating a key that is already embedded in a released app may require a new app release, which means it takes time depending on the platform and review process. A hasty rotation process can also risk breaking versions that have already been released.
Therefore, we should simulate the flow in advance.
If a token is leaked, how do we revoke it? How do we issue a new one? How do we deploy it? How do we avoid breaking existing users? Who needs to be notified? Which logs should we check?
Security is not only about avoiding incidents. It is also about reducing damage when an incident happens.
Conclusion
Fortunately, as far as I know, the Dart and Flutter ecosystem has not experienced a large-scale, widely reported supply chain incident like the ones we have recently seen in some other ecosystems.
However, when we look at the patterns behind those incidents, I think it is reasonable to say that similar incidents could also happen in the Dart and Flutter package ecosystem.
The point is not to avoid using packages, but to understand that package selection is a security decision as well as a development decision.
Dependencies can become risky if their publishing path is compromised; an author's GitHub Account can be compromised, stolen local credentials can become another publishing path, or CI automation can be abused. A fork of an abandoned package can also become a source of malicious code.
What is lucky is that these risks are easier to understand when we think in concrete scenarios.
By understanding our dependency graph, reviewing dependencies and their updates with AI as a practical support tool, reducing unnecessary dependencies, and preparing for compromise, we can continue to benefit from the Flutter package ecosystem while reducing unnecessary security risks.



