Security
Secure storage, certificate pinning, obfuscation, OWASP mobile top 10
- 01
What are the key security best practices for Flutter mobile apps?
HardMobile security splits into five layers, and an interviewer usually wants one concrete control named per layer.
// ── Secure Storage ──────────────────────────────── const _storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions( accessibility: KeychainAccessibility.first_unlock_this_device, ), … - 02
What is SSL/certificate pinning and how do you implement it in Flutter?
HardCertificate pinning prevents Man-in-the-Middle (MITM) attacks by hardcoding the expected server certificate or public key in the app.
// ── Approach 1: trust ONLY your own certificate (strongest) ── import 'dart:io'; import 'package:dio/io.dart'; Future<Dio> createPinnedDio() async { final pem = await rootBundle.load('assets/certs/api_example_com.pem'); … - 03
How does OAuth 2.0 with PKCE work for mobile apps and why is it required?
HardPublic clients (mobile apps, SPAs) cannot keep a client_secret confidential — the secret would be embedded in the app binary, trivially extracted.
// pubspec: flutter_appauth import 'package:flutter_appauth/flutter_appauth.dart'; final _appAuth = const FlutterAppAuth(); const _clientId = 'mobile-client-id'; const _redirect = 'com.example.app:/oauthredirect'; … - 04
How does code obfuscation work in Flutter, and how do you symbolicate crashes after enabling it?
MediumDart compiles to AOT machine code on release builds, which is harder to read than JS/Java but still leaks identifier names in the snapshot.
# 1) Build a release with obfuscation + split debug info flutter build apk --release \ --obfuscate \ --split-debug-info=build/symbols/v1_2_3 flutter build ipa --release \ … - 05
What are App Transport Security (iOS) and Network Security Config (Android), and when do you have to touch them?
MediumBoth platforms ship a system-level network policy that runs BEFORE your code.
<!-- iOS: ios/Runner/Info.plist — narrow ATS exception per domain --> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <false/> <key>NSExceptionDomains</key> … - 06
How do you avoid leaking sensitive data through logs, screenshots, and clipboard?
MediumMost production leaks are not fancy attacks — they are tokens in crash logs, PII in analytics events, and payment screens captured in the app switcher.
// ── Redacting logs ────────────────────────────────── class SafeLogger { static const _redactKeys = {'password', 'token', 'access_token', 'refresh_token', 'authorization', 'cookie'}; static String redact(Map<String, dynamic> body) => jsonEncode({ for (final e in body.entries) … - 07
How do you implement a robust access-token + refresh-token rotation strategy?
HardLong-lived bearer tokens are the most common credential-leak vector in mobile apps.
import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class AuthInterceptor extends Interceptor { AuthInterceptor(this._dio, this._storage); … - 08
What are the security pitfalls of using WebView in a Flutter app?
HardWebViews host arbitrary web content with the trust of your app process.
import 'package:webview_flutter/webview_flutter.dart'; // ✅ Constrained WebView — explicit allowlist, no JS bridge unless needed class HelpCenterPage extends StatefulWidget { const HelpCenterPage({super.key}); @override State<HelpCenterPage> createState() => _HelpCenterPageState(); … - 09
Why is a deep link untrusted input, and what stops another app from stealing yours?
HardA deep link arrives from outside your process, unauthenticated, with attacker-controlled parameters, and a plain custom scheme has no owner — any app on the device can register
myapp://too.// go_router: one guarded entry point for every incoming link final router = GoRouter( redirect: (context, state) { final loggedIn = ref.read(authProvider).valueOrNull != null; // ✅ session is re-checked on arrival, whatever the link claimed if (!loggedIn && _protected.hasMatch(state.matchedLocation)) { … - 10
A teammate wants to add a pub package with 30 likes to handle payments. How do you decide, and how do you keep the dependency tree safe over time?
MediumEvery package you add runs inside your app with your app's permissions, and so does everything it depends on — which is why OWASP lists supply chain as its own top-ten category.
#!/usr/bin/env bash set -euo pipefail # What is actually being installed, including the transitive tree flutter pub deps --style=compact flutter pub outdated --show-all … - 11
local_auth returns true after a Face ID prompt. What has that actually proven, and how do you turn it into real protection?
MediumIt proves only that the operating system was willing to say yes on this device, in your process, right now — it is a UI gate, not a credential.
import 'package:local_auth/local_auth.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; final _auth = LocalAuthentication(); const _storage = FlutterSecureStorage( iOptions: IOSOptions( … - 12
A third-party service hands you an API key and says to put it in the app. What do you do with it?
MediumStart by classifying it, because anything the app can read at runtime an attacker with the binary can read too.
// ✅ Publishable key — compiled in, restricted server-side, safe to ship class Config { static const mapsKey = String.fromEnvironment('MAPS_KEY'); static const apiBase = String.fromEnvironment( 'API_BASE', defaultValue: 'https://api.example.com', ); … - 13
Users are signed out after moving to a new phone, and a few Android launches throw a decryption error reading the token — what happened?
MediumThe ciphertext was restored and the key was not:
flutter_secure_storagewrites an encrypted blob into a normal file, while the key that opens it lives in the Keystore or Secure Enclave and is not exportable.import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; const _storage = FlutterSecureStorage( iOptions: IOSOptions( … - 14
Your app decodes the JWT, reads a role claim of admin and shows the admin tab — why is that a finding in a pentest report?
MediumBecause hiding a button is not access control: the endpoint behind it is still there, and the attacker calls the endpoint, not your widget tree.
// ❌ The tab is hidden; the endpoint is wide open final claims = JwtDecoder.decode(accessToken); if (claims['role'] == 'admin') { tabs.add(const AdminTab()); // repackaged build: force this branch, call the API } … - 15
A search box feeds its text straight into a sqflite rawQuery, and a download saves under the filename the API returned; what do you fix first?
MediumBoth lines are the same bug: input from outside the app is being spliced into a language — SQL in one case, a filesystem path in the other — instead of being handed over as a value.
import 'package:path/path.dart' as p; import 'package:sqflite/sqflite.dart'; // ❌ One apostrophe changes the statement; a crafted string changes the app Future<List<Map<String, Object?>>> searchBad(Database db, String q, String sort) => db.rawQuery("SELECT * FROM notes WHERE title LIKE '%$q%' ORDER BY $sort"); … - 16
App Store Connect emails you about ITMS-91053 right after you add two plugins — what is Apple asking for, and what should you audit at the same time?
MediumIt wants a
PrivacyInfo.xcprivacythat declares every required-reason API your binary touches, with a reason code from Apple's fixed list — and the missing one is almost certainly inside a plugin, not in your code.<!-- ios/Runner/PrivacyInfo.xcprivacy — add to Copy Bundle Resources --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> … - 17
Your root-detection check returns false on an obviously rooted phone and the tester says they simply attached Frida — what should have been protecting you?
HardAny answer computed inside your process can be rewritten inside your process; the only durable control is a hardware-backed attestation that your server verifies.
import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; /// Thin channel over Play Integrity / App Attest — the native side returns an /// opaque token; Dart deliberately cannot read or judge it. class Attestation { … - 18
A teammate encrypts the offline document cache with AES, using a key derived from the user's email and a constant IV copied from a tutorial — what breaks?
HardA key anyone can compute is not a key, and a fixed IV leaks the relationships between messages — together they turn encryption into obfuscation with extra steps.
import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:cryptography/cryptography.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; … - 19
The token store is encrypted, yet a forensics report pulled chat attachments and cached images off a locked test phone — how did they get out?
HardPlatform disk encryption stops helping after the first unlock since boot, and everything outside
flutter_secure_storagesits under exactly that default.import 'dart:io'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// Derived data belongs in Caches/tmp: not backed up, and the OS may reclaim it. … - 20
A modified build of your app unlocks the premium tier without paying, and the in-app purchase code looks correct — where is the trust boundary wrong?
HardThe entitlement is being granted by the device, and the device is running the attacker's code — the grant belongs on your server, decided from a receipt the store signed.
import 'package:dio/dio.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; class Purchases { Purchases(this._iap, this._api, this._entitlements); final InAppPurchase _iap; …