Matomo

Flutter application security considerations | Cossack Labs

đŸ‡ș🇩 We stand with Ukraine, and we stand for Ukraine. We offer free assessment and mitigation services to improve Ukrainian companies security resilience.

List of blogposts

Flutter application security considerations

Fast and easy cross-platform application frameworks are promising, yet vulnerable to attacks. Is it possible to make the cross-platform mobile application development safe while avoiding security gaps?

In this post we will focus on pros and cons of Flutter, compare it with other approaches to mobile app development, go deep into platform-specific security risks that developers are to be aware of, and finally offer fundamental mobile security recommendations to make your Flutter projects more secure.


  1. Flutter vs Native vs React Native
  2. Flutter application architecture
  3. RASP for Flutter
  4. Flutter libraries: Trustworthiness and quality
  5. Flutter mobile security: Similarities to native apps
  6. Conclusion

Flutter vs Native vs React Native #

Security of native vs cross-platform applications #

It is tempting to use cross-platform solutions that advertise faster and easier development processes. From a security point of view, as described in the React Native Security article, it may create a leaky abstraction. Making a cross-platform app secure requires more time to study platform-specific implementation of security controls for data storage, hardware encryption, biometrics authentication, secure configuration, resilience, etc.

The worst-case scenario is when security controls behave differently on different platforms, making an app more vulnerable to attacks.. Developing native apps for both iOS and Android teams requires constant collaboration to ensure consistent behaviour and security on both platforms.

When native iOS and Android apps have misaligned security controls, an attacker will target the weaker one.

Inconsistent security controls due to lack of communication #

Let’s imagine a mobile banking application that handles user accounts and payment cards data, and allows users to enable the in-app passcode.

The app encrypts data locally and stores it in the internal database. For better UX, it doesn’t require the passcode for every login, but offers biometrics login. However, envision a scenario where development teams were left without any clear instructions on how to implement the combination of in-app passcode, biometrics and application-level encryption.

The result may be the following:

  • Team iOS stores the passcode in the Keychain. They have implemented a biometrics binding to unlock the Keychain. Then, the app derives an encryption key from passcode to encrypt/decrypt the database.
  • Team Android stores passcode in the Encrypted Shared Preferences only if biometrics setting is not enabled. If the user enables biometrics, the app generates a new random encryption key, stores it in the Keystore and ties it to user biometry. The key is then used in the database encryption.

Both approaches are valid. However, each of them has its advantages and disadvantages. They have different threat vectors and cover distinct security scenarios.

For example, it is easier to steal passcode from a Keychain than a key from Secure Enclave. If biometry fails, the app uses fallback action. Team iOS can ask for an in-app passcode, while Team Android can only ask for a system-level passcode. But what if there is a very weak one? The app has no control over it. The weakness of native approach may be hidden in misalignment of the teams and lack of communication.


We empower companies to build secure systems and protect their innovations. Read about armoring a non-custodial wallet for the Tezos blockchain.


Flutter as a cross-platform solution #

Developed by Google, Flutter uses Dart—an object-oriented language. From developers perspective, it gives the platform two advantages, comparing to React Native and JavaScript:

  1. Native developers feel more comfortable with object-oriented languages, learning Dart faster than JavaScript.
  2. Google created Flutter libraries with an API similar to Android/Kotlin. So, there is minimum friction for switching from Android native development to Flutter.

Flutter engine—this is what differs Flutter from React Native.

React Native engine #

React Native apps use JavaScript code, which can run on the native JavaScript engine or on Hermes. Hermes is a JavaScript engine, developed by Facebook for React Native. It improves the performance of React Native apps.

When using the native JavaScript engine (JavaScriptCore, JavaScriptEngine), JavaScript code becomes minified and stored in the bundle file “as is”. When the app starts—native JavaScript engine executes JavaScript code. The app relies on the security of iOS and Android native engines, as well as their API.

Hermes significantly reduces the app’s startup time by employing ahead-of-time (AOT) compilation. During the build process, it compiles JavaScript code to optimised bytecode. In that case, the app relies on Facebook’s engine implementation and security.

However, referring to the React Native article, critical code execution vulnerabilities have been identified in the Hermes engine since 2020: CVE-2021-24037, CVE-2022-32234, CVE-2023-23557 and others.

Hermes CVE-2022-32234 could lead to the arbitrary code execution

Hermes CVE-2022-32234 could lead to the arbitrary code execution.

Flutter engine #

In the context of mobile applications, Flutter offers ahead-of-time compilation into machine code and its engine has no previously reported vulnerabilities compared to Hermes.

Since Dart has the capability of running code in just-in-time mode, the compilation goes into several stages:

  1. Dart code compiles into the intermediate representation (IR) code.
  2. In the case of ahead-of-time compilation, IR bytecode is compiled into machine code.

This approach provides slightly better performance compared to Hermes’ engine.

When comparing readability of the compiled code during reverse engineering, Flutter is the winner: It is hard to read and comprehend for the attackers. Hermes code can be decompiled to be almost as readable as minified code of native engines.

React Native UI vs Flutter UI #

Flutter seeks to unify UI rendering using the Skia rendering engine. This ensures that the UI rendering works the same way on each platform. On the other hand, due to its popularity, it attracts more security researchers.

Skia has known vulnerabilities in its implementations, such as CVE-2023-2136 and CVE-2022-3445, which might become a bigger security threat for Flutter applications in the future.

React Native uses the platform’s native interfaces for the UI. Its implementation significantly differs for both iOS and Android. Since React Native’s implementation for UI is different for each platform, this may introduce various platform-specific bugs.


Flutter application architecture #

To dive deeper into Flutter security, we need to understand its internals and how everything works under the hood.

Flutter application architecture

Flutter application architecture.

Flutter applications are multi-layered and consist of the Embedder, Runner, the Flutter engine, and the Flutter app. The Embedder acts as an entry point to the native platform’s rendering surfaces, exposing platform-specific API, providing accessibility features, input and handling the message event loop. Runner acts as kick-off part of the Flutter application, composing the pieces exposed by the platform-specific API of the Embedder into an application package runnable on the target platform.

Runner acts as kick-off part of the Flutter application

Runner acts as kick-off part of the Flutter application.

The engine is responsible for a wide variety of tasks, including a low-level implementation of Flutter’s main API, managing file and network I/O operations, and handling interactions between platform native code and Dart code via Platform Channels.

The engine that is included in the bundle is the same for any Flutter application, and no application-specific code is included.

Flutter engine is located in a binary called Flutter on iOS

Flutter engine is located in a binary called Flutter on iOS.

Flitter engine in Android applications is called libflutter.so

In Android applications, it is called libflutter.so.

The application’s business logic is included in the so-called ‘snapshot’ as a library. It uses a customised binary container that includes the application layout, natively compiled code, and objects.

Snapshots can be identified by the flutter_assets folder located with the app’s binary on iOS.

Snapshots can be identified by the flutter_assets folder located with the app’s binary on iOS.

The Android snapshot goes by the name libapp.so.

The Android snapshot goes by the name libapp.so.

Platform communication #

Communication between native platforms and Flutter applications can be implemented in two different ways: FFI (Foreign Function Interface) and Platform Channels.

Foreign Function Interface is an experimental feature that allows developers to use functions from components written in several programming languages, such as Swift and Kotlin. Working with FFI is complicated and susceptible to security vulnerabilities due to direct memory manipulation. Developers must be cautious to prevent buffer overflows and code injection attacks.

Platform Channels provide interaction with the platform using serialised asynchronous messages. As every event is serialised in the process of communication, this approach can introduce minor performance overheads.

Attacking Flutter Engine #

Due to Flutter’s architecture, the Flutter Engine itself is the most attractive target for attackers.

Tools like reFlutter can repackage targeted application with a modified version of the Flutter engine. This allows attackers to trace function calls and observe application’s behaviour both at the execution and at the network level.

As an example, usually attackers can bypass TLS pinning in native applications by hooking into certain pinning API methods.

Flutter doesn’t rely on the device’s native TLS stack, or respect proxy settings defined by the operating system. As Flutter’s TLS library and networking components are integrated directly into the Flutter engine, the standard techniques for establishing a proxy or intercepting network traffic are ineffective. The most straightforward method for attacking a Flutter app would be to utilise a modified Flutter engine that disables TLS pinning.


We reinforce companies through mobile security engineering and implementing modern end-to-end encryption solutions. Read how we secured notes in Bear iOS and macOS apps using Themis:


RASP for Flutter #

Flutter as a platform provides RASP (Runtime App Self Protection) tools and recommendations in different areas. However, there is no built-in tool to ensure application integrity.

As each application includes its own copy of Flutter engine, an attacker may swap the engine with a modified one to change application behaviour or to steal user data without modifying the application code. That is why protecting application integrity becomes an especially important security control for Flutter apps.

OWASP MASVS Resilience can be used to understand how well Flutter can cover RASP topics. An OWASP MAS Checklist shows hands-on requirements for a typical RASP tool in mobile apps.

Attacking Apple App Attest and Google Play Integrity #

Application integrity can be verified by natively provided services: Apple App Attest for iOS and Google Play Integrity for Android. Mobile development teams may feel confident using these services compared to writing custom solutions for integrity verification.

However, any system-level API could be easily bypassed using reverse engineering tools. There are Frida scripts or ready-to-use tweaks to turn off Apple App Attest and Google Play Integrity checks. It means that additional protection against reverse engineering tools is required.

One of the most popular community-driven solutions is FreeRASP – a closed-source security library designed for app protection during runtime.

According to the project’s owners, the library fulfils the OWASP MASVS Resilience requirements by providing a range of security measures: Root and jailbreak detection, device integrity checks and application install source checks.

When it comes to implementation, remote native app attestation services are not that easy to integrate with. It requires close communication and allocated hours for mobile and backend teams. The complexity of the flow and parsing data may require skills of senior engineers.

Free-RASP combines several tools and approaches to integrity verification, making it a heavy-weight solution. While it can give many benefits, adding it to an existing project may be challenging.

That is why many teams choose lightweight solutions, e.g. smaller community libraries, to protect against reverse engineering and tampering.

Security overview of Flutter Jailbreak Detection library #

There are such popular libraries as flutter jailbreak detection, flutter security, and rootchecker. But if you compare them to OWASP MASTG recommendations for Android and iOS, you will see that a lot of basic resilience security controls are missing in these libraries.

Flutter Jailbreak Detection library is available on iOS and Android. On iOS it uses only a small subset of features available in the iOSSecuritySuite library, on Android – the famous RootBeer library.

Flutter Jailbreak Detection library performs only jailbreak and root detection checks. This is reasonable for Android since RootBeer only provides root detection functionality. However, iOSSecuritySuite is capable of covering many other OWASP MASTG requirements.

Dart code communicates with the native code using Platform channels. They are the primary source of security concerns: They often become the primary target when bypassing jailbreak or root detection.

Take a look at this code snippet:

Interceptor.attach(Module.findExportByName("IOSSecuritySuite", "$s16IOSSecuritySuiteAAC13amIJailbrokenSbyFZ"), {
  onLeave: function(retval) {
    retval.replace(0x0);
  }
});

Interceptor.attach(Module.findExportByName("IOSSecuritySuite", "$s16IOSSecuritySuiteAAC16amIRunInEmulatorSbyFZ"), {
  onLeave: function(retval) {
    retval.replace(0x0);
  }
});

This is a code for popular reverse engineering tool Frida, which is often used for tampering with the operating system’s runtime.

This script replaces the return value of the ‘amIJailbroken’ method. As a result, return value is passed to the Dart code via Platform Channels, and the logic of jailbreak detection is broken. The jailbreak detection bypass requires only 11(!) lines of code.

The same applies to the Android platform: there are many publicly available scripts on Frida’s CodeShare to bypass RootBeer’s checks.

Security overview of Flutter Security library #

Another resilience library is the Flutter Security, it is used for application integrity checks. This library on iOS also uses iOSSecuritySuite and is trivially bypassable by a similar Frida script due to the boolean return type of the result.

However, on Android, Flutter Security uses its own implementation of integrity checks and therefore requires additional efforts from the reverse engineers.

Connecting resilience libraries to a project without additional protection is not enough—they should be obfuscated at the native level, as attackers would target the native code in the first place.

Security overview of Flutter obfuscation #

Flutter has a built-in Dart obfuscator tool. Even without using an obfuscator, Dart code in the app bundle is hardly readable.

Unlike React Native’s Hermes engine, there are no ready-to-go tools available (see hermes-dec) for the Flutter decompilation. The main reason is that the format of Dart’s snapshot, a file where the application’s business logic is stored, is constantly changing.

Although the built-in obfuscator randomises symbol names (function, class, and field names), it cannot prevent a motivated reverse engineer from viewing the class structure, library structure, and assembly code.

Thus, it is possible to filter out classes and methods that contain the application’s business logic by comparing method signatures. On this screenshot, we can see the names of class instances, field and function names in the compiled application.

Output of the compiled Flutter project without obfuscation flag

Output of the compiled project without obfuscation flag

If you take a look at line 198, you’ll notice an instance of the ‘Utf8Encoder’ class. After the project compilation with the obfuscation flag enabled, you’ll still see the same instance of this class, but now it goes by the name ‘Jd’.

Output of the compiled Flutter project with obfuscation flag

Output of the compiled project with obfuscation flag

Dart SDK offers an ‘analyze_snapshot’ tool for Flutter code static analysis. As a part of its functionality, this tool shows string objects stored in the application.

String example in the output of ‘analyze_snapshot’ tool used on the obfuscated snapshot

String example in the output of ‘analyze_snapshot’ tool used on the obfuscated snapshot

Dart’s built-in obfuscator doesn’t provide string obfuscation. Thus, protecting in-app secrets requires additional security controls. If developers ignore this fact, sensitive information such as API keys or other static secrets could potentially be exposed.


Flutter libraries: Trustworthiness and quality #

Flutter has a lot of community-made libraries, but the platform is young, so it might be difficult to figure out which ones are good. It is very easy to pick a library only to discover later that something is missing.

If you need to add local authentication with biometrics, it can be hard to find a library that works on both iOS and Android.

Not so many libraries deserve attention:

  • Flutter_secure_storage supports Keychain access policy settings, but provides no biometrics and no hardware encryption support.
  • Local_auth doesn’t allow binding biometry to the Keychain access.
  • Flutter-plugins-locker and Locker have biometry access policy, but don’t support the regular Keychain access policy.
  • BiometricStorage has secure access policy settings, including biometry and it uses hardware encryption on the Android. It looks like the best local authentication and storage library for the current moment, but not a perfect one.

Unfortunately, none of the reviewed data storage libraries use Secure Enclave functionality for hardware encryption on iOS. We cannot recommend any of them.

When it comes to cryptographic features implementation, a Dart library ‘cryptography’ is a good choice. Dart cryptographic library can be used on top of system-provided cryptographic API. For example, by encrypting data with Dart before storing it in Keychain or EncryptedSharedPreferences. So, if an attacker tries to hook system API or finds a system-level vulnerability, Dart portion of encryption will prevent data leakage as it uses different APIs under the hood.

This example shows a noticeable advantage of Flutter.

As Flutter is not mobile-only, Dart language is also not mobile-only. There are many Dart libraries for different purposes, well tested by the community, that can be used inside the Flutter app.


Struggling with mobile app security?
Get assistance from our engineers.


Flutter mobile security: Similarities to native apps #

As we explained above, using Flutter requires additional attention to the application integrity protections. However, there are still a lot of required security controls that are typical for both native and cross-platform apps.

Flutter secure storage and data security #

As any mobile app, Flutter app should handle data securely both at rest and in transit. Sensitive data should be stored encrypted, for example, by using system-provided encrypted storages like iOS Keychain or Android EncryptedSharedPreferences. Hardware-backed encryption should be used whenever available.

When encrypting locally stored data, the app should use modern and secure cryptographic algorithms. For example, Google suggests using AES-GCM for EncryptedSharedPreferences and the same algorithm can be used to encrypt other locally stored data.

Flutter network security: HTTPS and TLS pinning #

Secure network communication via HTTPS is a must for all mobile apps. While it is possible to allow HTTP traffic, developers should make sure that production builds support HTTPS only.

The OWASP MASVS Guide recommends implementing Transport Layer Security (TLS) pinning, which is a security measure that ensures that only trusted certificates are accepted when communicating with a server. This measure can provide an extra layer of protection against man-in-the-middle (MITM) attacks, where an attacker tries to intercept and modify the data being transmitted over the network.

In addition to native solutions for TLS pinning, such as Apple’s App Transport Security and Android’s Network Security Configuration, it is also possible to implement TLS pinning functionality directly on the Dart code level with default HTTP library.

Flutter application permissions security #

While Flutter is a cross-platform framework, it still requires native-specific functionality for certain tasks, such as accessing device or operating system features via permissions.

When adding third-party libraries to the project, it is important to review them. Libraries may request unnecessary permissions, which could compromise the security of the app and the data it handles.

For example, a dependency that provides access to the camera may also request access to the user’s location, when it is not required for the application’s intended purpose. Additionally, these dependencies may not be compatible with all platforms, which could cause issues and instability in the application.

We strongly advise removing and limiting permissions added by 3rd party libraries. One way to do that is to use a CI script to ensure new library versions don’t introduce additional permissions.

WebViews security in Flutter apps #

Adding a WebView into the mobile app significantly increases its attack surface and introduces attack vectors typical for web applications. OWASP MASVS has a number of recommendations to secure usage of the WebViews in the app.

Mobile developers should disable JavaScript in WebView whenever possible to avoid XSS-like attacks and significantly decrease the attack surface. WebViews allow to limit a list of domains and protocol handlers that can be opened.

If there is any sensitive data in the WebView, OWASP MASVS recommends clearing WebView cache. This can be done when the user logs out and when the WebView closes.

The most important part is to ensure that sensitive data from the application cannot leak into the WebView by any means, especially when WebView communicates with the app’s code.

Flutter Secure coding: Best practices #

Secure coding is important for all types of applications, including mobile ones. The important practices include keeping sensitive data private, handling errors correctly, protecting internal implementation details, and validating external inputs.

A basic rule for preventing security bugs is to keep the implementation clear and simple. Modern frameworks and approaches, like some reactive programming libraries, can have a high cognitive complexity, which may lead to potential bugs and security vulnerabilities.


Conclusion #

Flutter security is not better or worse than native or other cross-platform solutions. The key factor is how the mobile development team addresses native and Flutter-specific risks throughout the development process. Flutter provides a solid foundation for implementing strong security controls and building reliable apps.

Every platform has its own security challenges, and no app is perfectly secure. By proactively addressing Flutter security risks, implementing relevant security controls and continuously improving those, teams can mitigate the most serious consequences.

Consulting with security engineers can help your team to identify risks and threats of Flutter application and proactively address them.

Contact us

Get whitepaper

Apply for the position

Our team will review your resume and provide feedback
within 5 business days

Thank you!
We’ve received your request and will respond soon.
Your resume has been sent!
Our team will review your resume and provide feedback
within 5 business days