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 Measure | Implementation | Effectiveness |
---|---|---|
JavaScript Obfuscation | react-native-obfuscator | High |
Asset Encryption | react-native-asset-encryption | Medium |
Debug Detection | react-native-anti-debug | Medium |
Root/Jailbreak Detection | react-native-root-protection | High |
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
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.