Securing Flutter apps: best practices
By John · 29 October 2025
Securing a Flutter app means protecting data at rest, data in transit, and the app binary itself. No single measure is sufficient — security is layers. Here are the practical steps for production Flutter apps.
1. HTTPS everywhere
Flutter apps use dart:io's HTTP client which respects TLS by default. Ensure all endpoints are HTTPS and that you're not disabling certificate validation:
// NEVER do this in production
HttpOverrides.global = _InsecureHttpOverrides(); // Disables cert validation
Dio with stricter validation:
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
// Only accept certs from your own CA (certificate pinning)
client.badCertificateCallback = (cert, host, port) => false;
return client;
};
2. Certificate pinning
Certificate pinning rejects connections to servers with unexpected certificates, blocking MITM attacks:
dependencies:
http_certificate_pinning: ^4.0.0
final secureHttp = HttpCertificatePinning();
final response = await secureHttp.get(
url: 'https://api.example.com/orders',
headers: {'Authorization': 'Bearer $token'},
sslCertificate: [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Your cert fingerprint
],
timeout: 10,
);
Get your cert fingerprint:
openssl s_client -connect api.example.com:443 -servername api.example.com < /dev/null 2>/dev/null \
| openssl x509 -fingerprint -sha256 -noout
3. Secure storage for secrets
Never store tokens, keys, or passwords in SharedPreferences (not encrypted). Use flutter_secure_storage which uses Keychain on iOS and EncryptedSharedPreferences on Android:
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
// Store
await storage.write(key: 'auth_token', value: token);
// Read
final token = await storage.read(key: 'auth_token');
// Delete on logout
await storage.deleteAll();
4. Obfuscation
Dart's AOT compiler can obfuscate class and method names, making reverse engineering harder:
flutter build apk \
--obfuscate \
--split-debug-info=./debug-info
flutter build ipa \
--obfuscate \
--split-debug-info=./debug-info
Store the debug-info folder securely — you need it to symbolicate crashes from obfuscated builds.
5. Root/jailbreak detection
dependencies:
flutter_jailbreak_detection: ^1.10.0
final isCompromised = await FlutterJailbreakDetection.jailbroken;
if (isCompromised && kReleaseMode) {
// Log security event, show warning, or exit
await Sentry.captureMessage('App running on rooted/jailbroken device');
showSecurityWarningDialog();
}
6. Screenshot prevention
For apps with sensitive data (banking, health), prevent screenshots:
import 'package:flutter_windowmanager/flutter_windowmanager.dart';
// In your secure screens:
@override
void initState() {
super.initState();
FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE);
}
@override
void dispose() {
FlutterWindowManager.clearFlags(FlutterWindowManager.FLAG_SECURE);
super.dispose();
}
7. App backgrounding
When the app backgrounds, recent screenshots show in the app switcher:
class _AppState extends State<MyApp> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive) {
// Show a privacy screen
setState(() => _isPrivacyScreenVisible = true);
} else if (state == AppLifecycleState.resumed) {
setState(() => _isPrivacyScreenVisible = false);
}
}
}
8. No sensitive data in logs
// BAD: tokens in logs
debugPrint('Auth token: $token');
// GOOD: only log non-sensitive context
debugPrint('Auth: token refreshed at ${DateTime.now()}');
// In production, disable all debug prints
if (kReleaseMode) {
debugPrint = (String? message, {int? wrapWidth}) {};
}
9. API key management
Never embed API keys directly in Dart code — they can be extracted from the binary:
# Use dart-define from CI, never in code
flutter build apk \
--dart-define=API_KEY=${{ secrets.API_KEY }}
// Accessed via const — baked in at compile time but not in plain text
const apiKey = String.fromEnvironment('API_KEY');
For truly sensitive keys, proxy through your backend. Never send a secret key to the mobile client.
10. Network security config (Android)
Prevent cleartext traffic on Android:
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</base-config>
</network-security-config>
<!-- android/app/src/main/AndroidManifest.xml -->
<application
android:networkSecurityConfig="@xml/network_security_config">
Common pitfalls
Storing secrets in shared_preferences. It's stored as plain text on the device. Always use flutter_secure_storage for anything sensitive.
Trusting the client. The mobile app is on the user's device. All authorization checks must happen on your backend. Never make security decisions based on values the client sends.
Obfuscation without saving debug symbols. Obfuscated builds produce unreadable crash reports without the debug symbols file. Always save --split-debug-info output.
Sign in to like, dislike, or report.