App security is a broad topic. That’s why this article provides hands-on advice that you and your Flutter dev teams can use to prevent attacks on a basic and more advanced level. Let’s get to it then!
How to go about storing the credentials safely?
If there is a secret you want to protect, just avoid storing it on the client-side. The key can be extracted which is why client-side storage is not fully reliable.
Alternatively, such secrets can be kept and requested from a backend API. This sounds like a far safer strategy, since you have more control over a backend server.
But let’s say you are adamant about storing secrets on the device because there is no backend. Then there are a few things you could do in this regard.
- Make sure the code is obfuscated,
- Use encrypted data,
- Pass sensitive data in runtime (let’s say via “dart-define”),
- Inject secrets remotely (for example with Firebase Remote Config).
To read more about this topic, feel free to visit these websites:
What else can you do?
Handle sensitive data with care
The list of sensitive data is very broad — it encompasses anything that belongs to a user, for instance, passwords, tokens, identifiers, transaction histories.
Here is the list of data categories that OWASP treats as sensitive:
- Authentication tokens,
- Location data,
- UDID/IMEI, Device Name,
- Network Connection Name,
- Personal Information (e.g., date of birth, address, Social, credit card data),
- Application Data,
- Stored application logs (e.g., for an Android App ADB logcat tool),
- Debug information,
- Cached application messages,
- Transaction histories.
Here you can find more insights on insecure data storage.
Since user data is highly sensitive, you cannot store it like common preferences. There are various ways to store data locally in Flutter. However, our safest bet would be to use secure native storage such as IOS Keychain Services and Android KeyStore System.
Even though these two systems work in slightly different ways, they both offer a solution for storing small bits of data in a container that makes it much harder to extract data. There is already a plugin for secure storage in Flutter that will save the development team lots of time while properly saving sensitive data.
Lastly, as a rule of thumb, you should be very skeptical about storing data locally. Do not save anything unless it is necessary. The fewer data saved locally, the fewer things to worry about.
Ask yourself at every moment, 'Is this necessary?'
Interested in the practical side of Flutter apps? Check out this Flutter-based precision app!
Tend your dependencies
Every project uses dependencies. It is necessary because it is expensive and not sustainable to reinvent the wheel in each new project. Otherwise, projects would need dozens of components doing dozens of different stuff. That said, developers must handpick and tend the packages carefully.
You cannot afford to have performance issues as well as security leaks caused by dependencies.
Hungry for more tips? Check out how to improve mobile application performance!
Actually, this is fairly easy to do because Flutter packages are transparent. We can open anything at https://pub.dev/, investigate the code, identify the open issues, and see how frequently the code gets updated.
Flutter and Dart packages also include a points system. So each package gets rated based on specific criteria. For example, this is the breakdown of the badges 2.0.2. package with the full score.
A points system breakdown for the badges 2.0.2 package. Source: Flutter
So, make sure you install well-maintained and regularly-updated packages. Also, remember to upgrade your dependencies every once in a while. You can also install plugins that will automatically check the versions of your Flutter project dependencies like the Flutter Pub Version Checker.
Get rid of overcomplicated business logic
We all have been there, creating an over-engineered mass of code while trying to show off our programming skills. Dirty spaghetti code may also be a by-product of distracted attention mixed with a lack of knowledge about the platform or the programming language used. Regardless of the case, you and your teams end up having a code that will degrade your self-love — and for good reasons.
Overcomplicated business logic is an indirect security risk because, by nature, it makes everything harder. It is harder to read, debug, and change. Therefore, many things can get overlooked.
The bad news is, that there is no quick fix or a checklist for this problem. As a rule of thumb, a developer must go through the challenges of design patterns, code reviews, and budget constraints. Through this journey, one could realize that they are destined to search for the holy simplicity. Yet it is to be a lifelong search.
According to the prophecy, once the destiny is fulfilled, the developer gets the true KISS that can only be received by embracing simplicity in all aspects of his or her life.
So, keep on looking for simplicity in your code.
Use HTTPS instead of HTTP
Right off the bat, HTTP is insecure. It is so bad that any listener on the network would see the plain text being transferred. If you are dealing with any user data, it is obvious that you can’t let that happen.
Its successor, HTTPS, is the encrypted version of HTTP. So, listeners on the network won’t see the plain text but a bunch of random characters. While this is still not impossible to break, it provides a passive security measure against attackers.
A diagram showing the greater security coming with the HTTPS connection. Source: Cascading Media
HTTPS is used almost everywhere now. Flutter apps use HTTPS by default since version 2.0. It is still possible to use HTTP for debug purposes, though
Here you can read more about Flutter’s network policy.
Clean up local data
Every application works differently. Although many platforms manage tons of data locally as well as provide offline support, the logout means the same in every case, that is end the user session and delete every bit of information that belonged to the user.
Here things get a bit complicated. A software project uses all sorts of tokens, IDs, and preferences that are saved locally for a particular user and a particular device.
You and your teams are responsible for keeping the user data safe. So, make sure you are flushing everything down the drain after a log-out.
Digging deeper: Advanced security tips
After you’re done with the preliminary housekeeping, it’s worth digging deeper into the code and app setup to identify other weak points to address.
Prevent tampering and reverse engineering
You can hear these two terms often and they are somewhat related to each other.
The goal of reverse engineering is to understand how the app works. Code tampering is the attempt to change it.
An attacker would try to understand the code first, change how the app behaves afterward or introduce additional processes that are malicious.
Technically, all mobile code can be reverse-engineered and vulnerable to code tampering. This is because a mobile application runs on a client device susceptible to a number of inspection tools like simulators, SSL proxy tools, or decompilers...
A possible cycle of code tampering via a reverse engineering process
While it is impossible to make an application bulletproof, we can surely make things more difficult for an attacker.
- Check for integrity
There are a few things to ensure before we determine that our application runs on a trusty device.
- Check if it is a physical device,
- Check if the app is installed via Google Play Store or Apple store,
- Check if the device is rooted or jailbroken.
Checking for integrity provides a natural defense against suspicious devices and unofficial or tampered-with installations.
- Obfuscate the code
Code obfuscation is the process of deliberately modifying an app’s binary code in order to make it unreadable to humans. It sounds similar to minification but the purpose of minification is to shrink the size of the code, and obfuscating is to confuse and make the code unreadable.
So just for demonstration purposes, I played with an APK decompiler online. The tool uses a JADX decompiler that decompiled every APK that I uploaded within seconds. Let’s look at the comparison.
I used the three .apk versions of the same application:
- App in debug mode,
- App in release mode (which uses the R8 engine by default),
- App in rRelease mode with a specific obfuscator (ProGuard).
After the decompilation was over, I went through the folders and compared the generated set of files. Obviously, there is a huge difference between debug and release modes. However, the R8 and the ProGuard release modes both gave similar results.
Structure of the decompiled apps in debug mode (left-hand side) and the release mode (right-hand side)
The classes, folders, and method names are all renamed cryptically but the main target of obfuscation is the app’s binary code (which has a .so extension). Going through the .so files I could point out that the debug mode provides human-readable C++ code, whereas the release mode just drops random bits of code.
Here you can see a snapshot of the content from the debug mode:
Below you can see a snapshot of the release mode content oth ProGuard and R8 releases generated similar, obsfucated outcomes.
For more details about Flutter’s code obfuscating engine, see the Flutter docs on shrinking your code with R8.
Use certificate pinning
It’s one of the high-level guards against man-in-the-middle attacks. However, there is an important warning in the Android documentation about SSL:
Certificate Pinning is not recommended for Android applications due to the high risk of future server configuration changes, such as changing to another Certificate Authority, rendering the application unable to connect to the server without receiving a client software update.
Let’s say we ignore all the warnings because we develop a fintech app where we plan to enforce app updates regularly and already use HTTPS for any communication within the app. Then we can also afford to have SSL certificate pinning without worrying about older versions of the app.
Block screenshots and screen capturing
Some applications may contain sensitive content such as one requiring copyrights or they may include private conversations that shouldn’t be revealed. In this case, screenshot blocking may be added to your app as an additional security measure.
This can be easily achieved on Android devices, by playing with WindowManager flags but not so easy to achieve on iOS. This is because the iOS platform does not provide a way to block screenshots.
As far as I know, there is a paid solution (ScreenSheildKit) that is claimed to prevent screenshots via converting the screen into a DRM-protected video (whereby screenshots are not readable).
Apply two-factor authentication
Two-factor authentication (2FA) is an additional step to complete certain processes within the app. Just like the code sent to your phone or the biometrics (fingerprint or face identification), 2FA is nowadays most often used in login or 3-D Secure (3DS) payment systems.
A possible flow of 2FA authentication
As you can see, the 2FA provides yet another layer that an attacker must go through that requires a security measure different from the simple username and a password. There is a wide variety of security measures that can be used in 2FA. However, we can mainly group them into non-biometric (something that a user has) and biometric (something that represents a user’s physical trait). These methods ensure that the user is granted access only after they provide two or more pieces of evidence of their identity.
Microsoft concluded that 2FA works, blocking 99.9% of account compromise attacks. If a service provider supports multi-factor authentication, Microsoft recommends using it, even if it's as simple as SMS-based, one-time passwords.
Effectiveness of two-factor authentication in preventing security threats. Source: Microsoft
Nip errors and anomalies in the bud
Errors and anomalies are another window of opportunity for bad actors. To prevent attacks, use error reporting tools such as Sentry or Firebase Crashlytics for investigating anomalies. These tools may log exceptions, crashes, as well as failed HTTP requests, all with the corresponding information about the client and device. So, if we analyze these carefully, some of the irregularities can indicate security issues existing in your application. Interested in learning how Sentry’s error tracking and reporting helped create a sustainability-oriented app? Check out the Treasure Earth case study!
Have your app pen-tested
Mobile penetration testing is a testing methodology that is built on OWASP mobile security verification standards, and it focuses mainly on client-side security. It is becoming more and more popular, due to the increasing volume of mobile apps and the accompanying security threats.
By conducting a penetration test, companies gain insights into their apps’ vulnerabilities and can take action to fix them before the attackers exploit their platforms’ weak points.
Final thoughts on how to stay on the safe side
As a rule of thumb, you and your teams should treat an app as a liability because it lives on the client-side. While the best way to secure secrets is not to have them in the first place, once you need to store sensitive information locally, it pays off to know the ins and outs of the framework your teams are using.
Flutter is a cross-platform UI framework that runs on a wide variety of devices and operating systems. However, each platform still has its own rules of safety and limitations as well as threats. Therefore, a Flutter app should always be developed in line with these. As a result, the development team must have sufficient knowledge about each platform for which they are developing an app. Thanks for reading!
- Mobile Security Testing Guide
- Why is HTTP not secure? | HTTP vs. HTTPS
- Second-Factor Authentication with Facial Biometrics – Use Case 7/22
Looking for more app security tips? Check out our security-themed Insights section!