OWASP Top 10 For Flutter – M9: Insecure Data Storage in Flutter & Dart (cleaned copy)
Welcome back to our deep dive into the OWASP Mobile Top 10 for Flutter developers.
In earlier parts, we tackled:
In this ninth article, we focus on M9: Insecure Data Storage.
Let me start with a story that still makes me cringe.
A few years ago, I was doing a security review of a health-tracking app.
I discovered the developers stored complete medical records in a plain JSON file. No encryption. No access controls. Readable by anyone with a few minutes of physical access to the device.
The developers weren’t malicious. They simply didn’t realize that “saving to a file” on mobile isn’t like saving to a server behind a firewall.
Mobile devices get lost, stolen, backed up to cloud services, and sometimes compromised by malware.
Every piece of data you store becomes a potential liability.
OWASP M9: Insecure Data Storage addresses exactly this problem.
It’s the improper protection of sensitive data at rest. It’s also one of the most common vulnerabilities in mobile apps.
Flutter’s cross-platform nature adds an extra layer of complexity.
When you call SharedPreferences.setString(), do you know where that data ends up on Android versus iOS? Do you know who can access it?

Let’s get into it.
Source code. All code examples from this article are available as a runnable Flutter project on GitHub:
Understanding the Threat Landscape
Before we get to the solutions, I want to give you a clear picture of what you're defending against.
The threat isn't abstract. There are real people (and automated tools) that go after insecure storage.
Who's After Your Data?
You might be surprised how many different types of attackers care about what your app stores:
Hackers
Data extraction, credential theft
Reverse engineering, file system access
Malicious Insiders
Privilege abuse, data exfiltration
Direct database/file access
State-Sponsored Groups
Espionage, mass surveillance
Advanced persistent threats
Cybercriminals
Financial gain, ransomware
Malware, data selling on dark web
Competitors
Corporate espionage
Supply chain attacks, insider recruitment
Data Brokers
Selling personal information
Automated scraping of insecure apps
You might think your app isn't a big enough target to worry about.
Automated tools don't discriminate. A script that scans backup files for stored credentials doesn't care whether your app has 1,000 users or 1,000,000.
How Attackers Get Your Data
The exploitability of insecure data storage is rated EASY by OWASP.
That's the highest exploitability rating, and it's accurate.
Physical device access: even a few minutes with an unlocked device can be enough. Think lost phones, repair shops, or handing your phone over “just to see the photo”.
Rooted/jailbroken devices: sandboxes don’t help. Malware with privileged access can read your app’s private storage.
Backup extraction: a common blind spot. Backups to iCloud or Google may include app data in plain text.
Malware: a malicious app can read other apps' insecure storage on rooted devices. It can also abuse backup mechanisms.
Social engineering: tricking users into installing apps that request broad permissions and harvest stored data.
What Makes Flutter Apps Vulnerable?
The security weaknesses I see most often in Flutter apps fall into predictable patterns:

Flutter Storage Mechanisms: A Security Deep Dive
Before we get to the solutions, it helps to understand exactly what you're dealing with.
Flutter gives you several built-in ways to store data locally. Each one has a different security profile.
The bad news: three of the four most commonly used options are insecure for sensitive data.
1. SharedPreferences / NSUserDefaults
SharedPreferences is the go-to solution for storing simple key-value pairs in Flutter.
It's convenient, easy to use, and completely insecure for sensitive data.
I see it misused constantly. Developers store auth tokens, API keys, and sometimes even passwords here.
Here's what I mean by misuse:
"But wait," you might say, "isn't app data protected by the OS sandbox?"
Technically yes, but let me show you where this data actually ends up.
On Android, SharedPreferences are stored in XML files at:
And the contents look like this:
On iOS, NSUserDefaults are stored in plist files at:
Both locations are easily readable on rooted/jailbroken devices, through backup extraction, or with forensic tools.
The sandbox provides no protection against these attack vectors.
The rule is simple: SharedPreferences is for preferences (dark mode, language, onboarding completed), not for secrets.
Example output (what an attacker sees after extraction):
2. Local File Storage
Writing files to the app's document or cache directory is another common pattern.
Without encryption, you're essentially creating a readable archive of sensitive data:
I've seen this pattern in production apps handling financial data, medical records, and legal documents.
The developers assumed that because the file was in "their app's directory," it was safe. It wasn't.
Example output (plain JSON an attacker reads):
3. SQLite Databases
Flutter apps using sqflite or similar packages often create databases that seem more "serious" than SharedPreferences or JSON files.
But without encryption, they're just as vulnerable:
The database file at /data/data/com.example.app/databases/user_data.db can be extracted and opened with any SQLite viewer.
I once demonstrated this to a client by pulling their entire user database off a test device in under 30 seconds.
Example output (what an attacker dumps):
4. Application Logs
This one catches a lot of developers off guard.
Debug logging that seemed harmless during development persists in production builds and can leak sensitive information:
Example console output (credentials visible to anyone with ADB — Android Debug Bridge):
Platform-Specific Secure Storage
So what should you use instead?
The good news is that both Android and iOS provide solid secure storage mechanisms.
The key is understanding how to use them properly from Flutter.
Android: The Keystore System
Android’s Keystore system is the gold standard for secure storage on the platform.
When available (most modern devices), it provides hardware-backed cryptographic key storage.
This means encryption keys never leave a secure hardware module. Even the operating system can't extract them.

What makes Android Keystore special:
Hardware Backing
Keys stored in TEE (Trusted Execution Environment) or StrongBox (Pixel, Samsung Knox)
Key Extraction Prevention
Private keys never leave secure hardware
User Authentication
Can require biometrics/PIN before key use
Key Attestation
Verify key properties with signed certificate chain
Usage Restrictions
Limit key to specific algorithms/purposes
The important thing to understand is that when you use flutter_secure_storage on Android, your data is encrypted with keys protected by hardware security.
Even if an attacker extracts your encrypted data, they can't decrypt it without the secure hardware.
That requires physical possession of the specific device.
iOS: Keychain Services
iOS takes a slightly different approach with Keychain Services.
Rather than giving you direct access to encryption keys, the Keychain handles both key management and data storage.
You put secrets in, and iOS keeps them encrypted with hardware-protected keys.
On devices with a Secure Enclave, cryptographic keys created with the kSecAttrTokenIDSecureEnclave flag never leave that hardware.
General Keychain items are protected by the device’s Data Protection keys, not stored directly inside the Enclave.

One of the most important decisions you'll make with iOS Keychain is choosing the right accessibility level.
This determines when your data can be accessed and whether it gets included in backups:
WhenPasscodeSetThisDeviceOnly
Most restrictive; deleted if passcode removed
Highly sensitive (banking credentials)
WhenUnlockedThisDeviceOnly
Accessible only when unlocked, not backed up
Session tokens, temporary secrets
WhenUnlocked
Accessible when unlocked, backed up
User preferences with sensitive elements
AfterFirstUnlockThisDeviceOnly
Accessible after first unlock, not backed up
Background sync credentials
AfterFirstUnlock
Accessible after first unlock, backed up
Push notification tokens
For most sensitive data, I recommend AfterFirstUnlockThisDeviceOnly.
This provides strong protection while still allowing background operations like push notifications.
The "ThisDeviceOnly" suffix means the data won't be included in iCloud backups.
Implementing Secure Storage in Flutter
With the platform picture in mind, here's how to put it to work from your Flutter code.
Using flutter_secure_storage
The flutter_secure_storage package is the standard solution for secure storage in Flutter.
It provides a unified API that uses Android Keystore on Android and iOS Keychain on iOS:
Example output:
Secure Database with SQLCipher
For apps that need structured storage, plain SQLite isn't enough.
sqflite_sqlcipher provides transparent database encryption using SQLCipher.
The key insight: store the database encryption key securely.
That brings us back to flutter_secure_storage.
With this setup, even if someone extracts the database file, they'll just see encrypted gibberish.
The decryption key lives in the hardware-backed secure storage.
Example output:
Secure File Storage
Sometimes you need to store larger files, documents, images, or exported data.
Here’s how to handle that securely:
Example output:
Three things in this implementation are worth highlighting:
Unique IV per encryption: generate a fresh IV for every file. Never reuse an IV with the same key in AES-GCM.
AES-GCM mode: AES-GCM gives confidentiality and authenticity in one pass.
Secure deletion: overwriting raises the recovery effort. It is not a perfect guarantee on flash.
Caveat — flash storage and wear leveling: mobile devices use NAND flash with wear-leveling controllers.
Overwriting a file does not guarantee the original physical blocks are erased.
For the strongest guarantees, rely on full-disk encryption (enabled by default on Android 10+ and all iOS devices).
Secure Storage Architecture
Individual APIs are one thing. In a real app you want a single place where storage decisions live.
The rule I follow: UI and repository layers should never touch encryption keys or platform storage APIs directly.
That concern belongs in a dedicated security layer:

Complete Secure Storage Service
Here’s what that security layer looks like in practice.
It supports three sensitivity tiers, because a user preference and a banking credential shouldn't be stored the same way:
Usage Example: Authentication Service
Preventing Data Leakage
Secure storage protects data at rest, but sensitive information leaks through side channels.
I've seen each of the following cause real incidents: debug logs submitted in support tickets, screenshots captured by the OS app switcher, clipboard content harvested by other apps.
1. Secure Logging
A print() call that felt harmless in development quietly persists into your release build.
It ends up in support logs, ADB output, and crash reports.
Here’s a logger that makes it structurally difficult to accidentally expose sensitive values:
Output (debug console):
2. Clipboard Security
When users copy sensitive data (account numbers, recovery codes), that data sits in the system clipboard.
Any app can read it.
Auto-clear the clipboard after a short period:
3. Screenshot Protection
For apps handling sensitive information (banking, health, legal), you should prevent screenshots of sensitive screens.
Android Implementation (MainActivity.kt):
iOS Implementation (AppDelegate.swift):
4. Secure Memory Handling
This is an advanced topic, but important for truly sensitive data.
When you store a password or key in a Dart string, it sits in memory until the garbage collector clears it.
On a compromised device, memory can be dumped and searched for sensitive patterns.
Here’s a pattern for handling sensitive data that clears memory as soon as it’s no longer needed:
Note: Dart strings are immutable, so we can’t truly "clear" them.
This pattern still reduces the exposure window.
Example output:
Real Attack Scenarios & Prevention
Abstract advice only gets you so far.
Here’s what these attacks actually look like, and what stops each one.
Scenario 1: Backup Extraction Attack
The attack: an attacker gains access to a user's iCloud or Google account (phishing, password reuse, etc.).
They download device backups, extract your app's data, and find authentication tokens stored in plain SharedPreferences.
What happens next: the attacker uses those tokens to access the user's account. They steal personal information or make unauthorized purchases.
With the secure implementation, even if the attacker gets the backup, the tokens remain device-bound.
Scenario 2: Root/Jailbreak Data Extraction
The attack: a user’s device is compromised by malware.
The malware has privileged access and reads files from your app's private directory.
Example output:
Scenario 3: Logging Credential Leak
The attack: during a support investigation, someone shares device logs with a third party.
Those logs contain authentication tokens that were printed during debugging and never removed.
Security Checklist
I run through something like this before every major release.
It takes twenty minutes and has caught real issues more than once.
Encryption
Access control
Backup security
Logging
Memory
Root/Jailbreak
Data leakage
Conclusion
Data storage security isn't optional.
When someone trusts your app with health records or financial information, they're trusting you to protect that data.
The good news is Flutter gives us access to excellent platform-level security through packages like flutter_secure_storage.
Hardware-backed encryption from Android Keystore and iOS Keychain is genuinely difficult to break when used correctly.
Remember: it’s much easier to do this right from the start than fix it after an incident.
Resources
Stay secure, and remember: when it comes to user data, paranoia is a feature, not a bug.
Last updated
Was this helpful?

