{"id":100050,"date":"2026-11-12T16:23:28","date_gmt":"2026-11-12T16:23:28","guid":{"rendered":"https:\/\/www.nandee.ai\/blog\/flutter-security-checklist\/"},"modified":"2026-11-12T16:23:28","modified_gmt":"2026-11-12T16:23:28","slug":"flutter-security-checklist","status":"publish","type":"post","link":"https:\/\/www.nandee.ai\/blog\/flutter-security-checklist\/","title":{"rendered":"Flutter Security Checklist: 12 Vulnerabilities Scanners Often Miss"},"content":{"rendered":"<p>The scan report on the Flutter app came back clean. A week later, the pentester&#8217;s report did not. Their first finding took about five minutes: unzip the APK, open <code>assets\/flutter_assets\/.env<\/code>, and read the production API keys. The second was a deep link that could load any website into an in-app WebView with access to the user&#8217;s session.<\/p>\n<p>Neither issue was exotic. Both were missed because the tooling was looking for problems in the places native apps keep them, not where Flutter apps do.<\/p>\n<p>Flutter lets a small team ship to iOS and Android from one codebase, which is a big reason fintech, health tech, and consumer startups choose it. But it also changes where vulnerabilities hide. This checklist covers 12 issues that show up repeatedly in Flutter security reviews, why tools often overlook them, and how to fix each one.<\/p>\n<h2>Why Flutter Apps Are Harder to Scan<\/h2>\n<p>Most mobile security tooling grew up analyzing Java and Kotlin bytecode on Android and Objective-C or Swift on iOS. Flutter works differently.<\/p>\n<p>In release builds, Dart code is compiled ahead of time into a native library (<code>libapp.so<\/code> on Android, the App framework on iOS). Tools that expect bytecode or symbol-rich binaries see far less of your actual logic. The interesting code is in Dart, while the platform projects in <code>android\/<\/code> and <code>ios\/<\/code> look almost empty.<\/p>\n<p>Dynamic testing is harder too. Dart&#8217;s HTTP stack does not use the device&#8217;s proxy settings by default, and it performs its own TLS verification against the platform&#8217;s built-in certificate authorities instead of going through the platform&#8217;s networking libraries. That means user-installed certificates and Android&#8217;s network security config, both of which testers and tools rely on, do not apply to Dart traffic by default. A dynamic scan that cannot see the traffic cannot test it.<\/p>\n<p>Finally, risk is split across three layers: Dart code, native platform code and configuration, and plugins that bundle native dependencies of their own. A tool that understands one layer well can still miss the other two.<\/p>\n<p>OWASP MASVS and MASTG apply to Flutter apps exactly as they do to native ones. The checklist below maps each issue to them.<\/p>\n<h2>The 12-Item Flutter Security Checklist<\/h2>\n<h3>1. Secrets in Dart code and bundled assets<\/h3>\n<p>Packages like <code>flutter_dotenv<\/code> load a <code>.env<\/code> file that you must list under <code>assets<\/code> in <code>pubspec.yaml<\/code>. That file ships inside the app, readable by anyone. Constants in Dart code and values passed with <code>--dart-define<\/code> end up in the compiled binary as well.<\/p>\n<pre><code class=\"language-dart\">\/\/ Vulnerable: CWE-798. The .env asset ships in the APK and IPA.\nawait dotenv.load(fileName: '.env');\nfinal paymentsKey = dotenv.env['PAYMENTS_SECRET_KEY'];<\/code><\/pre>\n<p>The fix is architectural: <a href=\"https:\/\/www.nandee.ai\/blog\/m1-improper-credential-usage-examples-fixes\/\">secret keys live on your server<\/a>, and the app calls your backend, which performs the privileged action. Only keys designed to be public, such as publishable client keys, belong in the app. Rotate anything that has already shipped. Obfuscation does not change this, because a secret that is hard to find is still in the binary. (MASVS-CRYPTO-2)<\/p>\n<h3>2. Tokens in shared_preferences<\/h3>\n<p><code>shared_preferences<\/code> writes to <code>NSUserDefaults<\/code> on iOS and <code>SharedPreferences<\/code> on Android, neither of which is encrypted. <a href=\"https:\/\/www.nandee.ai\/blog\/insecure-data-storage-android-ios\/\">Store tokens and sensitive data<\/a> with <code>flutter_secure_storage<\/code>, which uses the Keychain on iOS and Keystore-backed encryption on Android. (MASVS-STORAGE-1, CWE-312)<\/p>\n<h3>3. Logs that survive release builds<\/h3>\n<p><code>debugPrint<\/code> is not stripped from release builds, and neither is <code>print<\/code>. Logging interceptors in HTTP clients often dump full request and response bodies. Guard logging behind <code>kDebugMode<\/code>, never log bodies or auth headers, and check what reaches your crash reporting tool. (MASVS-STORAGE-2, CWE-532)<\/p>\n<h3>4. Certificate validation disabled &#8220;temporarily&#8221;<\/h3>\n<p>A <code>badCertificateCallback<\/code> that returns <code>true<\/code>, added to reach a staging server, accepts every certificate in production. Remove it. If staging uses a private certificate authority, give staging a publicly trusted certificate instead, or, in debug builds only, add that one CA to a Dart <code>SecurityContext<\/code> and pass it to your HTTP client. (Android&#8217;s network security config will not help here, because Dart does its own TLS verification.) Then add a CI rule that fails the build if the callback appears. (MASVS-NETWORK-1, CWE-295)<\/p>\n<h3>5. Cleartext traffic allowed in platform config<\/h3>\n<p>Flutter developers rarely open <code>AndroidManifest.xml<\/code> or <code>Info.plist<\/code>, so settings like <code>android:usesCleartextTraffic=\"true\"<\/code> or a broad <code>NSAllowsArbitraryLoads<\/code> exception added during development often survive to production. Review both files before every release. (MASVS-NETWORK-1, CWE-319)<\/p>\n<h3>6. Biometrics as a yes or no answer<\/h3>\n<p>A common pattern calls <code>local_auth<\/code>, gets back <code>true<\/code>, and unlocks the app:<\/p>\n<pre><code class=\"language-dart\">\/\/ Weak: the result is just a boolean in app memory.\nfinal ok = await auth.authenticate(localizedReason: 'Unlock your account');\nif (ok) showAccountScreen();<\/code><\/pre>\n<p>On a compromised device, that boolean can be bypassed with runtime instrumentation, because nothing cryptographic depends on it. For sensitive actions, bind biometrics to a key or secret stored in the Keychain or Keystore with user authentication required, so the data cannot be unlocked without a successful biometric check. (MASVS-AUTH-2)<\/p>\n<h3>7. Deep links that trust their parameters<\/h3>\n<p>Routes registered for deep links are reachable from any website, message, or app. A route that takes a URL, a file path, or an account ID from the link and acts on it without validation is an open door. This one appears in the worked example below. (MASVS-PLATFORM-1, CWE-939)<\/p>\n<h3>8. WebViews with JavaScript channels<\/h3>\n<p><code>webview_flutter<\/code> lets you register a <code>JavaScriptChannel<\/code> so web content can call into Dart. If that WebView can load untrusted content, any page it loads can call those handlers. Restrict navigation to an allowlist, and never expose tokens or sensitive actions through a channel. (MASVS-PLATFORM-2, CWE-749)<\/p>\n<h3>9. Native code behind platform channels<\/h3>\n<p>Platform channels and plugins move logic into Kotlin, Java, Swift, or Objective-C. That native code is often reviewed less carefully than the Dart side, by people less familiar with it, and Dart-focused tools may not analyze it at all. Review native handlers for the same issues you check in Dart: insecure storage, file paths from untrusted input, and logging. (MASVS-CODE-4)<\/p>\n<h3>10. Neglected Android scaffolding<\/h3>\n<p>The <code>android\/<\/code> folder generated by <code>flutter create<\/code> is rarely revisited. Check for <a href=\"https:\/\/www.nandee.ai\/blog\/android-app-security\/\">exported components<\/a> that do not need to be exported, backup settings that include sensitive files (<code>android:allowBackup<\/code>, <code>dataExtractionRules<\/code>), and permissions added by plugins in the merged manifest. (MASVS-PLATFORM-1, CWE-926)<\/p>\n<h3>11. Sensitive screens exposed to screenshots and the app switcher<\/h3>\n<p>Both platforms capture a snapshot of your app for the app switcher, and users or other apps can capture the screen. For screens showing account numbers, health data, or one-time codes, set <code>FLAG_SECURE<\/code> on Android (via a small platform channel or a maintained plugin) and obscure content when the app moves to the background on iOS. (MASVS-PLATFORM-3)<\/p>\n<h3>12. Dependencies below pubspec.lock<\/h3>\n<p><code>pubspec.lock<\/code> shows your Dart packages, but many plugins pull in native dependencies through Gradle and CocoaPods. An SCA check that reads only the Dart lockfile misses those. Audit pub packages for known vulnerabilities and maintenance status, and include the resolved Android and iOS dependency trees. <a href=\"https:\/\/www.nandee.ai\/blog\/third-party-sdk-risk-mobile-apps\/\">Remove plugins you no longer use<\/a>. (MASVS-CODE-3, CWE-1395)<\/p>\n<h2>Worked Example: A Deep Link Into a WebView<\/h2>\n<p>Here is how items 7 and 8 combine in practice, using <code>go_router<\/code>:<\/p>\n<pre><code class=\"language-dart\">\/\/ Vulnerable: any link like myapp:\/\/app\/help?url=https:\/\/attacker.example\n\/\/ opens attacker content in a WebView that exposes a session channel.\nGoRoute(\n  path: '\/help',\n  builder: (context, state) =&gt;\n      HelpWebView(url: state.uri.queryParameters['url']!),\n),<\/code><\/pre>\n<p>And inside <code>HelpWebView<\/code>, a channel that hands the session token to any page:<\/p>\n<pre><code class=\"language-dart\">controller\n  ..setJavaScriptMode(JavaScriptMode.unrestricted)\n  ..addJavaScriptChannel('Session', onMessageReceived: (_) {\n    controller.runJavaScript(\"receiveToken('${auth.token}')\");\n  })\n  ..loadRequest(Uri.parse(url));<\/code><\/pre>\n<p>The fix validates the link, restricts navigation, and removes the token channel:<\/p>\n<pre><code class=\"language-dart\">const _allowedHosts = {'help.myapp.com'};\n\nbool _isAllowed(Uri uri) =&gt; uri.scheme == 'https' &amp;&amp; _allowedHosts.contains(uri.host);\n\nGoRoute(\n  path: '\/help',\n  redirect: (context, state) {\n    final raw = state.uri.queryParameters['url'];\n    final target = raw == null ? null : Uri.tryParse(raw);\n    return (target != null &amp;&amp; _isAllowed(target)) ? null : '\/home';\n  },\n  builder: (context, state) =&gt;\n      HelpWebView(url: Uri.parse(state.uri.queryParameters['url']!)),\n),<\/code><\/pre>\n<pre><code class=\"language-dart\">controller\n  ..setJavaScriptMode(JavaScriptMode.unrestricted)\n  ..setNavigationDelegate(NavigationDelegate(\n    onNavigationRequest: (request) =&gt; _isAllowed(Uri.parse(request.url))\n        ? NavigationDecision.navigate\n        : NavigationDecision.prevent,\n  ))\n  ..loadRequest(url);\n\/\/ No JavaScriptChannel: the session token never crosses into web content.<\/code><\/pre>\n<p>If a help page genuinely needs to be personalized, fetch the personalized content from your API with the user&#8217;s session, server-side, rather than exposing the token to the page.<\/p>\n<h2>How to Run This Checklist on Your App<\/h2>\n<p>Start with search. Grep the Dart code for <code>badCertificateCallback<\/code>, <code>dotenv<\/code>, <code>print(<\/code>, <code>debugPrint(<\/code>, <code>SharedPreferences<\/code>, <code>addJavaScriptChannel<\/code>, and <code>local_auth<\/code>, and grep the platform folders for <code>usesCleartextTraffic<\/code>, <code>NSAllowsArbitraryLoads<\/code>, <code>exported=\"true\"<\/code>, and <code>allowBackup<\/code>.<\/p>\n<p>Then look at a real build. Unzip a release APK and list <code>assets\/flutter_assets\/<\/code> to see exactly which files you ship. Check the merged Android manifest. Install the release build on a test device and inspect what lands on disk after a normal session.<\/p>\n<p>For traffic, remember that Dart ignores the system proxy. Testing Flutter traffic usually means redirecting it at the network level (with a VPN-based or transparent proxy setup) instead of simply setting a proxy on the device.<\/p>\n<p>Finally, turn the riskiest patterns into CI checks so they cannot return, and schedule a dependency review that covers the native trees as well as <code>pubspec.lock<\/code>.<\/p>\n<h2>Why Flutter Security Falls Between the Cracks<\/h2>\n<p>The checklist is not hard. What is hard is keeping it current on a team that ships weekly, where Dart developers own the app, native code gets touched a few times a year, and the one security person is reviewing three other things. Issues end up in the gaps between layers: the Dart code was reviewed, the manifest was not, and the plugin&#8217;s native dependency was never on anyone&#8217;s list.<\/p>\n<h2>How Nandee Secures Flutter Apps<\/h2>\n<p>Nandee is an AI Mobile Application Security Engineer with first-class support for Flutter, alongside native iOS, native Android, and React Native.<\/p>\n<p><strong>Scan across layers.<\/strong> Nandee runs SAST, DAST, and SCA\/OSS analysis on Flutter apps, along with API scanning and privacy analysis, so the Dart code, the running app, and the dependencies show up in one report instead of three tools.<\/p>\n<p><strong>Triage what matters.<\/strong> AI Triage removes false positives and ranks findings by <a href=\"https:\/\/www.nandee.ai\/blog\/exploitability-over-severity\/\">exploitability<\/a>. The deep link that reaches a session token rises to the top. A harmless debug string does not bury it.<\/p>\n<p><strong>Fix in Dart.<\/strong> The Green Agent writes the fix in the language the finding lives in, including Dart for Flutter, and Kotlin or Swift when the issue is in the native layer. Your developers review a proposed change and apply it in a few clicks.<\/p>\n<p><strong>Go deeper before launch.<\/strong> Nandee&#8217;s expert-led manual pentesting covers mobile and web apps when you need a human tester, and the Red Agent brings AI pentesting in early access. Health tech teams can sign a BAA with Nandee.<\/p>\n<p>Your team keeps the speed that made you choose Flutter, without the blind spots that come with it.<\/p>\n<h2>Ship Flutter Fast, and Ship It Secure<\/h2>\n<p>Flutter is a great choice for a startup. It just moves vulnerabilities into places that traditional tooling and native-focused habits do not look. Work through this checklist once, automate the riskiest checks, and keep testing every release.<\/p>\n<p><strong>Start a scan or book a demo at <a href=\"https:\/\/nandee.ai\">nandee.ai<\/a><\/strong> and see what your Flutter build really ships. Then choose the plan that keeps every release, on both platforms, tested and fixed.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A Flutter security checklist: 12 vulnerabilities scanners often miss in Dart code, platform config, and plugins, with fixes for iOS and Android.<\/p>\n","protected":false},"author":1,"featured_media":100049,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-100050","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-mobile-security-fundamentals"],"_links":{"self":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/posts\/100050","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/comments?post=100050"}],"version-history":[{"count":0,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/posts\/100050\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/media\/100049"}],"wp:attachment":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/media?parent=100050"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/categories?post=100050"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/tags?post=100050"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}