Flutter Security: Avoiding the Oops Moments


Since I started working with Flutter in 2018, I have noticed that most Flutter developers build and ship their apps with a clean and beautiful UI, fancy animations, and a sense of accomplishment. What they forget to ship with is the realisation that their app is leaking sensitive user data, exposing API keys, or opening the doors to man-in-the-middle MITM attacks. The worst part is that you might never know it's happening.
Security in mobile apps is often an afterthought, and security issues rarely announce themselves. There is no crash, no red (or grey) screen, no visible bug, no errors in the console, and no angry customer calling you. Everything works just fine until someone starts messing with your APK, runs a proxy server, or reverse engineers your app in minutes.
This Article Is based on a talk I gave about the most common Flutter security pitfalls (“Oops moments”) that developers often overlook. We will go through real-world risks, how they happen, and how they can be potentially prevented before they cost you.
“Your Flutter app is beautiful. It shouldn't also be an all-you-can-eat buffet for hackers.”
Below are some of the common mistakes or “Oops” that we should try to avoid when building Flutter apps.
🛑 Oops #1: Storing Sensitive Data Insecurely
No,
SharedPreferences
is not a safe place for tokens.
This is one of the most common “oops” moments for Flutter developers: storing sensitive data such as access tokens, refresh tokens, email addresses, or even user preferences in plain text using SharedPreferences
or similar unencrypted local storage.
It’s convenient, sure. But also dangerously exposed.
On a rooted Android device or a jailbroken iOS device, accessing the app's internal storage is easy. A malicious actor could extract your app’s storage folder, peek into the preferences file, and retrieve anything stored in plain text, like user data, auth tokens, etc.
And guess what?
They don’t even need to touch your UI to do it.
🚧 What Not to Do
final prefs = await SharedPreferences.getInstance();
await prefs.setString('access_token', 'abc123secret');
✅ What to Do Instead
Use libraries like flutter_secure_storage
, which stores values in encrypted form using the platform's secure keystore/keychain.
On Android, it uses the EncryptedSharedPreferences or Android Keystore
On iOS, it uses the Keychain Services
final FlutterSecureStorage secureStorage = FlutterSecureStorage();
Future<void> saveAccessToken(String accessToken) async {
await secureStorage.write(key: 'access_token', value: accessToken);
}
Future<String?> getAccessToken() async {
return await secureStorage.read(key: 'access_token');
}
💡 Bonus Tips
Only store what's necessary on the device.
If you're storing tokens, set an expiration and rotate them.
Don’t store anything that can be used to authenticate a user without proper checks.
🔓 Oops #2: Hardcoding Secrets – The Open Backdoor
Just because you hid it in your code doesn’t mean it’s safe.
One of the most dangerous but oddly common mistakes among Flutter Developers is hardcoding secrets like API Keys, credentials, or private endpoints directly into the app's source code or config files.
It might feel safe during development, or even in production, when hidden behind .env
file.
But here's the uncomfortable truth:
Anything inside your built APK or IPA can and will be extracted.
Flutter apps are compiled, but they are not opaque. Tools like jadx, apktool, or MobSF can easily decompile APKs and scan for strings, constants, or obfuscated logic. Even with obfuscation, a determined individual can still find sensitive keys if they are included in the binary.
🚩 Common Mistakes
const String apiKey = 'sk_live_abc123...';
const String endpoint = 'https://my-secret-api.com';
Or worse, placing them in .env
and thinking they’re private.
✅ What to Do Instead
Never store long-term secrets or private keys in the client.
Use build-time variables via
--dart-define
.Move secrets to a backend service and expose only short-lived or scoped tokens.
If using public APIs (like Stripe or Google Maps), always use restricted keys that:
Limit origin (domain/IP/app hash)
Limit scopes or permissions
Rotates regularly
🧩 Oops #3: Shipping Readable Code
You’re not just publishing your app, you’re publishing your logic.
Most Flutter devs don’t realize this, but when you ship your app in release mode without obfuscation, you’re also shipping:
Function names
Variable names
Class names
Logical structures
All of which can be decompiled and read using tools like jadx
, apktool
, or MobSF.
Even without access to your source code, an attacker can reverse engineer your business logic, premium unlock conditions, offline validation steps, or even manipulate what your app shows and when.
🕳️ What a Decompiler Sees (Without Obfuscation)
class PremiumManager {
bool isPremiumUser = false;
bool hasAccessToFeature(String feature) {
if (feature == 'advanced') {
return isPremiumUser;
}
return true;
}
}
To you, this is simple access control.
To an attacker, it’s a glowing invitation to patch the app and change:
return isPremiumUser;
to
return true;
Now every user is premium.
✅ What to Do Instead
Use Flutter’s built-in obfuscation options:
flutter build apk \
--release \
--obfuscate \
--split-debug-info=/<safe/output/dir>
--obfuscate
: Renames identifiers in Dart code to unreadable symbols (e.g.,a.b()
instead ofPremiumManager.hasAccessToFeature()
)--split-debug-info
: Separates debug symbols into a folder so stack traces can still be symbolicated later. Always keep these files safe.
🕶 Obfuscated Output (After --obfuscate
)
class a {
bool b = false;
bool c(String d) {
if (d == 'advanced') {
return b;
}
return true;
}
}
This is what an attacker sees after decompiling your obfuscated app. They’ll have to guess what a
, b
, and c
do. If your logic is more complex, this becomes a serious deterrent.
📡 Oops #4: Chatting Insecurely Over the Network
If your app talks over HTTP, anyone nearby can listen.
It's easy to overlook the vulnerability of your app's network traffic, particularly on mobile devices connected to public Wi-Fi, emulators, or rooted phones. Many Flutter apps continue to make API calls using plain HTTP or do not properly validate HTTPS connections. This allows an attacker on the same network to intercept, alter, and replay your network traffic using tools like Charles Proxy, Wireshark, or mitmproxy.
In other words:
Your app could be sending private data through a megaphone.
🚩 Common Issues
Making calls over
http://
instead ofhttps://
Using certificate pinning incorrectly—or not at all
Accepting all certificates by default (bad debug practice)
Skipping TLS altogether for "internal" APIs
💣 Real Threats
Packet sniffing (e.g., Wireshark): Captures raw traffic on open or weak networks
Man-in-the-middle attacks (e.g., mitmproxy): A Fake server intercepts requests and spoofs responses
Session hijacking: Attackers replay intercepted tokens or cookies
✅ What to Do
Enforce HTTPS (Always)
Use https://
URLs and make sure your backend has a valid SSL certificate.
final response = await http.get(Uri.parse('https://api.example.com/data'));
Block insecure traffic at the framework level
For Android:
<!-- android/app/src/main/AndroidManifest.xml -->
<application
android:usesCleartextTraffic="false" ... />
For iOS:
<!-- ios/Runner/Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
</dict>
🔐 Go Further with SSL Pinning
Even HTTPS isn’t foolproof if a fake certificate is trusted by the device (or installed by a malicious rootkit). With SSL pinning, your app only trusts a specific server certificate or public key.
Packages like http
with a custom client or dio
let you implement certificate validation manually.
📦 Oops #5: Trusting Dependencies Blindly
What happens in your pubspec doesn't stay in your app.
Flutter’s ecosystem is a major advantage, but it can also be a hidden risk. When you execute flutter pub get
, you might be importing numerous transitive dependencies that you haven't reviewed, audited, or even known about. These packages operate within your app with the same level of access as your own code.
That means:
A package can access sensitive data
A malicious update can introduce vulnerabilities
Even inactive or abandoned packages could be hijacked by bad actors
🚩 Real-World Incidents
ua-parser-js
npm hijack (2021): This widely used package was hijacked after the maintainer’s npm credentials were compromised. A malicious update was published, injecting crypto miners and password stealers into users’ systems. It was downloaded hundreds of thousands of times before being caught.PyPI
ctx
andphpjwt
malware (2022): Python’s package index was targeted by attackers uploading typo-squatted packages that mimicked popular libraries but included credential stealers.Android apps shipping malware via SDKs: There have been multiple cases (e.g., Mintegral SDK) where ad SDKs embedded in thousands of mobile apps were later found to be logging user data or bypassing App Store policies.
✅ What to Do
Audit your dependencies regularly
- Use
pub deps --style=compact
to inspect your dependency tree.
- Use
Pin your versions
- Avoid overly loose constraints (
any
,^1.0.0
) when possible.
- Avoid overly loose constraints (
Use vetted packages
- Prefer packages maintained by trusted organizations or community-recognized contributors.
Use
dependency_overrides
carefully- Only override when you're certain of what's inside.
Security mistakes don’t always show up as glaring errors. Sometimes, the cracks are smaller; habits we overlook, shortcuts we justify, or defaults we never question. Before we wrap up, here are a few extra pitfalls worth keeping an eye on as you scale your app.
🔒 Bonus Pitfalls Worth Mentioning
Input validation is still your first line of defense. Never assume user input is clean; always sanitize and validate both on the client and server.
Apply the principle of least privilege wherever possible. Don’t request permissions your app doesn’t strictly need, and don’t give background services access to everything by default.
Finally, follow secure coding best practices: avoid excessive logging of sensitive data, avoid storing secrets in logs or stack traces, and keep your dependencies and build tools up to date.
These may seem small, but security is rarely compromised by one big failure; more often, it's a series of tiny cracks. Seal them early.
Security in Flutter is not about being overly cautious, it's about being prepared. Most vulnerabilities won't affect your app until someone scrutinizes it. Once your APK is released, you don't get another chance to secure it. Fortunately, many significant risks can be mitigated with a few deliberate actions during development and release. Review your data storage, remove secrets from your codebase, secure your network communications, and consider every third-party package as a potential risk, because it is. Your goal isn't to create an impenetrable fortress, but to make your app secure enough that potential attackers move on to an easier target.
Thank you for reading.
If you found this helpful, don’t forget to clap, share, comment, and follow to explore more content around Flutter.
#flutter #flutter-app
Subscribe to my newsletter
Read articles from Festus Babajide Olusegun directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Festus Babajide Olusegun
Festus Babajide Olusegun
Festus is a Google Developer Expert (GDE) in Flutter and a software engineer who has spent most of his time working with Flutter since discovering it in 2018. He also has experience working with native Android and iOS development.