Security

Mobile App Code Review: React Native and Flutter Security Checklist

Tony Dong
June 7, 2025
12 min read
Share:
Featured image for: Mobile App Code Review: React Native and Flutter Security Checklist

Cross-platform mobile development with React Native and Flutter has accelerated app development, but it's also introduced unique security challenges that traditional mobile security doesn't cover. From bridge vulnerabilities to insecure storage patterns, this comprehensive guide covers the mobile-specific security issues every code reviewer needs to identify in cross-platform applications.

Key Takeaways

  • Platform-specific vulnerabilities: React Native and Flutter introduce unique attack vectors through their bridge architecture and native code interfaces.
  • Secure storage is critical: Mobile apps require specialized secure storage solutions beyond traditional web security practices.
  • Deep linking requires validation: URL schemes and universal links create attack vectors that need careful validation and sanitization.

Mobile Security Landscape

Cross-platform mobile frameworks like React Native and Flutter bring web development paradigms to mobile, but this also introduces security challenges that don't exist in native development. Understanding these unique attack vectors is essential for effective security reviews.

Common Mobile Security Threats

🚫 High-Risk Vulnerabilities

  • • Insecure local storage
  • • Unvalidated deep links
  • • Bridge injection attacks
  • • Certificate pinning bypasses
  • • Debug builds in production
  • • Exposed API keys
  • • Weak encryption implementation
  • • Insufficient session management

✅ Security Best Practices

  • • Secure keychain/keystore usage
  • • Proper input validation
  • • Certificate pinning
  • • Obfuscated production builds
  • • Secure communication protocols
  • • Runtime application protection
  • • Biometric authentication
  • • Secure session handling

⚠️ Mobile Security Mindset

Mobile devices are inherently untrusted environments. Assume your app will be reverse-engineered, modified, and run on compromised devices. Design your security accordingly.

React Native Security Vulnerabilities

React Native's bridge architecture creates unique security considerations. The JavaScript-to-native communication layer can be exploited if not properly secured.

Bridge Security Issues

🚫 Bridge Injection Vulnerability

// VULNERABLE - Unvalidated bridge communication
import { NativeModules } from 'react-native';
const { FileManager } = NativeModules;

// User input directly passed to native module
const downloadFile = (url) => {
  FileManager.downloadFile(url); // No validation!
};

// Dynamic method invocation vulnerability
const executeNativeMethod = (methodName, params) => {
  NativeModules.CustomModule[methodName](...params);
};

// Exposed sensitive functionality
const deleteAllUserData = () => {
  NativeModules.StorageModule.clearAllData();
};

✅ Secure Bridge Implementation

// SECURE - Validated bridge communication
import { NativeModules } from 'react-native';
const { FileManager } = NativeModules;

const ALLOWED_DOMAINS = ['https://api.myapp.com', 'https://cdn.myapp.com'];
const URL_PATTERN = /^https:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9._/-]+$/;

const downloadFile = (url) => {
  // Validate URL format
  if (!URL_PATTERN.test(url)) {
    throw new Error('Invalid URL format');
  }
  
  // Check against whitelist
  const isAllowed = ALLOWED_DOMAINS.some(domain => url.startsWith(domain));
  if (!isAllowed) {
    throw new Error('URL not in allowed domains');
  }
  
  FileManager.downloadFile(url);
};

// Secure method mapping with validation
const ALLOWED_METHODS = ['getUserProfile', 'updateSettings'];
const executeNativeMethod = (methodName, params) => {
  if (!ALLOWED_METHODS.includes(methodName)) {
    throw new Error('Method not allowed');
  }
  
  // Validate parameters
  if (!validateParams(methodName, params)) {
    throw new Error('Invalid parameters');
  }
  
  NativeModules.CustomModule[methodName](...params);
};

React Native Storage Security

🚫 Insecure Storage Patterns

// VULNERABLE - Storing sensitive data in AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';

// Plaintext storage of sensitive data
const storeUserCredentials = async (username, password) => {
  await AsyncStorage.setItem('username', username);
  await AsyncStorage.setItem('password', password); // NEVER do this!
};

// Storing API keys in plain text
const API_KEY = 'sk-1234567890abcdef'; // Exposed in JavaScript bundle
const storeApiKey = async () => {
  await AsyncStorage.setItem('apiKey', API_KEY);
};

✅ Secure Storage Implementation

// SECURE - Using react-native-keychain for sensitive data
import * as Keychain from 'react-native-keychain';
import { encrypt, decrypt } from './crypto-utils';

// Secure credential storage
const storeUserCredentials = async (username, password) => {
  await Keychain.setInternetCredentials(
    'MyApp',
    username,
    password,
    {
      accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
      authenticatePrompt: 'Authenticate to access your account',
    }
  );
};

// Encrypted storage for less sensitive data
const storeEncryptedData = async (key, data) => {
  const encryptedData = await encrypt(JSON.stringify(data));
  await AsyncStorage.setItem(key, encryptedData);
};

const getEncryptedData = async (key) => {
  const encryptedData = await AsyncStorage.getItem(key);
  if (encryptedData) {
    const decryptedData = await decrypt(encryptedData);
    return JSON.parse(decryptedData);
  }
  return null;
};

Flutter Security Considerations

Flutter's architecture differs from React Native, using a compiled Dart runtime instead of a JavaScript bridge. This creates different security considerations and attack vectors.

Flutter Platform Channel Security

🚫 Insecure Platform Channel Usage

// VULNERABLE - Unvalidated platform channel communication
import 'package:flutter/services.dart';

class InsecureFileManager {
  static const platform = MethodChannel('file_manager');
  
  // Direct user input to platform channel
  static Future<void> deleteFile(String filePath) async {
    await platform.invokeMethod('deleteFile', filePath); // No validation!
  }
  
  // Exposing sensitive system operations
  static Future<void> executeSystemCommand(String command) async {
    await platform.invokeMethod('systemCommand', command);
  }
  
  // Uncontrolled file access
  static Future<String> readAnyFile(String path) async {
    return await platform.invokeMethod('readFile', path);
  }
}

✅ Secure Platform Channel Implementation

// SECURE - Validated platform channel communication
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;

class SecureFileManager {
  static const platform = MethodChannel('secure_file_manager');
  static const allowedDirectories = ['/app/documents', '/app/cache'];
  
  static Future<void> deleteFile(String filePath) async {
    // Validate file path
    if (!_isPathAllowed(filePath)) {
      throw ArgumentError('File path not allowed: $filePath');
    }
    
    // Sanitize path to prevent directory traversal
    final normalizedPath = path.normalize(filePath);
    if (normalizedPath != filePath) {
      throw ArgumentError('Invalid file path');
    }
    
    await platform.invokeMethod('deleteFile', normalizedPath);
  }
  
  static bool _isPathAllowed(String filePath) {
    return allowedDirectories.any((dir) => filePath.startsWith(dir));
  }
  
  static Future<String> readUserFile(String fileName) async {
    // Only allow reading from user documents directory
    final safePath = path.join('/app/documents', path.basename(fileName));
    
    if (!_isPathAllowed(safePath)) {
      throw ArgumentError('Access denied');
    }
    
    return await platform.invokeMethod('readFile', safePath);
  }
}

Flutter Secure Storage

🔒 Flutter Secure Storage Best Practices

// Using flutter_secure_storage for sensitive data
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorageManager {
  static const storage = FlutterSecureStorage(
    aOptions: AndroidOptions(
      encryptedSharedPreferences: true,
      keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_PKCS1Padding,
      storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding,
    ),
    iOptions: IOSOptions(
      accessibility: IOSAccessibility.first_unlock_this_device,
      accountName: 'MyApp',
    ),
  );
  
  static Future<void> storeToken(String token) async {
    await storage.write(key: 'auth_token', value: token);
  }
  
  static Future<String?> getToken() async {
    return await storage.read(key: 'auth_token');
  }
  
  static Future<void> clearAll() async {
    await storage.deleteAll();
  }
}

Deep Linking Security

Deep linking is a common attack vector in mobile applications. Both React Native and Flutter apps need to properly validate and sanitize deep link parameters to prevent injection attacks.

Deep Link Vulnerability Patterns

🚫 Vulnerable Deep Link Handling

// VULNERABLE - React Native
import { Linking } from 'react-native';

// Direct use of deep link parameters
Linking.addEventListener('url', (event) => {
  const url = event.url;
  const params = parseURL(url);
  
  // No validation - dangerous!
  if (params.userId) {
    fetchUserData(params.userId);
  }
  
  // XSS vulnerability in webview
  if (params.html) {
    webView.injectJavaScript(params.html);
  }
});

✅ Secure Deep Link Handling

// SECURE - React Native
import { Linking } from 'react-native';

const ALLOWED_SCHEMES = ['myapp://'];
const USER_ID_PATTERN = /^[a-zA-Z0-9-]+$/;

Linking.addEventListener('url', (event) => {
  const url = event.url;
  
  // Validate URL scheme
  if (!ALLOWED_SCHEMES.some(scheme => url.startsWith(scheme))) {
    return; // Ignore unauthorized schemes
  }
  
  const params = parseURL(url);
  
  // Validate parameters
  if (params.userId) {
    if (!USER_ID_PATTERN.test(params.userId)) {
      console.warn('Invalid user ID format');
      return;
    }
    fetchUserData(params.userId);
  }
  
  // Never inject untrusted content
  // Use safe navigation instead
  if (params.page) {
    navigateToPage(params.page);
  }
});

Flutter Deep Link Security

// Secure deep link handling in Flutter
class DeepLinkHandler {
  static const allowedHosts = ['myapp.com', 'api.myapp.com'];
  static final userIdPattern = RegExp(r'^[a-zA-Z0-9_-]+$');
  
  static Future<void> handleDeepLink(String link) async {
    try {
      final uri = Uri.parse(link);
      
      // Validate host
      if (!allowedHosts.contains(uri.host)) {
        throw ArgumentError('Invalid host: ${uri.host}');
      }
      
      // Validate and sanitize parameters
      final params = uri.queryParameters;
      
      if (params.containsKey('userId')) {
        final userId = params['userId']!;
        if (!userIdPattern.hasMatch(userId)) {
          throw ArgumentError('Invalid userId format');
        }
        await _navigateToUser(userId);
      }
      
      if (params.containsKey('token')) {
        final token = params['token']!;
        await _validateAndStoreToken(token);
      }
      
    } catch (e) {
      // Log security violations
      print('Deep link security violation: $e');
      // Navigate to safe default
      await _navigateToHome();
    }
  }
  
  static Future<void> _validateAndStoreToken(String token) async {
    // Implement proper token validation
    if (token.length < 32) {
      throw ArgumentError('Invalid token format');
    }
    
    // Store securely
    await SecureStorageManager.storeToken(token);
  }
}

Network Security

Mobile apps often communicate with multiple APIs and services. Implementing proper network security, including certificate pinning and secure communication protocols, is essential.

Certificate Pinning Implementation

🔒 React Native Certificate Pinning

// Using react-native-cert-pinner
import { CertPinner } from 'react-native-cert-pinner';

const pinnedDomains = {
  'api.myapp.com': {
    includeSubdomains: true,
    pins: [
      'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Primary cert
      'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // Backup cert
    ]
  }
};

// Initialize certificate pinning
CertPinner.pin(pinnedDomains);

// Secure fetch with pinning
const secureFetch = async (url, options) => {
  try {
    const response = await fetch(url, {
      ...options,
      // Certificate pinning is handled automatically
    });
    return response;
  } catch (error) {
    if (error.message.includes('certificate')) {
      // Certificate pinning failure - potential MITM attack
      console.error('Certificate pinning failed:', error);
      throw new Error('Secure connection failed');
    }
    throw error;
  }
};

🔒 Flutter Certificate Pinning

// Using certificate pinning in Flutter
import 'package:dio/dio.dart';
import 'package:dio_certificate_pinning/dio_certificate_pinning.dart';

class SecureHttpClient {
  late Dio _dio;
  
  SecureHttpClient() {
    _dio = Dio();
    
    // Add certificate pinning interceptor
    _dio.interceptors.add(
      CertificatePinningInterceptor(
        allowedSHAFingerprints: [
          'AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA',
          'BB:BB:BB:BB:BB:BB:BB:BB:BB:BB:BB:BB:BB:BB:BB:BB',
        ],
      ),
    );
    
    // Add other security headers
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          options.headers['User-Agent'] = 'MyApp/1.0';
          options.headers['X-Requested-With'] = 'XMLHttpRequest';
          handler.next(options);
        },
      ),
    );
  }
  
  Future<Response> secureGet(String endpoint) async {
    try {
      return await _dio.get(endpoint);
    } on DioError catch (e) {
      if (e.type == DioErrorType.other && 
          e.message.contains('certificate')) {
        throw Exception('Certificate validation failed');
      }
      rethrow;
    }
  }
}

Build Security and Obfuscation

Production builds should include proper obfuscation and security configurations to make reverse engineering more difficult.

React Native Build Security

Security MeasureImplementationEffectiveness
JavaScript Obfuscationreact-native-obfuscatorHigh
Asset Encryptionreact-native-asset-encryptionMedium
Debug Detectionreact-native-anti-debugMedium
Root/Jailbreak Detectionreact-native-root-protectionHigh

Flutter Build Security

// Flutter build configuration for security
// android/app/build.gradle
android {
    buildTypes {
        release {
            // Enable obfuscation
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            
            // Signing configuration
            signingConfig signingConfigs.release
            
            // Remove debug info
            debuggable false
            jniDebuggable false
            renderscriptDebuggable false
        }
    }
}

// ios/Runner.xcodeproj settings
// Enable bitcode and app thinning
// Set ENABLE_BITCODE = YES
// Set STRIP_INSTALLED_PRODUCT = YES

// Flutter command for secure release build
// flutter build apk --release --obfuscate --split-debug-info=build/app/outputs/symbols
// flutter build ios --release --obfuscate --split-debug-info=build/ios/outputs/symbols

Runtime Security Monitoring

Implementing runtime application self-protection (RASP) helps detect and prevent attacks while your app is running in production.

Security Monitoring Implementation

🔍 React Native Monitoring

  • • Debug detection and response
  • • API tampering detection
  • • Screenshot prevention
  • • Suspicious behavior analytics
  • • Real-time threat response

🛡️ Flutter Monitoring

  • • Native code integrity checks
  • • Platform channel monitoring
  • • Emulator detection
  • • Runtime code injection prevention
  • • Behavioral anomaly detection

Mobile Security Review Checklist

📱 Complete Mobile Security Review

Data Storage: Sensitive data stored in secure keychain/keystore, not plain text
Network Security: Certificate pinning implemented for API communications
Deep Link Validation: All URL schemes and parameters properly validated
Bridge Security: Native bridge communications validated and restricted
Build Configuration: Production builds obfuscated with debug features disabled
Runtime Protection: Anti-debugging and tampering detection implemented
Authentication: Proper session management and biometric integration
Code Quality: No hardcoded secrets, proper input validation throughout

Platform-Specific Security Tools

Different platforms require specialized security tools for comprehensive protection and analysis.

React Native

• Flipper Security Plugin

• react-native-keychain

• react-native-cert-pinner

• react-native-obfuscator

Flutter

• flutter_secure_storage

• dio_certificate_pinning

• flutter_jailbreak_detection

• local_auth (biometrics)

Cross-Platform

• MobSF (static analysis)

• OWASP ZAP

• Burp Suite Mobile

• Frida (dynamic analysis)

📱 Mobile security requires platform-specific expertise and continuous vigilance.

Secure Your Mobile Apps

Propel's AI automatically detects mobile security vulnerabilities in React Native and Flutter code during review, protecting your users from common attack vectors.

Explore More

Propel AI Code Review Platform LogoPROPEL

The AI Tech Lead that reviews, fixes, and guides your development team.

SOC 2 Type II Compliance Badge - Propel meets high security standards

Company

© 2025 Propel Platform, Inc. All rights reserved.