Mobile Development

Mobile App Security: Common Flutter & React Native Flaws

The Debuggers Engineering Team
12 min read

TL;DR

  • SharedPreferences and AsyncStorage are NOT secure. Tokens and secrets must go in flutter_secure_storage or react-native-keychain
  • Certificate pinning is essential for apps handling financial or personal data - without it, man-in-the-middle attacks are trivial
  • Hardcoded API keys in mobile apps can be extracted in minutes. Use server-side proxies or build-time configuration
  • Always validate JWT expiry on the client before making API calls, and implement refresh token rotation

Table of Contents

Mobile security concept with encrypted data on a smartphone screen

Insecure Token Storage

This is the single most common mobile security mistake. Developers store authentication tokens in SharedPreferences (Flutter) or AsyncStorage (React Native) because it is easy. Both are insecure.

SharedPreferences on Android stores data as an unencrypted XML file in the app's private directory. On a rooted device, this file is readable. On a non-rooted device, ADB backup can extract it. If a user installs a malicious app that exploits a privilege escalation vulnerability, your tokens are exposed.

AsyncStorage in React Native has the same problem. On Android, it uses SQLite with no encryption. On iOS, it uses NSUserDefaults, which is not encrypted at rest on devices without a passcode.

The Correct Solution

Flutter:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureTokenStorage {
  final _storage = const FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
  );

  Future<void> saveToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }

  Future<String?> getToken() async {
    return await _storage.read(key: 'auth_token');
  }

  Future<void> deleteToken() async {
    await _storage.delete(key: 'auth_token');
  }
}

React Native:

import * as Keychain from 'react-native-keychain';

async function saveToken(token: string): Promise<void> {
  await Keychain.setGenericPassword('auth', token, {
    service: 'com.yourapp.auth',
    accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
    accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
  });
}

async function getToken(): Promise<string | null> {
  const credentials = await Keychain.getGenericPassword({
    service: 'com.yourapp.auth',
  });
  return credentials ? credentials.password : null;
}

Both solutions use the OS-level keystore (Android Keystore / iOS Keychain), which provides hardware-backed encryption on supported devices.

JWT Handling in Mobile Apps

JWT tokens in mobile apps have unique challenges compared to web apps. Use our JWT debugger to inspect your tokens and verify their claims before implementing client-side logic.

Client-Side Expiry Validation

Always check token expiry before making API calls to avoid unnecessary 401 responses:

// Flutter JWT expiry check
import 'dart:convert';

bool isTokenExpired(String token) {
  final parts = token.split('.');
  if (parts.length != 3) return true;

  final payload = json.decode(
    utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
  );

  final exp = payload['exp'] as int?;
  if (exp == null) return true;

  // Add 30-second buffer to account for clock skew
  return DateTime.now().millisecondsSinceEpoch / 1000 > exp - 30;
}

Refresh Token Rotation

Implement refresh token rotation where each refresh request invalidates the previous refresh token. This limits the damage of a stolen token:

  1. Access token expires (short-lived, 15-30 minutes)
  2. Client sends refresh token to get a new access token
  3. Server issues new access token AND new refresh token
  4. Server invalidates the old refresh token

If an attacker steals a refresh token and the legitimate user also uses it, the server detects the reuse and invalidates all tokens for that user.

Authentication flow diagram showing JWT token refresh process

Certificate Pinning

Without certificate pinning, your app trusts any certificate signed by any CA (Certificate Authority) installed on the device. An attacker who installs a custom CA certificate (via a corporate proxy, public WiFi captive portal, or social engineering) can intercept all HTTPS traffic.

Flutter:

import 'package:dio/dio.dart';
import 'package:dio/io.dart';

Dio createSecureClient() {
  final dio = Dio();
  
  (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
    final client = HttpClient();
    client.badCertificateCallback = (cert, host, port) {
      // Compare certificate fingerprint against known pin
      final fingerprint = cert.sha256Fingerprint;
      return fingerprint == 'YOUR_CERTIFICATE_SHA256_HASH';
    };
    return client;
  };

  return dio;
}

React Native:

// Using react-native-ssl-pinning
import { fetch as sslFetch } from 'react-native-ssl-pinning';

const response = await sslFetch('https://api.yourapp.com/data', {
  method: 'GET',
  sslPinning: {
    certs: ['your_cert_hash'],
  },
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

Certificate pinning requires updating your app when certificates rotate, but the security benefit for apps handling financial or health data is worth the operational overhead.

Code Obfuscation and Protection

Flutter: Dart compiled to native code via AOT compilation makes reverse engineering harder than JavaScript, but not impossible. Enable obfuscation with build flags:

flutter build apk --obfuscate --split-debug-info=./debug-info/
flutter build ipa --obfuscate --split-debug-info=./debug-info/

Store the debug-info directory securely for crash report symbolication but never include it in the release bundle.

React Native: The JavaScript bundle can be extracted and read from the APK/IPA. Use Metro's minification (enabled by default in release builds) and consider Hermes bytecode compilation, which makes the code harder (but not impossible) to reverse engineer.

For additional protection, use our AES encryption tool to encrypt sensitive configuration strings that you embed in the app binary. Decrypt them at runtime using a key derived from the device keystore.

API Key Exposure

Every API key hardcoded in a mobile app can be extracted. Decompiling an APK takes minutes with tools like jadx or apktool. Even obfuscated keys can be found through string analysis or network traffic monitoring.

Do not do this:

const stripeKey = 'sk_live_abc123...'; // Extractable from binary
const mapsKey = 'AIzaSy...'; // Extractable from binary

Better alternatives:

  1. Server-side proxy: Route all third-party API calls through your own backend. The mobile app authenticates with your server, and your server calls Stripe, Google Maps, etc. with the real API keys.
  2. Build-time injection: Store keys in CI/CD environment variables and inject them during build. They still end up in the binary but are not in source control.
  3. Remote configuration: Fetch sensitive configuration from a secure server endpoint after authentication. Keys never exist in the app binary.

Jailbreak and Root Detection

Jailbroken iOS devices and rooted Android devices bypass OS security measures that your app depends on (sandbox isolation, keystore protection).

Flutter (flutter_jailbreak_detection):

import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart';

Future<bool> isDeviceCompromised() async {
  return await FlutterJailbreakDetection.jailbroken;
}

React Native (jail-monkey):

import JailMonkey from 'jail-monkey';

const isCompromised = JailMonkey.isJailBroken();

Detection is not foolproof. Sophisticated users can bypass detection libraries. Use it as one layer in a defence-in-depth strategy, not as your sole protection. For financial apps, combine jailbreak detection with server-side risk scoring.

Mobile device security testing with developer tools visible

Screenshot Prevention

For apps displaying sensitive data (banking, health records, passwords), prevent screenshots on sensitive screens:

Flutter:

import 'package:flutter_windowmanager/flutter_windowmanager.dart';

@override
void initState() {
  super.initState();
  FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE);
}

@override
void dispose() {
  FlutterWindowManager.clearFlags(FlutterWindowManager.FLAG_SECURE);
  super.dispose();
}

React Native (Android):

// In your MainActivity.java
import android.view.WindowManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().setFlags(
        WindowManager.LayoutParams.FLAG_SECURE,
        WindowManager.LayoutParams.FLAG_SECURE
    );
}

iOS does not have an equivalent system flag but you can detect screenshots via UIApplication.userDidTakeScreenshotNotification and blur the screen during app switcher.

Penetration Testing Mobile Apps

Before releasing any app that handles sensitive data, conduct a mobile penetration test:

  1. Static analysis: Decompile the binary and search for hardcoded secrets, insecure API endpoints, and debug code
  2. Network analysis: Use a proxy (Burp Suite, mitmproxy) to intercept HTTPS traffic. If you can see the traffic, certificate pinning is not working
  3. Storage analysis: Check all files the app creates on disk for unencrypted sensitive data
  4. Authentication testing: Try using expired tokens, modified tokens, and tokens from other users
  5. Input validation: Send malformed data to every API endpoint the app calls

For testing your mobile backend API endpoints during security review, use our free API Request Tester. And for generating secure hash values, use our Hash Generator.

The Debuggers provides mobile security consulting for Flutter and React Native applications, from architecture review through penetration testing.

For more on mobile framework comparisons, see our Flutter vs React Native 2026 guide.

Frequently Asked Questions

Is Flutter more secure than React Native?

Flutter has a slight edge because Dart AOT compilation produces native machine code that is harder to decompile than React Native's JavaScript bundle. However, the security of a mobile app depends far more on the developer's practices (secure storage, certificate pinning, input validation) than on the framework itself. Both Flutter and React Native can produce secure or insecure apps depending on implementation.

Do I need certificate pinning for every app?

Certificate pinning is essential for apps that handle financial data, health records, authentication credentials, or any personally identifiable information. For apps that only display non-sensitive content (news readers, weather apps), the overhead of certificate management may not be justified. The risk assessment depends on what data traverses the network connection.

How do I securely store API keys in a mobile app?

You cannot securely store API keys in a mobile app binary. Any key embedded in the app can be extracted with sufficient effort. The correct approach is to never expose sensitive API keys to the mobile client. Route all calls through your own backend server, which holds the real keys. The mobile app authenticates with your server using user credentials, and your server proxies the requests to third-party APIs.

Should I use biometric authentication in my Flutter or React Native app?

Biometric authentication is a strong usability improvement but should supplement, not replace, primary authentication. Use biometrics under these conditions: the device has biometric hardware, the user has opted in, and you fall back to passcode if biometrics fail. Store the authentication token in the secure keystore and gate access behind biometric verification. Never use biometrics as the only authentication factor for sensitive operations.


Inspect your JWT tokens

Use our free JWT Debugger to decode, inspect, and verify JWT tokens from your mobile app. Check claims, expiry, and signatures instantly, all processed locally in your browser.

Need a mobile security review? The Debuggers provides security consulting for Flutter and React Native applications.

Need Help Implementing This in a Real Project?

Our team supports end-to-end development for web and mobile software, from architecture to launch.

mobile app security flutter react nativeflutter securityreact native securitymobile app authenticationsecure storage mobile

Found this helpful?

Join thousands of developers using our tools to write better code, faster.