VAPT Remediation OWASP MASVS Flutter 3.x / Dart 3 Android · iOS · Web

Flutter Security
Hardening Guide

A complete, copy-paste-ready security baseline for Flutter applications on Android, iOS, and Web. Every control targets Flutter's unique attack surface — from dart-define secret management to certificate pinning, obfuscation, and Firebase rules.

16
Controls
50+
Dart Snippets
3
Platforms
100%
Free / OSS
🔑
Secrets & Storage
API keys, local data encryption, and keychain/keystore access
01
API Key Security with --dart-define
CRITICAL All Platforms dart-defineenviedpubspec.yaml
⚠ Issue
Hardcoding API keys, Firebase config, or secrets directly in Dart source files embeds them in the compiled app binary. Anyone can extract these strings using tools like strings, APKTool, or MobSF within seconds.
🔥 Risk
API key theft from published APK/IPA enables unauthorized API abuse, cloud resource misuse (billing fraud), data exfiltration, and backend account takeover.
📁 Files & Paths
lib/config/env.dartpubspec.yaml .env (never commit)CI/CD secrets
🔧 Fix 1 — Pass secrets via --dart-define at build time
Terminal — Build with secrets injected
# ✗ NEVER: hardcode in source
# const apiKey = 'AIzaSyABC123...';

# ✓ Inject at build time via --dart-define
$ flutter build apk \
  --dart-define=API_KEY=your_api_key_here \
  --dart-define=FIREBASE_PROJECT_ID=my-project \
  --dart-define=BASE_URL=https://api.your-app.com \
  --release

# Or from a file (Flutter 3.7+)
$ flutter build apk \
  --dart-define-from-file=.env.production \
  --release

# .env.production format (JSON):
# { "API_KEY": "xxx", "BASE_URL": "https://api.your-app.com" }
lib/config/env.dart — Type-safe env accessor
// Access dart-define values — only available after build injection
abstract class Env {
  // fromEnvironment reads the --dart-define value
  static const String apiKey = String.fromEnvironment(
    'API_KEY',
    defaultValue: '',
  );

  static const String baseUrl = String.fromEnvironment(
    'BASE_URL',
    defaultValue: 'https://api.your-app.com',
  );

  static const bool isProduction = bool.fromEnvironment(
    'IS_PROD',
    defaultValue: false,
  );

  // Validate all required vars are set at app start
  static void validate() {
    assert(apiKey.isNotEmpty, 'API_KEY not set. Use --dart-define=API_KEY=...');
    assert(baseUrl.isNotEmpty, 'BASE_URL not set.');
  }
}
lib/main.dart — Validate on startup
void main() {
  // Validate secrets are injected before app launches
  Env.validate();
  runApp(const MyApp());
}
🔧 Fix 2 — Use envied package for compile-time obfuscation
pubspec.yaml
dependencies: envied: ^0.5.4 dev_dependencies: envied_generator: ^0.5.4 build_runner: ^2.4.9
lib/env/env.g.dart (generated — add to .gitignore)
import 'package:envied/envied.dart'; part 'env.g.dart'; @Envied(path: '.env', obfuscate: true) // obfuscate=true XOR-encodes values abstract class Env { @EnviedField(varName: 'API_KEY', obfuscate: true) static const String apiKey = _Env.apiKey; @EnviedField(varName: 'BASE_URL', obfuscate: true) static const String baseUrl = _Env.baseUrl; }

⚠️ Add lib/env/env.g.dart and .env to .gitignore — never commit them.

👥 Responsible
Flutter DeveloperDevOps/CI
🔄 Rebuild Required
⟳ flutter clean && flutter build
✅ Verification
# Scan release APK for hardcoded secrets $ strings build/app/outputs/flutter-apk/app-release.apk | grep -iE 'AIza|sk_live|secret|password' → (empty — no secrets in binary) # Decompile and search with apktool $ apktool d app-release.apk -o decompiled/ $ grep -r "API_KEY\|apiKey" decompiled/smali/ → Should show XOR-obfuscated bytes, NOT the raw key # Verify dart-define values don't appear in strings $ strings app-release.apk | grep "your_actual_api_key_value" → (empty)
📋 VAPT Closure Statement
🔒
Audit Closure
All API keys and secrets are injected at compile time via --dart-define-from-file from CI/CD pipeline secrets — never from committed source files. The envied package applies XOR-based compile-time obfuscation. Binary string scan of the release APK confirms no plaintext API keys. .env files and generated code are excluded from version control.
02
Secure Local Storage — Never SharedPreferences for Secrets
CRITICAL AndroidiOS flutter_secure_storagehive_ce
⚠ Issue
SharedPreferences stores data as plaintext XML/JSON files in the app's data directory. On rooted Android devices or jailbroken iPhones, these files are fully readable. Never store tokens, passwords, or PII here.
🔥 Risk
Token theft from plaintext SharedPreferences on rooted/jailbroken devices, cold-boot attacks, and forensic tool extraction (adb backup, iMazing) exposing all stored credentials.
🔧 Fix — flutter_secure_storage backed by Keychain/Keystore
pubspec.yaml
dependencies: flutter_secure_storage: ^9.2.2 # Uses Android Keystore / iOS Keychain
lib/services/secure_storage.dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureStorageService { static const _storage = FlutterSecureStorage( aOptions: AndroidOptions( encryptedSharedPreferences: true, // AES-256 via Keystore keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_OAEPwithSHA_256andMGF1Padding, storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding, ), iOptions: IOSOptions( accessibility: KeychainAccessibility.first_unlock_this_device, // Requires device authentication before access ), ); // ─── Write secure value ─────────────────────────────────────── static Future<void> write(String key, String value) async { await _storage.write(key: key, value: value); } // ─── Read secure value ──────────────────────────────────────── static Future<String?> read(String key) async { return await _storage.read(key: key); } // ─── Delete a key ───────────────────────────────────────────── static Future<void> delete(String key) async { await _storage.delete(key: key); } // ─── Wipe all data on logout ────────────────────────────────── static Future<void> deleteAll() async { await _storage.deleteAll(); } } // Storage key constants — centralise all key names abstract class StorageKeys { static const accessToken = 'access_token'; static const refreshToken = 'refresh_token'; static const deviceId = 'device_id'; // ✗ NEVER store raw passwords — only tokens }

📊 Storage decision matrix:

Data TypeStorageSafe?
Auth tokens, passwordsflutter_secure_storage✅ Required
PII (email, name)flutter_secure_storage✅ Recommended
User preferences, themeSharedPreferences⚠ OK (non-sensitive)
Cached data (posts, images)Hive CE encrypted✅ With encryption
👥 Responsible
Flutter Developer
✅ Verification
# Android: check SharedPreferences XML for sensitive data $ adb shell run-as com.your.app cat shared_prefs/*.xml → Should NOT contain access_token, password, or PII # Android: verify EncryptedSharedPreferences (keystore-backed) $ adb shell ls /data/data/com.your.app/shared_prefs/ → FlutterSecureStorage files should be encrypted blobs, not readable XML # iOS: verify Keychain access # Use iMazing or Keychain-Dumper on jailbroken device → Keys stored with kSecAttrAccessibleWhenUnlockedThisDeviceOnly → Values should be encrypted (not readable without device unlock)
📋 VAPT Closure Statement
🔒
Audit Closure
All sensitive data (auth tokens, refresh tokens, device identifiers) is stored exclusively via flutter_secure_storage backed by Android Keystore (AES-256-GCM) and iOS Keychain (kSecAttrAccessibleWhenUnlockedThisDeviceOnly). SharedPreferences is restricted to non-sensitive UI preferences. ADB inspection of the app's data directory confirms no plaintext tokens in shared_prefs XML files. A deleteAll() method wipes all secure storage on explicit logout.
03
Android Keystore & iOS Keychain Hardening
HIGH AndroidiOS android:networkSecurityConfigTEE
⚠ Issue
Without proper Keystore configuration, cryptographic keys may be stored in software (not hardware-backed TEE), making them extractable from rooted devices. Android backup may also expose Keystore data if not disabled.
🔥 Risk
Encryption key extraction from software Keystore on rooted devices, ADB backup exposing app data, and missing allowBackup=false leaking app files to cloud backup services.
🔧 Fix — AndroidManifest.xml hardening
android/app/src/main/AndroidManifest.xml
<application <!-- Disable automatic ADB backup — prevents data extraction --> android:allowBackup="false" <!-- Custom backup rules if you need selective backup --> android:fullBackupContent="@xml/backup_rules" <!-- Restrict data extraction (Android 12+) --> android:dataExtractionRules="@xml/data_extraction_rules" <!-- Restrict app to debuggable=false in release --> android:debuggable="false" <!-- Network config for SSL pinning --> android:networkSecurityConfig="@xml/network_security_config" >
android/app/src/main/res/xml/backup_rules.xml
<full-backup-content> <!-- Exclude sensitive data from cloud backup --> <exclude domain="sharedpref" path="FlutterSecureStorage" /> <exclude domain="database" path="app_data.db" /> </full-backup-content>
ios/Runner/Info.plist — iOS hardening flags
<!-- Prevent iCloud backup of sensitive keychain items --> <key>NSAllowsArbitraryLoads</key> <false/> <!-- Require encrypted backups --> <key>UIFileSharingEnabled</key> <false/> <!-- Prevent file sharing via iTunes --> <key>LSSupportsOpeningDocumentsInPlace</key> <false/>
👥 Responsible
Flutter DeveloperMobile Security
✅ Verification
# Android: verify backup is disabled $ aapt dump badging app-release.apk | grep "allowBackup" → allowBackup='false' # Android: attempt ADB backup — should fail $ adb backup -f test.ab com.your.app → "Backup not allowed" or empty backup # Android: verify debuggable is false in release $ aapt dump badging app-release.apk | grep "debuggable" → (empty — debuggable not set = false by default in release)
📋 VAPT Closure Statement
🔒
Audit Closure
android:allowBackup=false is set in AndroidManifest preventing ADB and cloud backup data extraction. Sensitive directories are explicitly excluded from backup rules. android:debuggable is absent from the release manifest (defaults to false). iOS file-sharing entitlements are disabled. ADB backup test confirms backup is blocked.
🌐
Network Security
Certificate pinning, cleartext traffic prevention, WebView hardening
04
Certificate Pinning (HTTPS + Dio)
CRITICAL All Platforms diohttp_certificate_pinningdart:io
⚠ Issue
Without certificate pinning, Flutter apps trust any CA-signed certificate. Attackers using proxy tools (Burp Suite, mitmproxy) or installing a rogue CA certificate can intercept all HTTPS traffic — including auth tokens and API requests.
🔥 Risk
Complete HTTPS traffic interception via MITM attack — auth tokens, API keys, user data, and private communications are exposed in plaintext to the attacker.
🔧 Fix 1 — Certificate pinning via Dio + SecurityContext
pubspec.yaml
dependencies: dio: ^5.7.0
lib/network/pinned_http_client.dart
import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; class PinnedHttpClient { static Future<Dio> create() async { // Load pinned certificate from assets final certBytes = await rootBundle.load('assets/certs/api_cert.cer'); final cert = certBytes.buffer.asUint8List(); // Create SecurityContext with ONLY your certificate trusted final secCtx = SecurityContext(withTrustedRoots: false); secCtx.setTrustedCertificatesBytes(cert); // Create HttpClient using pinned context final httpClient = HttpClient(context: secCtx); // Reject connections that don't match pinned cert httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) { // In prod: always return false — never trust bad certs return false; }; final dio = Dio(BaseOptions( baseUrl: Env.baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 30), )); // Attach custom adapter with pinned client dio.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () => httpClient, ); // Add auth interceptor dio.interceptors.add(AuthInterceptor()); return dio; } } // ─── Extract cert public key hash (use for SPKI pinning) ────── // $ openssl x509 -in cert.pem -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | base64
🔧 Fix 2 — Android Network Security Config (backup pinning)
android/app/src/main/res/xml/network_security_config.xml
<network-security-config> <base-config cleartextTrafficPermitted="false"> <trust-anchors> <!-- Only trust your own CA / Let's Encrypt --> <certificates src="@raw/api_cert" /> </trust-anchors> </base-config> <domain-config> <domain includeSubdomains="true">api.your-app.com</domain> <pin-set expiration="2026-12-31"> <!-- Primary pin (current cert SHA-256 SPKI hash) --> <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin> <!-- Backup pin (keep at least one backup!) --> <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin> </pin-set> </domain-config> <!-- NEVER allow debug-time CA trust to leak to release --> <debug-overrides> <trust-anchors> <certificates src="user" /> </trust-anchors> </debug-overrides> </network-security-config>
👥 Responsible
Flutter DeveloperBackend/DevOps
✅ Verification
# Test with mitmproxy — intercept should fail $ mitmproxy --listen-port 8080 # Configure device to use 192.168.x.x:8080 as proxy # Open app and make API calls → App shows SSL handshake error or network error (NOT intercept) # Test Burp Suite certificate interception $ burpsuite → Proxy → Install cert → Configure device proxy # Make API call from Flutter app → javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure # Verify cleartext is blocked (Android) $ adb shell curl http://api.your-app.com/test → CLEARTEXT communication not permitted
📋 VAPT Closure Statement
🔒
Audit Closure
Certificate pinning is implemented at two layers: Dart SecurityContext (withTrustedRoots: false, custom CA only) and Android Network Security Config (SHA-256 SPKI pin with backup pin). Cleartext traffic is globally prohibited. Mitmproxy and Burp Suite interception tests confirm SSL handshake failure when using a rogue CA certificate. A pin expiry date is configured with a documented rotation procedure.
05
Block Cleartext HTTP Traffic
HIGH AndroidiOS network_security_configATSInfo.plist
⚠ Issue
Flutter apps may communicate over plain HTTP (http://) — especially in debug builds or when usesCleartextTraffic=true was added for development and never removed. All production traffic must be HTTPS only.
🔥 Risk
Credentials and session tokens transmitted in cleartext over HTTP are visible to anyone on the same network (coffee shops, ISPs, DNS providers) via passive MITM.
🔧 Fix — Force HTTPS on both platforms
android/app/src/main/AndroidManifest.xml
<!-- ✗ REMOVE this line if it exists in your manifest: --> <!-- android:usesCleartextTraffic="true" --> <!-- ✓ Set to false explicitly: --> <application android:usesCleartextTraffic="false" android:networkSecurityConfig="@xml/network_security_config" >
ios/Runner/Info.plist — App Transport Security (ATS)
<!-- ✗ NEVER allow arbitrary loads: --> <!-- <key>NSAllowsArbitraryLoads</key><true/> --> <!-- ✓ Enforce ATS (default, but make it explicit): --> <key>NSAppTransportSecurity</key> <dict> <!-- Require TLS 1.2+ for all connections --> <key>NSAllowsArbitraryLoads</key> <false/> <!-- If specific domains need exceptions (e.g., CDN): --> <key>NSExceptionDomains</key> <dict> <key>api.your-app.com</key> <dict> <key>NSRequiresCertificateTransparency</key><true/> <key>NSExceptionMinimumTLSVersion</key><string>TLSv1.3</string> </dict> </dict> </dict>
👥 Responsible
Flutter Developer
✅ Verification
# Android: scan manifest for cleartext permission $ aapt dump xmltree app-release.apk AndroidManifest.xml | grep -i cleartext → usesCleartextTraffic=false (or absent) # iOS: check Info.plist for NSAllowsArbitraryLoads $ grep -A2 "NSAllowsArbitraryLoads" ios/Runner/Info.plist → <false/> # Dart: enforce HTTPS in all URLs at code level $ grep -r "http://" lib/ | grep -v "https://" → (empty — no http:// URLs in production code)
📋 VAPT Closure Statement
🔒
Audit Closure
Cleartext HTTP traffic is prohibited on both platforms: android:usesCleartextTraffic=false in AndroidManifest and NSAllowsArbitraryLoads=false in iOS ATS configuration. All API base URLs use HTTPS. Certificate Transparency is required for the primary API domain. Codebase grep confirms no http:// URLs in production Dart code.
06
Flutter WebView Security
HIGH All Platforms webview_flutterNavigationDelegate
⚠ Issue
Flutter WebViews without navigation controls can be hijacked to load malicious pages, execute JavaScript injected via URL manipulation, or access native bridge methods exposing sensitive app APIs.
🔥 Risk
XSS via injected JavaScript accessing native bridge, phishing via malicious redirects, deep-link hijacking, and session token theft via JavaScript message channels.
🔧 Fix — Hardened WebView with navigation whitelist
lib/widgets/secure_webview.dart
import 'package:webview_flutter/webview_flutter.dart'; class SecureWebView extends StatefulWidget { final String initialUrl; final List<String> allowedDomains; const SecureWebView({required this.initialUrl, required this.allowedDomains}); @override State<SecureWebView> createState() => _SecureWebViewState(); } class _SecureWebViewState extends State<SecureWebView> { late final WebViewController _controller; @override void initState() { super.initState(); _controller = WebViewController() // ✗ NEVER enable JavaScript unless absolutely required ..setJavaScriptMode(JavaScriptMode.disabled) // Whitelist-based navigation delegate ..setNavigationDelegate(NavigationDelegate( onNavigationRequest: (request) { final uri = Uri.parse(request.url); // Only allow HTTPS + whitelisted domains if (uri.scheme != 'https') { return NavigationDecision.prevent; } final isAllowed = widget.allowedDomains.any( (domain) => uri.host == domain || uri.host.endsWith('.$domain'), ); return isAllowed ? NavigationDecision.navigate : NavigationDecision.prevent; }, onWebResourceError: (error) { // Never expose internal error details to the page debugPrint('WebView error: ${error.errorCode}'); }, )) // Disable mixed content (HTTP in HTTPS context) ..loadRequest(Uri.parse(widget.initialUrl)); } }
👥 Responsible
Flutter Developer
📋 VAPT Closure Statement
🔒
Audit Closure
WebView instances use a domain allowlist-based NavigationDelegate, blocking all HTTP and non-whitelisted HTTPS navigation. JavaScript is disabled by default; any exceptions require explicit sign-off. Native JavaScript channels are not exposed. Redirection to external domains is blocked and logged.
🔐
Auth & Access Control
Biometrics, OAuth flows, deep link security
07
Biometric Authentication (local_auth)
HIGH AndroidiOS local_authBiometricType
⚠ Issue
Incorrectly implemented biometrics (e.g., accepting BiometricOnly: false which allows PIN/pattern fallback) or not validating available biometric strength allows bypass via weaker authenticators.
🔥 Risk
Biometric bypass via PIN/pattern fallback on shared/stolen devices, fake biometric enrollment on rooted Android devices, and Class 1 (convenience) biometric acceptance instead of Class 3 (strong).
🔧 Fix — Secure biometric authentication
pubspec.yaml
dependencies: local_auth: ^2.3.0
lib/services/biometric_service.dart
import 'package:local_auth/local_auth.dart'; class BiometricService { final _auth = LocalAuthentication(); Future<bool> canAuthenticate() async { if (!await _auth.canCheckBiometrics()) return false; final available = await _auth.getAvailableBiometrics(); // Require strong biometrics (fingerprint or face) return available.any((b) => b == BiometricType.fingerprint || b == BiometricType.face || b == BiometricType.strong, ); } Future<bool> authenticate() async { if (!await canAuthenticate()) { throw Exception('Biometric authentication not available'); } return _auth.authenticate( localizedReason: 'Authenticate to access your account', options: const AuthenticationOptions( biometricOnly: true, // ✓ NO PIN/pattern fallback sensitiveTransaction: true, stickyAuth: true, // keep auth dialog active if app backgrounded useErrorDialogs: true, ), ); } } // ─── Android: declare in AndroidManifest.xml ────────────────── // <uses-permission android:name="android.permission.USE_BIOMETRIC" />
👥 Responsible
Flutter Developer
📋 VAPT Closure Statement
🔒
Audit Closure
Biometric authentication enforces biometricOnly: true — PIN/pattern fallback is disabled for sensitive operations. Only strong biometrics (Class 3: fingerprint, face) are accepted. The service validates biometric availability before presenting the prompt. sensitiveTransaction: true prevents biometric bypass attacks on Android.
08
Secure OAuth 2.0 with PKCE (flutter_appauth)
CRITICAL All Platforms flutter_appauthPKCEcode_verifier
⚠ Issue
Implementing OAuth without PKCE (Proof Key for Code Exchange) leaves the authorization code vulnerable to interception on mobile — especially via custom URL scheme hijacking where any app can register the same scheme.
🔥 Risk
Authorization code interception by malicious apps registered with the same custom URL scheme, enabling full account takeover without needing a client secret.
🔧 Fix — OAuth with PKCE via flutter_appauth
pubspec.yaml
dependencies: flutter_appauth: ^8.0.1
lib/services/auth_service.dart
import 'package:flutter_appauth/flutter_appauth.dart'; class AuthService { static const _appAuth = FlutterAppAuth(); // ─── Authorization Code + PKCE flow ────────────────────────── Future<AuthorizationTokenResponse?> signIn() async { return await _appAuth.authorizeAndExchangeCode( AuthorizationTokenRequest( 'YOUR_CLIENT_ID', 'com.your.app:/oauth2callback', // use App Links, not custom scheme issuer: 'https://accounts.your-idp.com', scopes: ['openid', 'profile', 'email'], preferEphemeralSession: true, // no cookie persistence // PKCE is enabled by default in flutter_appauth // code_verifier & code_challenge generated automatically additionalParameters: { 'prompt': 'consent', // always show consent }, ), ); } // ─── Refresh token securely ─────────────────────────────────── Future<TokenResponse?> refreshToken(String refreshToken) async { return await _appAuth.token( TokenRequest( 'YOUR_CLIENT_ID', 'com.your.app:/oauth2callback', refreshToken: refreshToken, issuer: 'https://accounts.your-idp.com', scopes: ['openid', 'profile'], ), ); } }

⚠️ Use Android App Links / iOS Universal Links (HTTPS-based) instead of custom URL schemes (myapp://) to prevent scheme hijacking. Custom schemes can be registered by any app on the device.

👥 Responsible
Flutter DeveloperBackend/IdP Team
📋 VAPT Closure Statement
🔒
Audit Closure
OAuth 2.0 is implemented via flutter_appauth using the Authorization Code + PKCE flow. PKCE code_verifier is generated automatically per-session with 256-bit entropy. HTTPS-based App Links replace custom URL schemes to prevent authorization code interception. Tokens are stored in flutter_secure_storage (Keystore/Keychain backed). Refresh tokens rotate on use with server-side revocation capability.
09
Deep Link Security & Validation
HIGH AndroidiOS go_routerApp Linksassetlinks.json
⚠ Issue
Deep links accepted without validation can trigger unintended navigation (e.g., passing crafted parameters to internal screens), execute actions on behalf of the user, or enable open-redirect attacks to phishing pages.
🔥 Risk
Malicious deep links triggering authenticated actions (payments, account changes), open redirects to phishing sites, and path traversal via crafted deep link parameters passed to file operations.
🔧 Fix — Validate all deep link parameters
lib/router/deep_link_validator.dart
class DeepLinkValidator { // Allowed deep link hosts static const _allowedHosts = {'your-app.com', 'app.your-app.com'}; // Allowed path prefixes — reject unknown paths static const _allowedPaths = { '/product/', '/user/', '/order/', '/share/', }; static bool validate(Uri uri) { // 1. Only accept HTTPS deep links (App Links / Universal Links) if (uri.scheme != 'https') return false; // 2. Validate host against allowlist if (!_allowedHosts.contains(uri.host)) return false; // 3. Validate path starts with known prefix final validPath = _allowedPaths.any((p) => uri.path.startsWith(p)); if (!validPath) return false; // 4. Validate query parameters (no path traversal) for (final value in uri.queryParameters.values) { if (value.contains('..') || value.contains('/')) return false; } return true; } // ─── Usage in main.dart ─────────────────────────────────────── // Stream<Uri?> incoming = app_links.uriLinkStream; // incoming.listen((uri) { // if (uri != null && DeepLinkValidator.validate(uri)) { // router.go(uri.path, extra: uri.queryParameters); // } // }); }
Hosting: /.well-known/assetlinks.json (Android App Links)
[{ "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.your.app", "sha256_cert_fingerprints": [ "YOUR_APP_SIGNING_CERT_SHA256" ] } }]
👥 Responsible
Flutter DeveloperBackend
📋 VAPT Closure Statement
🔒
Audit Closure
All incoming deep links are validated against host and path allowlists before processing. Path traversal sequences in query parameters are rejected. Android App Links and iOS Universal Links are used exclusively — custom URL schemes are disabled. The assetlinks.json file is hosted at the verified domain and tested via Google's Statement List Checker.
🛡️
Runtime Protection
Root/jailbreak detection, anti-debug, screenshot prevention
10
Root / Jailbreak Detection
HIGH AndroidiOS flutter_jailbreak_detectionsafe_device
⚠ Issue
Rooted Android / jailbroken iOS devices bypass OS security boundaries — app sandboxing, Keystore encryption, file permission isolation, and certificate pinning can all be circumvented. Financial and healthcare apps must detect this condition.
🔥 Risk
Full app data extraction, Keystore bypass, certificate pinning defeat via Frida/SSL kill switch, memory dumping of sensitive runtime data, and app patching to bypass business logic.
🔧 Fix — Multi-factor device integrity checks
pubspec.yaml
dependencies: flutter_jailbreak_detection: ^1.10.0 safe_device: ^1.1.4
lib/security/device_check.dart
import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart'; import 'package:safe_device/safe_device.dart'; class DeviceIntegrityChecker { static Future<DeviceCheckResult> check() async { final checks = await Future.wait([ FlutterJailbreakDetection.jailbroken, // iOS jailbreak FlutterJailbreakDetection.developerMode, // Android dev mode SafeDevice.isRealDevice, // emulator check SafeDevice.isSafeDevice, // combined safe check ]); final isJailbroken = checks[0] as bool; final isDevMode = checks[1] as bool; final isRealDevice = checks[2] as bool; final isSafe = checks[3] as bool; return DeviceCheckResult( isCompromised: isJailbroken || !isSafe, isEmulator: !isRealDevice, isDeveloperMode: isDevMode, ); } } // ─── Usage in app startup ───────────────────────────────────── Future<void> checkDeviceSecurity(BuildContext context) async { final result = await DeviceIntegrityChecker.check(); if (result.isCompromised) { // Log the event (for analytics) but don't reveal WHY to attacker await AnalyticsService.logEvent('compromised_device_detected'); // Show generic security warning — don't crash silently if (context.mounted) { showDialog(context: context, builder: (_) => const SecurityWarningDialog()); } } }

⚠️ Detection alone is not sufficient — a sophisticated attacker with Frida can hook and bypass these checks. Use alongside code obfuscation and Play Integrity API (Android) / DeviceCheck (iOS) for server-side verification.

👥 Responsible
Flutter DeveloperSecurity Team
📋 VAPT Closure Statement
🔒
Audit Closure
Device integrity checks run at app startup using flutter_jailbreak_detection and safe_device. Compromised devices receive a security warning, and events are logged server-side. Play Integrity API (Android) provides server-verified device attestation for high-security operations. Detection checks are obfuscated in the release build to resist Frida hooking.
11
Debug Mode Detection & Prevention
MEDIUM All Platforms kReleaseModekDebugMode--release
⚠ Issue
Debug builds expose Dart DevTools port, verbose logging, hot reload, and often relaxed security checks. Publishing a debug build or having debug features active in production creates serious attack exposure.
🔥 Risk
Dart VM service port exposure enabling code execution and memory inspection, verbose log leakage of sensitive API responses, and relaxed certificate/biometric checks in debug mode leaking to production.
🔧 Fix — Build mode guards and debug hardening
lib/utils/logger.dart — Environment-aware logging
import 'package:flutter/foundation.dart'; class AppLogger { // ✓ kReleaseMode is a compile-time constant — dead code eliminated in prod static void debug(Object? msg) { if (kDebugMode) debugPrint('[DEBUG] $msg'); } static void error(Object? msg) { if (kDebugMode) debugPrint('[ERROR] $msg'); // In release: send to crash reporting service (Sentry, Firebase Crashlytics) if (kReleaseMode) CrashReporter.log(msg); } // ✗ NEVER: print(response.data) — exposes full API response in console // ✓ ALWAYS: AppLogger.debug('Response received: ${response.statusCode}') } // ─── Security checks that only apply in release ─────────────── void applySecurityPolicies() { if (kReleaseMode) { // Disable Flutter DevTools Observatory // This is automatic in release builds // Verify this is not a debug build that slipped to prod assert(!kDebugMode, 'Do not publish debug builds!'); } }
Terminal — Always build for release in production
# ✗ NEVER publish debug or profile builds # flutter build apk (defaults to debug!) # ✓ ALWAYS use --release flag $ flutter build apk --release $ flutter build appbundle --release # Google Play $ flutter build ipa --release # iOS App Store $ flutter build web --release --dart-define=IS_PROD=true # Verify the build mode in the output $ strings app-release.apk | grep -i "debug\|profile" → Should not contain debug build indicators
👥 Responsible
Flutter DeveloperDevOps / CI
📋 VAPT Closure Statement
🔒
Audit Closure
All logging is gated behind kDebugMode compile-time constants, which the Dart compiler eliminates entirely in release builds (dead code removal). CI/CD pipeline enforces --release flag for all production builds. API responses, tokens, and sensitive objects are never passed to print() or debugPrint(). Release build scan confirms no debug artefacts or verbose log statements in the binary.
12
Screenshot & Screen Recording Prevention
MEDIUM AndroidiOS flutter_windowmanagerFLAG_SECURE
⚠ Issue
Financial, healthcare, and auth screens can be captured via screenshots, screen recorders, or the iOS/Android app switcher preview — exposing sensitive PII, balances, and authentication data without the user's knowledge.
🔥 Risk
Sensitive data visible in app switcher thumbnails leaking to spyware, screenshots captured by malicious apps with RECORD_SCREEN permission, and user-generated screenshots of financial/auth data shared inadvertently.
🔧 Fix — FLAG_SECURE and iOS screenshot prevention
pubspec.yaml
dependencies: flutter_windowmanager: ^0.3.0
lib/security/screenshot_protection.dart
import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_windowmanager/flutter_windowmanager.dart'; class ScreenshotProtection { // ─── Enable on sensitive screens ───────────────────────────── static Future<void> enable() async { if (Platform.isAndroid) { // Android: FLAG_SECURE blocks screenshots AND screen recording await FlutterWindowManager.addFlags( FlutterWindowManager.FLAG_SECURE, ); } else if (Platform.isIOS) { // iOS: overlay an opaque view when screen is captured // Use platform channel to call UITextField.isSecureTextEntry approach await const MethodChannel('screenshot_protection') .invokeMethod('enable'); } } // ─── Disable when leaving sensitive screens ─────────────────── static Future<void> disable() async { if (Platform.isAndroid) { await FlutterWindowManager.clearFlags( FlutterWindowManager.FLAG_SECURE, ); } else if (Platform.isIOS) { await const MethodChannel('screenshot_protection') .invokeMethod('disable'); } } } // ─── Usage: in sensitive screen's initState / dispose ───────── // @override void initState() { super.initState(); ScreenshotProtection.enable(); } // @override void dispose() { ScreenshotProtection.disable(); super.dispose(); }
👥 Responsible
Flutter Developer
📋 VAPT Closure Statement
🔒
Audit Closure
FLAG_SECURE (Android) is applied on all screens displaying financial data, authentication flows, and PII. The flag also prevents screen recording and masks the app switcher thumbnail. iOS uses a secure overlay technique to blank sensitive content during screen capture. Screens with FLAG_SECURE are documented in the security design document.
🏗️
Build & Deploy Security
Obfuscation, dependency auditing, and CI/CD pipeline
13
Code Obfuscation & Symbol Splitting
HIGH AndroidiOS --obfuscate--split-debug-info
⚠ Issue
Without obfuscation, Dart class names, method names, and string literals are embedded in the binary, making reverse engineering trivial with tools like jadx, Ghidra, or MobSF. Internal business logic and API structure are fully exposed.
🔥 Risk
Reverse engineering of business logic, API endpoint enumeration, authentication bypass via patched binary, and intellectual property theft of proprietary algorithms.
🔧 Fix — Build with obfuscation flags
Terminal — Obfuscated release builds
# ─── Android APK / AAB ─────────────────────────────────────── $ flutter build appbundle \ --release \ --obfuscate \ --split-debug-info=build/debug-symbols/android/ # --split-debug-info separates debug symbols for crash reporting # Store debug symbols SECURELY — not in the APK! # ─── iOS IPA ───────────────────────────────────────────────── $ flutter build ipa \ --release \ --obfuscate \ --split-debug-info=build/debug-symbols/ios/ # ─── Upload debug symbols to crash reporter ───────────────── # Firebase Crashlytics (Android): $ firebase crashlytics:symbols:upload \ --app=APP_ID \ build/debug-symbols/android/app.android-arm64.symbols # ─── Verify obfuscation worked ─────────────────────────────── $ strings app-release.apk | grep -E "^[A-Z][a-z]+[A-Z]" # Class names should be short: a, b, c, aa, etc. NOT: UserRepository
android/app/build.gradle — ProGuard rules
android { buildTypes { release { minifyEnabled true // Enable ProGuard/R8 shrinkResources true // Remove unused resources proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } }
👥 Responsible
Flutter DeveloperDevOps / CI
✅ Verification
# Check class names in APK are obfuscated $ apktool d app-release.apk -o decompiled/ $ ls decompiled/smali/ → a/, b/, c/, aa/ ... (NOT: com/yourcompany/app/UserRepository/) # Check with strings command — method names should be single chars $ strings app-release.apk | grep "UserRepository\|AuthService\|ApiClient" → (empty — obfuscated to a, b, c) # Verify debug symbols stored separately (NOT in APK) $ ls build/debug-symbols/android/ → *.symbols files present here, not inside the APK
📋 VAPT Closure Statement
🔒
Audit Closure
Release builds are compiled with --obfuscate and --split-debug-info flags. Dart class and method names are reduced to single characters (a, b, c). ProGuard/R8 minification is enabled for Android. Debug symbols are stored securely outside the APK and uploaded to Firebase Crashlytics for symbolicated crash reports. APK inspection confirms no readable class or method names from the Dart codebase.
14
Flutter Dependency Security (pub.dev Audit)
HIGH All Platforms dart pub auditpubspec.lockdependency_review
⚠ Issue
Flutter/Dart packages from pub.dev can contain vulnerabilities in their native Android/iOS code, Dart implementation, or transitive dependencies. Unmaintained packages with known CVEs are a common source of app store rejection and security incidents.
🔥 Risk
Supply chain attacks via compromised pub.dev packages, native code vulnerabilities in platform plugins (RCE in WebView plugins), and API compatibility breaks from unpinned dependencies.
🔧 Fix — Regular dependency auditing
Terminal — Audit commands
# ─── Dart built-in security audit ──────────────────────────── $ dart pub audit → Found X security advisories (lists CVE numbers and fixes) # ─── Check for outdated packages ────────────────────────────── $ flutter pub outdated → Lists all packages with newer versions available # ─── Upgrade to latest safe versions ───────────────────────── $ flutter pub upgrade --major-versions # major version bumps $ flutter pub upgrade # minor/patch updates # ─── Verify pubspec.lock is committed (reproducible builds) ── $ git status pubspec.lock → (tracked and committed) # ─── Check for packages with no recent activity ────────────── $ dart pub outdated --show-all | grep "discontinued\|unlisted" → (investigate any discontinued packages)
pubspec.yaml — Security-conscious dependency pinning
dependencies: flutter: sdk: flutter # ✓ Pin to specific compatible version ranges dio: ^5.7.0 # HTTP client flutter_secure_storage: ^9.2.2 flutter_appauth: ^8.0.1 local_auth: ^2.3.0 # ✗ AVOID: very wide ranges or no constraint # some_package: any ← never do this # some_package: >=1.0.0 ← too wide # Check package origins — prefer flutter/dart org maintained packages # Verify publisher: pub.dev → package → "Publisher: flutter.dev" or "dart.dev"
👥 Responsible
Flutter DeveloperDevOps
📋 VAPT Closure Statement
🔒
Audit Closure
dart pub audit is integrated into the CI pipeline and runs on every build. Current audit result: 0 security advisories. All dependencies are pinned to version ranges in pubspec.yaml and locked in pubspec.lock (committed). Weekly automated outdated-dependency checks are scheduled. Packages from trusted publishers (flutter.dev, dart.dev, verified organisations) are preferred.
🔥
Web & Firebase Security
Flutter Web security headers and Firebase security rules
15
Flutter Web — Security Headers & Nginx Config
HIGH Flutter Web nginx.confCSPweb/index.html
⚠ Issue
Flutter Web apps lack security headers by default, and the compiled main.dart.js (often 5-10MB) exposes the entire Dart SDK in JavaScript form. Missing CSP, HSTS, and frame protection leaves the app vulnerable to XSS and clickjacking.
🔥 Risk
XSS in Flutter Web via injected JavaScript accessing Dart's dart:html bindings, clickjacking attacks on the canvas-rendered app, and MIME-type confusion attacks on the JS bundle.
🔧 Fix — Nginx for Flutter Web
/etc/nginx/sites-enabled/flutter-app.conf
server { listen 443 ssl http2; server_name your-flutter-app.com; root /var/www/flutter-app/build/web; # ─── SPA routing for Flutter Web ──────────────────────── location / { try_files $uri $uri/ /index.html; } # ─── Security headers ──────────────────────────────────── add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; # ─── CSP for Flutter Web (canvas-based renderer) ───────── add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://api.your-app.com wss://api.your-app.com; worker-src 'self' blob:; frame-src 'none'; object-src 'none';" always; # ─── Cache control ──────────────────────────────────────── location ~* \.(js|css|wasm|png|ico|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; } location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } # Block source maps in production location ~* \.map$ { deny all; return 404; } }
Terminal — Build Flutter Web for production
# Production Flutter Web build $ flutter build web \ --release \ --dart-define=IS_PROD=true \ --dart-define-from-file=.env.production \ --web-renderer canvaskit \ # or 'html' — canvaskit harder to scrape --source-maps=false # no source maps in production # Deploy build/web/ to Nginx root $ rsync -avz build/web/ user@server:/var/www/flutter-app/build/web/
👥 Responsible
Flutter DeveloperDevOps / Nginx Admin
📋 VAPT Closure Statement
🔒
Audit Closure
Flutter Web is served via a hardened Nginx configuration with HSTS, CSP (allowing unsafe-eval required by the Dart compiler output), X-Frame-Options DENY, and nosniff headers. Source maps are disabled in production. The CanvasKit renderer is used, making Dart code harder to reverse-engineer than the HTML renderer. Security headers verified via securityheaders.com (Grade A).
16
Firebase Security Rules
CRITICAL All Platforms Firestore rulesStorage rulesfirebase.json
⚠ Issue
Firebase default security rules are either completely open (read/write allowed by anyone) or insecure test-mode rules. Many production apps launch with allow read, write: if true — exposing all data publicly.
🔥 Risk
Complete Firestore database exposure to unauthenticated reads, any authenticated user modifying any other user's data, public Firebase Storage buckets exposing all uploaded files, and mass data exfiltration.
🔧 Fix — Production Firestore Security Rules
firestore.rules
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // ✗ NEVER EVER in production: // match /{document=**} { allow read, write: if true; } // ─── Users: own data only ───────────────────────────────── match /users/{userId} { allow read: if request.auth != null && request.auth.uid == userId; allow create: if request.auth != null && request.auth.uid == userId && validUserData(request.resource.data); allow update: if request.auth != null && request.auth.uid == userId && !request.resource.data.diff(resource.data).affectedKeys() .hasAny(['role', 'createdAt']); // can't self-promote role allow delete: if false; // no self-delete } // ─── Posts: authenticated read, owner write ─────────────── match /posts/{postId} { allow read: if request.auth != null; allow create: if request.auth != null && request.resource.data.authorId == request.auth.uid && validPostData(request.resource.data); allow update: if request.auth != null && resource.data.authorId == request.auth.uid; allow delete: if request.auth != null && resource.data.authorId == request.auth.uid; } // ─── Admin operations — server-side only ───────────────── match /admin/{document=**} { allow read, write: if false; // ONLY via Firebase Admin SDK } // ─── Helper functions ───────────────────────────────────── function validUserData(data) { return data.keys().hasAll(['name', 'email']) && data.name is string && data.name.size() <= 100 && data.email is string; } function validPostData(data) { return data.keys().hasAll(['title', 'authorId']) && data.title is string && data.title.size() <= 500; } } }
storage.rules — Firebase Storage
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // User profile images: owner can write, authenticated can read match /users/{userId}/profile/{fileName} { allow read: if request.auth != null; allow write: if request.auth != null && request.auth.uid == userId && request.resource.size < 5 * 1024 * 1024 // 5MB max && request.resource.contentType.matches('image/.*'); } // Deny all other access by default match /{allPaths=**} { allow read, write: if false; } } }
Terminal — Test and deploy rules
# Test rules before deploying (Firebase Emulator Suite) $ firebase emulators:start $ firebase emulators:exec --only firestore 'dart test test/security_rules_test.dart' # Deploy rules to production $ firebase deploy --only firestore:rules $ firebase deploy --only storage:rules
👥 Responsible
Flutter DeveloperBackend / Firebase Admin
✅ Verification
# Test unauthenticated read is blocked $ curl "https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/users" → 403 PERMISSION_DENIED # Test cross-user write is blocked # Log in as User A, try to write to User B's document → FirebaseException: PERMISSION_DENIED: Missing or insufficient permissions. # Firebase Security Rules Simulator (Firebase Console) # → Rules → Simulator → test each scenario → All unauthorized operations: Denied → Authorized operations: Allowed # Run automated rules tests $ firebase emulators:exec 'dart test' → All tests passing
📋 VAPT Closure Statement
🔒
Audit Closure
Firestore and Storage security rules enforce authentication on all operations. Users can only read and write their own documents (UID-matched). Role escalation via client-side writes is prevented by blocking changes to the role field. The /admin/ collection is restricted to server-side Firebase Admin SDK only (client access returns false). File size (5MB) and MIME type restrictions are enforced in Storage rules. Rules are tested via the Firebase Emulator Suite before deployment.

🦋 Flutter Security Controls Master Checklist

Track remediation status · Update as each control is implemented and verified across all target platforms

#ControlSeverityPlatformKey Package / ToolStatus
01API Key Security (--dart-define / envied)CRITICALAllenvied☐ Open
02Secure Local StorageCRITICALAndroidiOSflutter_secure_storage☐ Open
03Keystore / Keychain HardeningHIGHAndroidiOSallowBackup=false☐ Open
04Certificate PinningCRITICALAlldio SecurityContext☐ Open
05Block Cleartext HTTP TrafficHIGHAndroidiOSnetwork_security_config☐ Open
06Flutter WebView SecurityHIGHAllwebview_flutter☐ Open
07Biometric AuthenticationHIGHAndroidiOSlocal_auth☐ Open
08OAuth 2.0 + PKCECRITICALAllflutter_appauth☐ Open
09Deep Link ValidationHIGHAndroidiOSassetlinks.json☐ Open
10Root / Jailbreak DetectionHIGHAndroidiOSflutter_jailbreak_detection☐ Open
11Debug Mode DetectionMEDIUMAllkReleaseMode☐ Open
12Screenshot PreventionMEDIUMAndroidiOSFLAG_SECURE☐ Open
13Code ObfuscationHIGHAndroidiOS--obfuscate☐ Open
14Dependency Security (pub.dev)HIGHAlldart pub audit☐ Open
15Flutter Web Security HeadersHIGHWebnginx CSP☐ Open
16Firebase Security RulesCRITICALAllfirestore.rules☐ Open
Critical: 5 controls
High: 9 controls
Medium: 2 controls
OWASP MASVS · Flutter 3.x · April 2026