OWASP Top 10 For Flutter – M4: Insufficient Input/Output Validation in Flutter
Last updated
Was this helpful?
Last updated
Was this helpful?
Welcome back to our deep dive into the OWASP Mobile Top 10, explicitly tailored for Flutter developers. In our last article, we tackled , exploring how lapses in identity and permissions checks can lead to serious breaches.
Today, we shift gears to M4: Insufficient Input/Output Validation, arguably one of the most pervasive and deceptively simple risks in any application. Last year, a popular finance app accidentally let users paste SQL payloads into its search bar, wiping out months of user data overnight. That’s the exact kind of risk M4 warns us about.
Even the most bulletproof authentication logic can be undone instantly if your app blindly trusts data crossing its boundaries. In a typical Flutter project, you’re juggling form inputs, HTTP APIs, deep links, platform channels, and WebViews, each a potential entry point for malformed or malicious data.
Over the following sections, we’ll define what OWASP means by “Insufficient Input/Output Validation,” and arm you with practical Dart and Flutter examples to lock down every trust boundary. Let’s dive in!
Input Validation: Checking every piece of incoming data (user input, API responses, deep links, messages from native code) to ensure it matches the exact shape and constraints you expect.
Output Sanitization: Cleaning or encoding data before it leaves your app (when you render it in the UI, send it back to a server, or hand it over to native code), so no hidden threats slip through.
Think of input validation as inspecting every carriage entering the fortress, no weapons, no stowaways. Output sanitization is like searching messengers before they depart, ensuring they carry no secret orders for sabotage.
Common trust boundaries in a Flutter app include:
Forms and TextFields where users type data
HTTP requests and responses from your backend
Deep links or app links that jump into specific screens
Platform channels bridging Dart and native code
WebViews or HTML renderers showing rich content
Our goal in the following sections is to ensure that both checkpoints, validation, and sanitization are rock-solid so that malicious data can neither reach the heart of your app nor escape it.
This section explores four common patterns of M4 failures, explains why they’re dangerous, and provides clear examples of how to fix them. While many examples focus on the Flutter frontend, the principles apply equally to Dart-based backends. Once you understand the underlying issues and solutions, use them wherever needed in your Flutter and Dart codebases.
Every time your app accepts free‑form data, from a search box to a deep link, it’s like inspecting every carriage at your fortress gate: "Every carriage must be searched for hidden weapons". Attackers can turn that innocent-looking bundle into a weapon if you let any unchecked item slip through.
A. SQL Injection in Local Databases
Imagine a simple search field wired straight into your SQLite database:
An attacker typing:
won’t see search results; they’ll crash your app and delete your users
table. Local stores often hold sensitive profiles, tokens, or settings so that a single flawed query can expose or wipe everything.
A safer approach is to parameterize the query and allow‑list any dynamic parts:
Here, the ?
placeholder keeps the user’s input separate from SQL code, and we verify sortColumn
against a known list instead of concatenating it blindly.
B. Command Injection via Platform Channels
When Dart invokes native scripts, the stakes are just as high. Suppose you do:
If the native side executes:
then an input like:
could run arbitrary shell commands. Treat the Dart–native boundary like another untrusted gate: check the path’s pattern before you send it:
And remember: always repeat similar validation in your native code too—never let Dart’s checks be the only line of defense.
C. Deep‑Link Parameter Poisoning
Deep links let you jump straight to a payment screen or a product detail page, but they also open a backdoor if you trust their parameters blindly:
An attacker could send:
or a non‑numeric string, crashing your parser or injecting malicious data.A safer flow is to parse, validate, and then sanitize:
You can even write integration tests or run a MITM proxy (like Charles) to feed malformed links and ensure your app rejects them cleanly.
D. Syntactic vs. Semantic Validation
It’s not enough to check format (“can this parse?”); you must also check the meaning (“does it make sense?”). For example, validating date ranges:
Here, parsing ensures you won’t crash on invalid text; comparing dates enforces your business rule.
E. Unicode Normalization & Safe Character Sets
Free‑form text can include emojis, accents, and look‑alike characters. Normalize it so that “é” is always one code point, then allow only expected character classes:
This prevents hidden control codes or bypasses that sneak past naive filters.
F. Avoiding Regex Denial‑of‑Service
Complex regex patterns with nested quantifiers can lock up your UI if an attacker feeds crafted input. Always anchor and simplify:
Test any new regex against long, repetitive strings in a small Dart script to confirm it completes in milliseconds.
G. Client‑Side File Upload Validation
If your app lets users pick images or documents, don’t send them straight to the server:
Extension allow‑list: .jpg
, .png
, .pdf
Size limit: e.g., 5 MB
Content sniffing: verify it really is an image or PDF
Rename to a safe, server‑controlled filename
H. Canonicalize Before You Validate
Attackers often use percent‑encoding or Unicode variants to slip past allow‑lists. Always decode into one canonical form, then apply your checks:
%2e%2e
vs ..
: Without decoding, RegExp(r'^[\w\-]+\$')
might miss a path‑traversal attack.
Mixed‑width Unicode characters can hide dangerous sequences if you validate on raw code units.
Making canonicalization the first step of every validation routine ensures your regex and length checks see the actual data, not a cleverly encoded variant.
I. NoSQL / GraphQL Injection
While SQL injection is well‑known, modern apps often talk to NoSQL stores or GraphQL endpoints. Unvalidated inputs in query objects or GraphQL queries can let attackers manipulate filters or execute unauthorized queries.
To fix it, allow‑list field names and validate types before constructing queries:
Beyond filter injections, be cautious when dynamically building Firestore document paths based on user input. If you let users control path segments like userId
, projectName
, or orderId
without validation, they might access or overwrite data that doesn't belong to them, especially if your Firestore rules are too broad.
An attacker could craft inputProjectId
as ../../admins/root
, possibly navigating out of bounds or triggering unexpected rules behavior. The fix is to validate all Firestore path components explicitly.
Avoid path control characters like /
or .
unless absolutely necessary. Firestore treats document and collection paths as part of its access control model, so a poorly validated string can become a security bug.
J. JSON Schema Validation
While you can create your own JSON validator, using a package might work for you too. For complex payloads, embed a JSON Schema validator to enforce structure:
Even if input is clean, output can become dangerous when sent to contexts that interpret it, particularly HTML, CSV, or logging systems.
A. XSS in WebView
Loading unfiltered HTML is a direct path to script execution:
Instead:
Sanitize HTML before loading:
Disable JavaScript if possible:
Whitelist tags and attributes if JS is needed.
B. CSV Injection
If you export user data into a CSV, spreadsheet apps may treat cells starting with =
or -
as formulas:
To prevent this, prefix dangerous cells with a single quote:
C. Unsafe URL Generation
When building query URLs:
Always use Dart’s Uri
helpers:
With unsafe outputs now neutralized, let’s turn next to how context shapes what “safe” really means.
A string that’s safe in one scenario might be fatal in another. Context is king.
A. File Path Traversal
If fileName
is ../../etc/passwd
, you could read system files (on rooted devices).A safe pattern:
B. Dynamic UI Generation
If you let the server send widget definitions as JSON, a malformed request could crash your app or introduce logic flaws. Always validate the JSON schema before mapping it to widget code. Check "JSON Schema Validation" section to learn more how you can do that.
C. Accessibility & Localization
Don’t forget to validate user inputs for different locales (dates, numbers) and ensure error messages and validation cues are announced via Flutter’s accessibility widgets (e.g., `Semantics`, `SnackBar`) so all users get clear, localized feedback.Here is an example for inspiration.
Even well‑formed and properly encoded data can be altered once stored or cached. You must verify it hasn’t been tampered with.
A. SharedPreferences Flag Tampering
B. Verifying Remote Config or Assets
If you download a JSON config or an asset at runtime (for feature toggles, theming, etc.), use a signature or checksum provided by your server. After download:
Compute the SHA‑256 of the payload.
Compare it against the checksum you received over a secure channel.
If they don’t match, reject the payload and fall back to defaults.Now that we’ve secured every gate for inputs, outputs, and stored data, it’s time to adapt these defenses to platform‑specific rules.
Even though Flutter gives us a single codebase, Android and iOS (and Windows, Web, macOS, and Linux) enforce different rules under the hood. Your validation strategy must account for those differences.
iOS: Apps live in a strict sandbox. Even if you allow ../
in a filename, iOS will block access outside your container.
Android: Modern Android uses scoped storage, but if you request legacy or external‑storage permissions, a path‑traversal attack can hit shared directories.
To mitigate:
Always use path_provider
to get the correct app directory.
Normalize and strip directory parts with path.basename()
.
Target-scoped storage on Android and avoid broad storage permissions unless absolutely necessary.
Flutter plugins like uni_links
or receive_sharing_intent
let you handle incoming data:
Tip: On Android, set
android:autoVerify="true"
in yourAndroidManifest.xml
to reduce phishing via fake intents, but Dart-side validation remains essential.
Android WebView (Chromium-based) lets you disable file access and set Safe Browsing flags, but enabling JavaScript will run any script in loaded HTML.
iOS WKWebView respects Content Security Policies if you inject them, but will execute JavaScript if enabled.
Secure WebView setup for both platforms:
If you must enable JavaScript (e.g., for interactive widgets):
Sanitize the HTML.
Restrict JS bridges (JavaScriptChannel on Android, message handlers on iOS) to known methods.
Clear cache and data on logout to remove leftover scripts or cookies.
Platform channels let Dart talk to Kotlin/Java or Swift/Objective-C, another trust boundary that needs validation on both sides.
On the Kotlin side:
Use Pigeon to auto‑generate type-safe stubs and eliminate a whole class of runtime type errors.
By understanding sandbox models, intent handling, WebView quirks, and native bridges, you can tailor your validation strategy to close every gap.
OWASP’s “How Do I Prevent M4?” is built on these six pillars. So far, we’ve examined Input Validation, Output Sanitization, Context‑Specific Validation, and Data Integrity. Two more critical pieces remain: Secure Coding Practices and Continuous Security Testing & Maintenance.
Unsafe APIs can undo even rock-solid validation. Elevate your code quality by relying on high‑level, type‑safe libraries and avoiding string concatenation for any data that reaches a lower layer.
A. Parameterized Queries & ORMs
Rather than hand‑crafting SQL strings, use an ORM like Drift (formerly Moor). Drift auto‑parameterizes queries and provides Dart types with compile‑time checks:
With this approach, SQL injection is impossible, and your data layer is much easier to maintain.
B. Safe URL & Path Construction
Building URIs or file paths by hand invites subtle bugs and vulnerabilities. Always use Flutter’s built‑in helpers:
Uri
and the path
package handle encoding and normalization, so you never accidentally slip unsafe characters into a URL or path.
Validation logic isn’t “set and forget.” You must ensure your defenses stay aligned as your app evolves—new fields, screens, and data flows. Here’s how to bake security checks into your workflow:
A. Fuzzing with Unit & Integration Tests
Write tests that feed known attack patterns into your validation and data‑handling code:
Extend these to widget tests, use WidgetTester
or an external proxy to inject malformed deep links or simulate malicious user input.
B. Static Analysis & CI Integration
Automate linting and test runs so vulnerabilities never slip past a pull request:
Add security‑focused lints (e.g., flagging rawQuery
usage or unrestricted JavaScriptMode.unrestricted
) to catch risky code patterns before review.
If you're using Firebase Cloud Functions as your backend, don’t rely on client-side validation alone. Functions receive raw data directly from users, and even though the UI might enforce types and formats, an attacker can bypass the frontend and call the function directly using tools like Postman or a custom app.
This opens the door to abuse if the amount
is not a number, or it's negative, or too large.Always validate critical fields on the backend, even if they've already been checked on the client:
Whether it’s a Cloud Function or a Firestore path, your server-side Firebase logic should replicate the same rigorous checks as your Flutter app.
C. Validation Coverage & Maintenance
As OWASP warns under “Improper Data Validation,” adding a new form field and forgetting its validator is easy. Prevent that drift by centralizing and testing your validators:
Then verify coverage with a unit test:
And never skip calling _formKey.currentState!.validate()
on submit—otherwise, none of your carefully written validators will run.
Before we end this article, I made a quick checklists for you to ensure you are on top of the security for your application to comply the M4 best practices.[ ] All user inputs run through allow‑lists and type checks[ ] Outputs to HTML/CSV/URLs are sanitized/escaped[ ] Percent‑encoding & Unicode are canonicalized before validation[ ] NoSQL/GraphQL filters validated against field‑name allow‑lists[ ] Integrity of persisted/cached data verified (HMAC/checksum)[ ] CI runs both static (lint/SAST) and dynamic (fuzz/DAST) tests[ ] Firestore paths are validated and encoded before use[ ] Firebase Cloud Functions enforce server-side schema validation
Remember that finance app wiped out by a single SQL payload? By layering strict input checks, context‑aware rules, and continuous testing, you build a fortress where no malicious data can slip in or out. Until then, keep validating like a fortress guard, no unchecked carriage should pass!Stay tuned for our next article on M5: Insecure Communication.
Picture your Flutter app as a fortress. In our , we built high walls around identity and sessions, but what about the gates where data flows in and out? M4 is all about guarding those gates.
When OWASP talks about , they mean two complementary practices:
If any of these gates is left unchecked, attackers can inject SQL commands, sneak in scripts for XSS, manipulate file paths, or corrupt data. OWASP 2023 Mobile Top 10 ranks these flaws as with impact, meaning they happen frequently and can cause real damage.
All in all, there is one thing I want to emphasis again.Client‑side vs. Server‑side ValidationAll these checks in your Flutter app are essential for immediate feedback, but they can be bypassed by a determined attacker (e.g., by intercepting traffic with a proxy).Always replicate critical validation rules on the server before processing or storing any data. Use client‑side validation for a smooth UX, but enforce the same strict type, format, length, and semantic checks in your backend to guarantee security. to read more.
Integrate a dynamic analysis tool (e.g., or ) into your CI pipeline to regularly scan your running app for input/output flaws and endpoint injection points. This is out of scope of this article and requires a dedicate article. I will tend to write about them but also let me know if this is interesting to you so I can focus on prioritizing it for you.
Majid Hajian - Azure & AI advocate, Dart & Flutter community leader, Organizer, author