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 of PremiumManager.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 of https://

  • 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 and phpjwt 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.
  • Pin your versions

    • Avoid overly loose constraints (any, ^1.0.0) when possible.
  • 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

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