🎯 Why This Guide Exists (And Why You Need It)
Mobile-specific attack vectors you must defend against:
🔓 Certificate pinning bypass → MITM attacks on API calls
🧩 Binary reverse engineering → hardcoded secrets extraction
🎣 Deep link hijacking → session takeover via malicious apps
💾 Insecure storage → sensitive data in plain text files
👁️ Biometric bypass → fake authentication flows
📹 Screen recording → credential harvesting via malware
🔐 1) Secure Data Storage & Key Management
Protect data at rest — even on rooted/jailbroken devices
Never store secrets in AsyncStorage or SharedPrefs. Use platform-secure storage (iOS Keychain / Android Keystore) + biometric fallbacks.
❌ What NOT to Do: Plain Text Storage
Expo (Vulnerable)
// 🚫 NEVER DO THIS await AsyncStorage.setItem('api_key', 'sk_live_xxx'); // Accessible to malware
Flutter (Vulnerable)
// 🚫 NEVER DO THIS prefs.setString('refresh_token', token); // Stored in plain text XML/plist
✅ What TO Do: Hardware-Backed Secure Storage
Expo (Fix)
await SecureStore.setItemAsync('api_key', apiKey, {
requireAuthentication: true,
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
});Flutter (Fix)
await _storage.write(
key: 'api_key',
value: apiKey,
iOptions: IOSOptions(accessibility: IOSAccessibility.when_passcode_set_this_device_only)
);🔐 Pro Tip: Always implement encrypted fallbacks using user-derived keys (via biometrics) if SecureStore fails.
🚫 Attack → Fix: Hardcoded Secrets in Binary
Hardcoded keys = low-hanging fruit for reverse engineers. Derive keys at runtime or fetch them securely from your backend.
❌ Expo (Vulnerable)
const deviceId = await getSecureDeviceId(); // Hashed device fingerprint
const response = await fetch('/auth/device', { body: JSON.stringify({ deviceId }) });
const { apiKey } = await response.json(); // Key never hardcoded🚫 BUNDLE EXPOSED
const API_KEY = 'sk_live_12345'; // Visible in JS bundle✅ Expo (Fix — Runtime Key Exchange)
const deviceId = await getSecureDeviceId(); // Hashed device fingerprint
const response = await fetch('/auth/device', { body: JSON.stringify({ deviceId }) });
const { apiKey } = await response.json(); // Key never hardcoded✅ Flutter (Fix)
final deviceId = await _generateSecureDeviceId(); // SHA256 of device + app signature
final response = await http.post('/auth/device', body: jsonEncode({ 'deviceId': deviceId })🔗 2) Network Security & Certificate Pinning
Certificate pinning ensures your app only talks to YOUR servers — even if the device has malicious CA certs installed.
❌ Expo (Vulnerable — No Pinning)
fetch('https://api.example.com/data'); // Trusts ANY valid cert → MITM possible✅ Expo (Fix — expo-ssl-pinning)
await NetworkingModule.fetch(url, {
sslPinning: {
certificateFilenames: ['AA:BB:CC:DD…', 'BB:CC:DD:EE…'], // Primary + backup
validateCertificateChain: true
}
});✅ Flutter (Fix — dio + certificate_pinning)
_dio.interceptors.add(
CertificatePinningInterceptor(
allowedSHAFingerprints: ['AA:BB:CC:DD…', 'BB:CC:DD:EE…']
)
);🛡️ Critical: Always implement pinning failure handlers — enter “security mode” and wipe tokens if pinning fails.
⛓️ 3) Deep Link Security & Intent Validation
Never trust deep link parameters. Validate, sanitize, rate-limit, and ALWAYS require user confirmation for sensitive actions.
❌ Expo (Vulnerable)
// 🚫 Executes transfer without confirmation if (path === '/transfer') executeTransfer(params.to, params.amount);
✅ Expo (Fix)
if (path === '/transfer') {
const confirmed = await showTransferConfirmation(params.to, params.amount);
if (confirmed) await executeSecureTransfer(…);
}✅ Flutter (Fix)
if (uri.path == '/transfer') {
final confirmed = await _showConfirmationDialog(…);
if (confirmed) await _executeSecureTransfer(…);
}✅ Must-Haves:
Regex-validate ALL parameters (no script injection!)
Rate limit actions per device
Require biometric auth for high-risk ops
Log all deep link incidents
4) Biometric Authentication Security
Stop photo spoofing, replay attacks, and root bypass
Biometric ≠ secure. Validate context, detect presentation attacks, and lock out after failures.
✅ Expo (Fix Highlights)
// Validate hardware-backed auth only
authenticationTypes: [FACIAL_RECOGNITION, FINGERPRINT, IRIS]
// Detect rapid replays
const timeDiff = Date.now() - parseInt(lastAuth);
if (timeDiff < 2000) indicators.push('rapid_successive_auth');
// Check device integrity (root/jailbreak)
if (await isDeviceRooted()) issues.push('device_rooted');✅ Flutter (Fix Highlights)
// Enforce secure biometrics only
final hasSecureBiometrics = availableBiometrics.any((b) =>
b == BiometricType.face || b == BiometricType.fingerprint);
// Lockout after 3 failures
if (attempts >= _maxAttempts) await setLockoutUntil(DateTime.now().add(_lockoutDuration));📹 5) Screen Security & Anti-Tampering
Block screenshots, screen recording, and overlay attacks. Use platform APIs to disable screen capture. Clear sensitive fields on background. Detect screenshot events.
✅ Expo (Fix)
await ScreenCapture.preventScreenCaptureAsync();
ScreenCapture.addScreenshotListener(() => handleSecurityViolation('screenshot_attempted'));await ScreenCapture.preventScreenCaptureAsync();
ScreenCapture.addScreenshotListener(() => handleSecurityViolation('screenshot_attempted'));✅ Flutter (Fix)
await ScreenProtector.protectDataLeakageOn();
if (Platform.isAndroid) await FlutterWindowManager.addFlags(FLAG_SECURE);🔐 Bonus: Build your own secure keypad (no system keyboard) to prevent keyloggers
🔄 6) API Security & Token Management
Access tokens = 5–15 mins max. Rotate refresh tokens. Sign every request. Validate context
✅ Expo (Fix Highlights)
// Store tokens with expiry
const expiresAt = Date.now() + 900000; // 15 mins
await SecureStore.setItemAsync('access_token', token);
// Auto-refresh before expiry
setTimeout(() => refreshTokens(), (expiresAt - Date.now() - 300000));
// Sign requests
const signature = await signRequest(endpoint, payload, timestamp);
headers['X-Signature'] = signature;🔥 7) Clipboard Hijacking & Data Leakage
On both Android and iOS, apps can read clipboard contents without explicit permission. Attackers exploit this to steal 2FA codes, passwords, crypto wallet addresses, and sensitive messages
❌ Expo (Vulnerable — No Clipboard Monitoring)
import { Clipboard } from 'expo-clipboard';
const PaymentScreen = () => {
const [walletAddress, setWalletAddress] = useState('');
useEffect(() => {
// ❌ Dangerous: Auto-paste from clipboard without user consent
const pasteFromClipboard = async () => {
const content = await Clipboard.getStringAsync();
if (content && content.startsWith('0x')) { // Assume Ethereum address
setWalletAddress(content); // Auto-fill = auto-leak
}
};
pasteFromClipboard();
}, []);
return (
<TextInput
value={walletAddress}
onChangeText={setWalletAddress}
placeholder="Paste crypto address"
/>
);
};
// 💣 Attack: Malware app runs in background, constantly reads clipboard.
// User copies 2FA code → malware steals it.
// User copies private key → sent to attacker server.✅ Expo (Fix — User-Initiated Paste + Sanitization)
import { Clipboard } from 'expo-clipboard';
import * as SecureStore from 'expo-secure-store';
const SecurePaymentScreen = () => {
const [walletAddress, setWalletAddress] = useState('');
const [clipboardMonitorActive, setClipboardMonitorActive] = useState(false);
const handlePaste = async () => {
try {
const content = await Clipboard.getStringAsync();
// Validate format BEFORE setting state
if (!/^0x[a-fA-F0-9]{40}$/.test(content)) {
Alert.alert('Invalid Address', 'Please paste a valid Ethereum address.');
return;
}
// Log paste event for audit
await logSecurityEvent('clipboard_paste', {
length: content.length,
timestamp: Date.now()
});
setWalletAddress(content);
// Clear clipboard after successful paste for security
await Clipboard.setStringAsync('');
} catch (error) {
console.error('Paste failed:', error);
}
};
const logSecurityEvent = async (type, details) => {
const event = { type, details, deviceId: await getDeviceId() };
await SecureStore.setItemAsync(`clipboard_event_${Date.now()}`, JSON.stringify(event));
};
return (
<View>
<TextInput
value={walletAddress}
onChangeText={setWalletAddress}
placeholder="Enter or paste address"
/>
<Button title="Paste from Clipboard" onPress={handlePaste} /> {/* User must tap */}
</View>
);
};✅ Flutter (Fix — Clipboard Listener + User Consent)
import 'package:flutter/services.dart';
class SecureClipboardHandler {
bool _isMonitoring = false;
StreamSubscription<String?>? _clipboardSubscription;
Future<void> startMonitoringWithConsent() async {
final confirmed = await showConsentDialog();
if (!confirmed) return;
_clipboardSubscription = ClipboardDataNotifier.addListener((data) {
_onClipboardChange(data?.text);
});
_isMonitoring = true;
}
void _onClipboardChange(String? content) {
if (content == null || content.isEmpty) return;
// Heuristic: if content looks like sensitive data, alert user
if (_isSensitiveContent(content)) {
_showSecurityAlert(content);
// Optionally auto-clear clipboard
Clipboard.setData(ClipboardData(text: ''));
}
}
bool _isSensitiveContent(String content) {
final patterns = [
r'^[0-9]{6}$', // 6-digit 2FA code
r'^0x[a-fA-F0-9]{40}$', // Ethereum address
r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$', // Password pattern
];
return patterns.any((pattern) => RegExp(pattern).hasMatch(content));
}
Future<bool> showConsentDialog() async {
return await showDialog<bool>(
context: navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: Text('Enable Clipboard Monitoring?'),
content: Text('This helps protect you from clipboard hijacking attacks.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text('No')),
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: Text('Yes')),
],
),
) ?? false;
}
void _showSecurityAlert(String content) {
showDialog(
context: navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: Text('⚠️ Security Alert'),
content: Text('An app may be trying to steal data from your clipboard.\n\nContent: ${content.substring(0, 20)}...'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
void dispose() {
_clipboardSubscription?.cancel();
}
}🎭 8) Accessibility Service Abuse (Android) / VoiceOver Exploits (iOS)
Malicious apps request Accessibility permissions to “help disabled users” — then use it to read your app’s entire UI hierarchy, extract text, and even auto-tap buttons (e.g., “Transfer All Funds”).
❌ Flutter (Vulnerable — No Accessibility Shield)
// ❌ No protection against accessibility overlay class BankingTransferScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Text('Transfer $500 to John'), // Readable by accessibility services ElevatedButton( onPressed: () => _executeTransfer(), // Can be auto-clicked by malware child: Text('CONFIRM TRANSFER'), ), ], ), ); } } // 💣 Attack: Malware enables Accessibility Service → reads screen → finds “CONFIRM TRANSFER” button → clicks it automatically.
✅ Flutter (Fix — Accessibility Shield + User Confirmation)
import 'package:flutter/material.dart';
class SecureTransferScreen extends StatefulWidget {
@override
_SecureTransferScreenState createState() => _SecureTransferScreenState();
}
class _SecureTransferScreenState extends State<SecureTransferScreen> {
bool _accessibilityDetected = false;
@override
void initState() {
super.initState();
_checkAccessibilityStatus();
}
Future<void> _checkAccessibilityStatus() async {
// Android: Check if any accessibility services are active
// iOS: Check if VoiceOver is enabled (less malicious, but still a risk)
final isAndroid = Platform.isAndroid;
bool accessibilityActive = false;
if (isAndroid) {
// Use native channel to query AccessibilityManager
final result = await MethodChannel('security_channel')
.invokeMethod('isAccessibilityActive');
accessibilityActive = result as bool? ?? false;
} else {
// iOS: Check VoiceOver
final result = await MethodChannel('security_channel')
.invokeMethod('isVoiceOverActive');
accessibilityActive = result as bool? ?? false;
}
if (accessibilityActive) {
setState(() {
_accessibilityDetected = true;
});
_showAccessibilityWarning();
}
}
void _showAccessibilityWarning() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('⚠️ Security Notice'),
content: Text(
'Accessibility services are active. '
'Malicious apps can use this to automate actions in your banking app. '
'Disable accessibility services not in use for maximum security.'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('I Understand'),
),
],
),
);
}
void _executeTransfer() {
if (_accessibilityDetected) {
// Require additional confirmation if accessibility is active
_requireBiometricReAuth();
} else {
_performTransfer();
}
}
void _requireBiometricReAuth() {
// Trigger biometric auth before allowing sensitive action
LocalAuthentication().authenticate(
localizedReason: 'Confirm transfer with biometrics',
).then((success) {
if (success) {
_performTransfer();
}
});
}
void _performTransfer() {
// Execute actual transfer logic
print('Transfer executed securely');
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
if (_accessibilityDetected)
Container(
color: Colors.red[100],
padding: EdgeInsets.all(16),
child: Text(
'⚠️ Accessibility Mode Active — Extra security enabled',
style: TextStyle(color: Colors.red[800]),
),
),
Text('Transfer $500 to John'),
ElevatedButton(
onPressed: _executeTransfer,
child: Text('CONFIRM TRANSFER'),
),
],
),
);
}
}For Expo/React Native, use react-native-accessibility or native modules to detect TalkBack/VoiceOver status.
📡 8) Wi-Fi & Bluetooth-Based Side-Channel Attacks
Sophisticated attackers can monitor Wi-Fi/BLE traffic patterns or signal strength to infer when users are active, what screens they’re on, or even guess PINs based on timing.
❌ Expo (Vulnerable — No Network Obfuscation)
import { NetInfo } from '@react-native-community/netinfo';
const LoginScreen = () => {
const [pin, setPin] = useState('');
const handlePinSubmit = async () => {
// ❌ Predictable network call after each digit? Easy to fingerprint.
if (pin.length === 6) {
const response = await fetch('/api/validate-pin', {
method: 'POST',
body: JSON.stringify({ pin })
});
// Response time reveals if PIN is correct (timing attack)
}
};
return (
<TextInput
value={pin}
onChangeText={(text) => {
setPin(text);
if (text.length === 6) handlePinSubmit(); // Predictable pattern
}}
/>
);
};✅ Expo (Fix — Constant-Time Validation + Traffic Padding)
const SecureLoginScreen = () => {
const [pin, setPin] = useState('');
const handlePinSubmit = async () => {
// ✅ Add random delay to prevent timing attacks
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500));
const response = await fetch('/api/validate-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin }),
});
const result = await response.json();
// ✅ Always take same code path regardless of success/failure
if (result.valid) {
navigateToHome();
} else {
// Show generic error — don’t reveal if PIN was partially correct
Alert.alert('Error', 'Invalid credentials. Please try again.');
}
};
// ✅ Optional: Send decoy “heartbeat” requests during idle time to obfuscate real traffic
useEffect(() => {
const interval = setInterval(async () => {
if (pin.length > 0 && pin.length < 6) {
// Send harmless, encrypted ping to server
await fetch('/api/heartbeat', {
method: 'POST',
headers: { 'X-Mock-Request': 'true' },
body: JSON.stringify({ noise: Math.random() })
});
}
}, 3000);
return () => clearInterval(interval);
}, [pin]);
return (
<TextInput
value={pin}
onChangeText={setPin}
maxLength={6}
keyboardType="numeric"
/>
);
};🧩 9) Dependency Confusion & Supply Chain Attacks
Prevent attackers from injecting malicious code via
Attackers publish malicious packages via npm with names similar to your internal/private packages. If your build system is misconfigured, it may accidentally pull the public (malicious) version instead of your private one.
❌ Flutter (Vulnerable pubspec.yaml)
dependencies:
flutter:
sdk: flutter
http: ^0.13.3
shared_utils: ^1.0.0 # ❌ If "shared_utils" exists publicly, you might pull attacker's version!
crypto: ^3.0.1✅ Flutter (Fix — Use Git/Private Repo URLs + Integrity Hashes)
dependencies:
flutter:
sdk: flutter
http: ^0.13.3
# ✅ Pin to internal Git repo — avoid public package names
shared_utils:
git:
url: https://github.com/yourcompany/flutter-shared-utils.git
ref: v1.0.0
# Optional: Use pub.dev’s --hash flag to generate integrity hash
crypto: ^3.0.1
# ✅ Add this to enforce package integrity (Flutter 3.16+)
dependency_overrides:
# Lock transitive dependencies to known good versions
meta: ^1.9.1
path: ^1.8.3✅ Expo (Fix — npm audit + lockfile + private registry)
# ✅ Use npm/yarn lockfiles religiously npm ci # instead of npm install — ensures exact versions # ✅ Audit dependencies regularly npm audit --production # ✅ Use scope for private packages dependencies: { "@yourcompany/shared-utils": "^1.0.0", // Harder to spoof "axios": "^1.6.0" } # ✅ Configure .npmrc to use private registry first @yourcompany:registry=https://npm.yourcompany.com/ registry=https://registry.npmjs.org/
✅ CI/CD Pipeline Guardrails (Both Platforms)
# GitHub Actions / GitLab CI Example
- name: Audit Dependencies
run: |
npm audit --audit-level=high --production # Expo
flutter pub deps | grep -i "unknown" # Flutter — check for unexpected deps
flutter pub outdated --mode=null-safety # Ensure no outdated packages
- name: Fail on High-Severity Vulnerabilities
run: |
if npm audit --json | jq '.metadata.vulnerabilities.high + .metadata.vulnerabilities.critical' | grep -q '[1-9]'; then
echo "🛑 High/Critical vulnerabilities found!"
exit 1
fi🧠 BONUS: Runtime Application Self-Protection (RASP)
Assume breach. Monitor memory, network, behavior. Auto-lockdown on critical threats
// Expo RASP Snippet
monitorDeviceIntegrity(); // Every 30s
monitorNetworkSecurity(); // Override fetch()
monitorAppBehavior(); // Detect brute force
if (threat.severity === 'CRITICAL') await enterLockdownMode(); // Wipe data, force reauth📊 Mobile Security Checklist

Produce by Napkin AI
❓ FAQ (AEO Snippet Optimized)
Q: What’s the #1 mobile security mistake devs make?
A: Storing secrets in AsyncStorage or SharedPrefs. Always use platform-secure storage.
Q: How do I prevent deep link hijacking?
A: Validate scheme + params, require user confirmation, rate limit, log incidents. Never auto-execute.
Q: Is biometric auth enough?
A: No. Combine with device integrity checks, presentation attack detection, and lockout policies.
Q: What if certificate pinning fails?
A: Enter security mode — wipe tokens, disable sensitive features, force reauth on trusted network.
🧭 Conclusion: Build Hack-Resilient by Design
Mobile security isn’t a feature — it’s a lifecycle.
Assume breach. Layer defenses. Monitor everything. Respond instantly.
Your users trust you with their fingerprints, bank accounts, and private messages — on devices they leave in taxis and lend to friends. Make that trust unbreakable.
Don’t leave your app exposed.
💌 Join 3000+ developers already subscribed on Medium and get my next checklist before it goes public.
▶️ Watch secure coding tutorials on my YouTube channel.
📖 Revisit my article on Solving Mobile Security through AI Prompts for a full-stack view.
💡 Creators: become an early seller on PromptBazaar and get featured to a global audience.
#MobileFirstSecurity #SecureByDesign #DefenseInDepth #Expo #Flutter #AppSec #ShipSecure
