OWASP Top 10 For Flutter – M8: Security Misconfiguration in Flutter & Dart

Welcome back to our deep dive into the OWASP Mobile Top 10 for Flutter developersarrow-up-right. OWASP (Open Worldwide Application Security Project) maintains this industry-standard risk rankingarrow-up-right to help developers prioritize mobile security efforts.

In earlier parts, we tackled:

Each is a critical piece of the mobile security puzzle.

In this eighth article, we focus on M8: Security Misconfigurationarrow-up-right, a vulnerability that often hides in plain sight. Unlike the complex code vulnerabilities we discussed earlier, misconfigurations are usually simple oversights. A flag left enabled. A permission not restricted. A default setting unchanged. These mistakes are easy to make. They also go unnoticed.

This hits Flutter devs hard. We ship multiple configuration layers at once. Dart code, Android’s AndroidManifest.xml, iOS’s Info.plist, Gradle files, Xcode settings, and more. Each layer adds its own failure modes. A review that only covers Dart misses a big chunk of the attack surface.

I’ve reviewed many Flutter projects. Security misconfigurations are among the most common issues I see. The good news is simple. They’re also among the easiest to fix.

Let’s break down what security misconfiguration means for Flutter apps. Let’s harden your configs.

circle-check

Understanding Security Misconfiguration

What is Security Misconfiguration?

Security misconfiguration occurs when security settings are defined, implemented, or maintained incorrectly. Think of it like leaving your front door unlocked. It’s not a flaw in the lock itself. It’s how you’re using (or not using) the security mechanism.

In mobile apps, security misconfiguration takes many forms:

  • Insecure default settings left unchanged

  • Debug features enabled in production

  • Unnecessary permissions requested

  • Exposed components (activities, services, content providers)

  • Weak network security configurations

  • Improper backup settings that leak data

  • Missing security headers or policies

circle-info

The following diagram illustrates the various categories of security misconfiguration:

Why Flutter Developers Must Pay Extra Attention

Here's a reality check for Flutter developers: you're responsible for configuring two completely different platforms correctly, plus managing Dart-level configurations. A single misconfiguration on either Android or iOS can compromise your entire app, regardless of how secure your Dart code is.

Let me break down what you’re actually managing:

Configuration layer
Android
iOS
Flutter/Dart

App manifest

AndroidManifest.xml

Info.plist

N/A

Build config

build.gradle

Xcode build settings

pubspec.yaml

Network security

network_security_config.xml

ATS in Info.plist

HTTP client config

Permissions

<uses-permission>

NS…UsageDescription

permission_handler

Signing

Keystore + Gradle

Provisioning profiles

N/A

That's a lot of surface area to secure. And here's the thing: most Flutter tutorials and documentation focus on the Dart layer. The platform-specific security configurations are often an afterthought, or worse, copy-pasted from StackOverflow without understanding what they do.

Business and Technical Impact

What can actually go wrong with security misconfigurations? According to OWASP, the consequences include:

  • Unauthorized data access: Attackers exploit exposed components and read user data.

  • Account hijacking: Weak session or auth configs let attackers take over accounts.

  • Data breaches: Backups extracted from unprotected storage end up leaked or sold.

  • Compliance violations: GDPRarrow-up-right, HIPAAarrow-up-right, and PCI-DSSarrow-up-right penalties can be substantial.

  • Reputation damage: Public disclosure erodes user trust.

So let's get into the specific misconfigurations to watch out for.

Android Security Misconfigurations in Flutter

Android configuration is particularly prone to security issues because of its flexibility and the sheer number of settings available. Let's walk through the most critical misconfigurations I encounter when reviewing Flutter apps.

1. Debug Mode in Production

This one is the classic "oops" moment. Releasing an app with android:debuggable="true" is like shipping a car with the hood permanently unlatched. Anyone can look inside.

What it looks like:

Why this is catastrophic:

When debug mode is enabled, attackers can:

  • Attach debuggers to your running app and step through code

  • Inspect and modify memory contents at runtime

  • Set breakpoints to bypass security checks

  • Extract sensitive data directly from memory

I've seen production apps with this flag enabled—it happens more often than you'd think, especially when developers manually override build configurations for testing and forget to revert.

The good news: Flutter's build system handles this correctly by default. The debuggable flag is set to true only in debug builds. However, you should still verify your release builds, especially if you've ever modified build configurations manually:

Expected output for a correctly configured release build:

If you see value='true' or debuggable="true", stop and fix your build configuration before shipping.

Explicitly ensure debug is disabled in release builds:

For extra safety, explicitly set the flag in your Gradle build configurationarrow-up-right. Belt and suspenders approach:

2. Backup Configuration Leaks

Here's a subtle one that catches many developers off guard. By default, Android backs up your app data to Google Drivearrow-up-right. Sounds helpful, right? The problem is that this backup might include sensitive information: authentication tokens, cached user data, and encryption keys stored in SharedPreferences.

The problem in action:

How attackers exploit this:

The following sequence shows how backup extraction works in practice:

An attacker with brief physical access to a device (think: borrowed phone, lost device, or malicious app with ADB access) can extract your entire app's data:

Output:

Nuance on flutter_secure_storage: If you configure it with AndroidOptions(encryptedSharedPreferences: true), the encryption key lives in the Android Keystorearrow-up-right and is not included in backups. The extracted XML will contain ciphertext that is useless without the key.

However, when using the default configuration (or older package versions < 5.0), the key material may be stored alongside the data. That makes backup extraction a real threat.

circle-exclamation

The fix:

Explicitly disable backups. Or, if you need selective backup for user convenience, carefully exclude sensitive files. See the <application> element docsarrow-up-right for the full attribute reference:

For Android 12+ (API 31+), create android/app/src/main/res/xml/data_extraction_rules.xml:

For older Android versions, create android/app/src/main/res/xml/backup_rules.xml:

And reference it:

3. Cleartext Traffic (HTTP)

In 2026, there's really no excuse for allowing unencrypted HTTP traffic. Yet I still see Flutter apps with this misconfiguration regularly, usually because developers enabled it "temporarily" for local testing and forgot to disable it.

The problematic setting:

When this flag is enabled, your app can communicate over unencrypted HTTP. Any attacker on the same network (coffee shop WiFi, compromised router, or hostile network) can see everything your app sends and receives: authentication tokens, personal data, everything.

The fix:

Create a proper network security configurationarrow-up-right. This approach gives you fine-grained control and makes your intent explicit. Create android/app/src/main/res/xml/network_security_config.xml:

Reference it in your manifest:

4. Certificate Pinning

If your app communicates with sensitive backend services (especially for financial or health data), consider adding certificate pinning. This prevents attackers from intercepting traffic even if they manage to install a rogue CA (Certificate Authority) certificate on the device.

Here's how to add certificate pinning in your network security config:

Generate pin hash from your server's certificate:

Output:

Use this Base64 string as the <pin> value in your config.

Important: Always include at least two pins (a backup), and set an expiration date. If your certificate rotates and you only have one pin, your app will stop working.

5. Exported Components

This is one of the most frequently overlooked security issues in Android development. Components marked as exported="true"arrow-up-right can be accessed by any other app on the device. This includes activities, services, broadcast receivers, and content providers.

Since Android 12 (API 31), the android:exported attribute is required for every component that declares an <intent-filter>. Builds targeting

circle-exclamation

The problem:

Why this matters for Flutter:

Flutter's main activity is often exported for deep linking. While this is necessary for handling deep links, you need to be extremely careful about validating any data that comes through:

The Fix:

  1. Set exported="false" for internal components

  2. Add permission requirements for exported components

  3. Validate all inputs from intent data

Validate deep link data in Flutter:

The following flow shows how every incoming deep link should be validated before the app acts on it:

Example output when processing deep links:

6. Excessive Permissions

This is one of those areas where I see a lot of "it works, ship it" mentality. Requesting unnecessary permissions increases your app's attack surface and violates the principle of least privilege. Every permission you request is a potential vector for abuse—by malicious code in your dependencies, by attackers who compromise your app, or by you accidentally leaking data you shouldn't have access to.

I've reviewed apps that request camera, contacts, location, SMS, microphone, and storage access... for a simple note-taking app. Each of those permissions opens a door. The more doors you open, the more you have to defend.

The problematic approach:

The better approach:

Only request what you actually need, and request it at runtime with proper explanation. Users have become increasingly permission-aware, they'll uninstall your app if you ask for things that don't make sense.

Request permissions properly in Flutter:

Console output (example flow):

iOS Security Misconfigurations in Flutter

Now let's shift our attention to iOS. While iOS has a reputation for being more secure than Android (and in some ways it is), it's not immune to misconfiguration issues. In fact, some iOS-specific security features are so easy to disable that developers do it without realizing the implications.

1. App Transport Security (ATS)

App Transport Securityarrow-up-right is Apple's way of enforcing secure network connections. It requires HTTPS by default, with modern TLS versions and cipher suites. It's a fantastic security feature that protects your users from man-in-the-middle attacks.

And yet, one of the most common Stack Overflow answers to "my app can't connect to my server" is "just disable ATS." This is terrible advice that has made its way into countless production apps.

The problematic configuration:

When you add this to your Info.plist, you're telling iOS "I don't care about network security, let me talk to any server over any protocol." Your app can then be tricked into communicating with malicious servers, or have its traffic intercepted on insecure networks.

The secure approach:

If you absolutely must connect to a server that doesn't support HTTPS (and please, pressure them to fix this), use targeted exceptions rather than disabling ATS entirely:

Best practice: Don't disable ATS at all. If you're hitting HTTP endpoints, the right fix is to update those servers to use HTTPS, not to weaken your app's security. Let's Encryptarrow-up-right makes free certificates available to everyone—there's no excuse for HTTP anymore.

2. Missing Privacy Usage Descriptions

iOS requires you to explain why your app needs access to sensitive features—camera, location, contacts, etc. This isn't just a formality; it's a privacy protection that helps users make informed decisions about what they're allowing.

Here's the fun part: if you request a permission without providing the corresponding usage description, your app doesn't show a generic dialog. It crashes. Immediately. And this will definitely get caught in App Store review if you somehow missed it in testing.

What happens without descriptions:

No graceful error handling, no fallback—just a crash. So make sure you add all necessary descriptions to ios/Runner/Info.plist:

3. Insecure Keychain Configuration

The iOS Keychainarrow-up-right is one of the most secure places to store sensitive data on the device, but only if you configure it correctly. The Keychain has different accessibility levels that determine when your data can be accessed, and choosing the wrong one can leave your users' data exposed.

The problematic approach (in native code):

Using kSecAttrAccessibleAlways means the data can be read even when the device is locked. If someone steals the phone, they can potentially extract this data without knowing the passcode.

The secure approach in Flutter (using flutter_secure_storage):

The good news is that flutter_secure_storage gives you control over Keychain accessibility. Here's how to configure it properly:

Output:

Understanding Keychain Accessibility Options:

Choosing the right accessibility level requires balancing security with user experience. Here’s a quick breakdown:

Option
When accessible
Synced to iCloud

passcode

Only when unlocked with passcode

No

unlocked

When device is unlocked

Yes

unlocked_this_device

When device is unlocked

No

first_unlock

After first unlock until restart

Yes

first_unlock_this_device

After first unlock until restart

No

always

Always (deprecated since iOS 12)

Yes

always_this_device

Always (deprecated since iOS 12)

No

kSecAttrAccessibleAlways and kSecAttrAccessibleAlwaysThisDeviceOnly have been deprecated since iOS 12arrow-up-right. Any code still targeting these values will trigger App Store review warnings.

Migrate to first_unlock_this_device or unlocked_this_device.

For most sensitive data like auth tokens, I recommendfirst_unlock_this_device. This provides strong protection while still allowing background operations (like push notification handling) to access the data after the user has unlocked their device at least once since reboot.

4. URL Scheme Hijacking

Custom URL schemes are a convenient way to open your app from links, but they come with a significant security risk: any app can register the same URL scheme.If a malicious app registers your scheme first (or the user installs a malicious app that does), it can intercept links meant for your app.

The problematic approach:

There's nothing stopping a bad actor from creating an app that also registers the myapp:// scheme. When a user taps a link, iOS might open their malicious app instead of yours, and they could capture OAuth callbacks, payment confirmations, or other sensitive deep link data.

The secure solution:Universal Linksarrow-up-right

Instead of custom URL schemes, use Universal Links. These are tied to a domain you own and cryptographically verified by Apple. No other app can intercept them.

You'll also need to host an apple-app-site-association file at your domain's root (served over HTTPS, no redirect):

This file must be served athttps://yourapp.com/.well-known/apple-app-site-association. Apple will fetch and verify this file when your app is installed, ensuring only your app can handle links to your domain.

Flutter/Dart Level Misconfigurations

Beyond Android and iOS platform configurations, there are security considerations in your Dart code as well. Let's look at some common issues.

1. Debug Mode Detection Bypass

Your app should behave differently in debug versus release mode. Sensitive endpoints, verbose logging, and development shortcuts should never be available in production builds. Here's a pattern I use to manage this:

Output (debug build):

Output (release build):

2. Insecure HTTP Client Configuration

One of the most dangerous patterns I see in Flutter codebases is disabling certificate validation "to make development easier." The problem is, this often gets left in production code, or worse, it's deliberately added to bypass certificate errors without understanding why those errors exist.

Output:

Why badCertificateCallback and not a plugin? Dart's HttpClient gives you direct access to the TLS handshake. For package:http or Dio users, you can pass a custom SecurityContext or HttpClient to get the same effect.The trade-off: native-level pinning (e.g., network_security_config.xml) fires before Dart code runs, so it blocks non-Dart traffic too.

If you're seeing certificate errors, the solution is to fix the server's certificate configuration, not to tell your app to ignore security errors.

3. Insecure SharedPreferences Usage

SharedPreferences is incredibly convenient for storing small bits of data, but it's not encrypted. On a rooted or jailbroken device, anyone can read its contents. I've seen apps store auth tokens, API keys, and even passwords inSharedPreferences. Don't be one of those apps.

Output:

The rule is simple: if you wouldn't want it on the front page of a newspaper,don't put it in SharedPreferences.

4. Logging Sensitive Information

Logging is essential for debugging, but it's also a common source of security leaks. I've seen production apps that logged full user credentials, API keys, and payment information—all visible to anyone with access to device logs.

Debug-mode output:

Even in debug mode, be thoughtful about what you log. Screenshots and screen recordings can capture log output, and you never know where those might end up.

Security Configuration Audit Checklist

Here's a checklist you can use to audit your Flutter app's security configuration. I recommend going through this checklist before every major release, it's saved me from shipping vulnerable code more than once.

Android Configuration

AndroidManifest.xml

  • android:debuggable is NOT set to true (or is absent)

  • android:allowBackup="false" or properly configured backup rules

  • android:usesCleartextTraffic="false"

  • android:networkSecurityConfig points to secure config

  • No unnecessary exported="true" on components

  • Exported components have permission requirements

  • Only necessary permissions are declared

  • android:hasFragileUserData="false"arrow-up-right for sensitive apps (prevents data retention on uninstall)

build.gradle

  • minifyEnabled true for release builds

  • shrinkResources true for release builds

  • Release signing configured properly

  • debuggable false explicitly set for release

network_security_config.xml

  • cleartextTrafficPermitted="false" as default

  • Certificate pinning for sensitive endpoints

  • No overly broad exceptions

  • debug-overrides only for debugging

iOS Configuration

Info.plist

  • No NSAllowsArbitraryLoads set to true

  • Specific domain exceptions only if necessary

  • All privacy usage descriptions present and accurate

  • URL schemes are documented and necessary

Entitlements

  • Only necessary entitlements enabled

  • Associated Domains configured for Universal Links

  • Keychain sharing groups are appropriate

Xcode Settings

  • Code signing configured properly

  • Debug builds don't use release certificates

  • Bitcode settings appropriate for your needs

Dart/Flutter Configuration

Code configuration

  • No hardcoded secrets or API keys

  • Proper environment configuration

  • Secure storage used for sensitive data

  • HTTP client validates certificates

  • Deep links are validated before processing

  • Logging doesn't include PII in release

  • Debug features disabled in release mode

Dependencies

  • All packages are from trusted sources

  • No known vulnerabilities in dependencies

  • Minimum necessary permissions for plugins

Automated Security Scanning

Manual checklists are great, but humans make mistakes, especially when we're rushing to meet deadlines. That's why I strongly recommend implementing automated security checks in your CI/CD pipeline. Here's a GitHub Actions workflow that catches common misconfigurations before they reach production:

circle-info

Note: you may adjust the following config a bit to match your project setup!

This workflow won't catch everything, but it will catch the most common and dangerous misconfigurations. I've seen this simple check prevent several "oops, I shipped with debug mode enabled" incidents.

Runtime Configuration Validation

For an extra layer of protection, you can add runtime checks that validate your app's configuration at startup. This is particularly useful for catching issues that might slip through static analysis:

Output (debug, non-production package):

Output (release, production package accidentally in debug):

Conclusion

Security misconfiguration is one of the most common vulnerabilities in mobile apps, and honestly, one of the most frustrating because it's so preventable.Unlike complex vulnerabilities that require deep security expertise to understand, misconfigurations are simply settings that someone forgot to change or didn't realize were insecure.

As Flutter developers, we have the added responsibility of managing configurations across multiple platforms simultaneously. Every time you configure Android's AndroidManifest.xml, you need to remember to check iOS'sInfo.plist too. Every permission you add to Gradle needs a corresponding entry in Xcode. It's a lot to keep track of.

Here's what I want you to take away from this article:

  1. Disable debug features in production builds. Verify before release.

  2. Disable or restrict backups so attackers can’t extract user data.

  3. Enforce HTTPS everywhere. Add pinning for high-security cases.

  4. Minimize permissions to what you need. Users notice. Attackers too.

  5. Protect exported components with permissions. Validate deep link inputs.

  6. Automate security checks in CI/CD. Don’t rely on manual review.

  7. Audit regularly. Configurations drift as features ship.

The key insight is that a single misconfiguration can undermine all your other security efforts. You can implement perfect encryption, bulletproof authentication, and thorough input validation—but if you leave android:debuggable="true" in your release build, attackers can bypass it all.

Make configuration review a standard part of your development process. Add it toyour pull request checklist. Run automated scans on every commit. It's boringwork, but it's the kind of boring work that prevents headlines.

In the next article, we'll explore M9: Insecure Data Storage, where we'll dive deep into how to properly store sensitive data on mobile devices. Spoiler alert: it's more nuanced than just "use encrypted storage."

Additional Resources

Stay secure, Flutter engineers!

Last updated

Was this helpful?