17Advanced

Mobile Security

Mobile Security — Instruction 17

Coverage

React Native, Expo, Flutter — Mobile-specific security OWASP Mobile Top 10


React Native & Expo Checks

1. No Secrets in JS Bundle

// 🔴 CRITICAL — Mobile JS bundle is extractable by anyone
// Someone can download your APK/IPA and extract the JavaScript

const API_SECRET = 'sk_live_abc123'  // 🔴 visible in bundle
const DB_PASSWORD = 'mypassword'     // 🔴 visible in bundle

// 🔴 Even process.env in React Native bundles at build time!
const KEY = process.env.REACT_APP_SECRET  // 🔴 baked into bundle

// 🟢 Only use BACKEND for sensitive operations
// Frontend: calls your API → API calls external services with secrets
// Never: Frontend → directly calls Stripe/Firebase/etc with secret keys

2. Secure Storage for Sensitive Data

// 🔴 AsyncStorage is plaintext, not encrypted
import AsyncStorage from '@react-native-async-storage/async-storage'
await AsyncStorage.setItem('authToken', token)  // stored unencrypted

// 🟢 Use SecureStore (Expo) or Keychain (React Native)
import * as SecureStore from 'expo-secure-store'
await SecureStore.setItemAsync('authToken', token)  // encrypted, keychain-backed

// 🟢 React Native (without Expo)
import RNSecureStorage from 'rn-secure-storage'
await RNSecureStorage.set('authToken', token, { accessible: ACCESSIBLE.WHEN_UNLOCKED })

3. Certificate Pinning for API Calls

// 🔴 Without pinning: MITM with Burp Suite / Charles Proxy is trivial
// A developer can intercept ALL API calls from the app

// 🟢 React Native SSL pinning
import { fetch } from 'react-native-ssl-pinning'
await fetch(API_URL, {
  method: 'POST',
  sslPinning: {
    certs: ['api-cert']  // certificate hash in assets
  }
})

4. No Sensitive Data in Logs

// 🔴 console.log visible in device logs (accessible without root on iOS)
console.log('Auth token:', token)
console.log('User data:', { email, password })

// 🟢 Remove all console.log in production
// Use babel-plugin-transform-remove-console
// OR conditional logging
if (__DEV__) console.log('Debug info')

// 🟢 Never log: tokens, passwords, PII, payment data

5. Deeplink Validation

// 🔴 Deeplink handled without validation
// myapp://reset-password?token=abc → if token not validated → account takeover

// 🟢 Validate all deeplink parameters
Linking.addEventListener('url', ({ url }) => {
  const { pathname, searchParams } = new URL(url)
  // Validate token server-side before any action
  if (pathname === '/reset-password') {
    const token = searchParams.get('token')
    if (!token || !isValidFormat(token)) return  // ignore malformed
    verifyTokenServer(token)  // validate server-side
  }
})

6. Sensitive Data in Screenshots / App Switcher

// 🔴 App switcher shows last screen (may show sensitive data)
// 🟢 On iOS: blur sensitive screens when app goes to background
import { AppState } from 'react-native'
AppState.addEventListener('change', (nextState) => {
  if (nextState === 'background') {
    setShowBlur(true)  // overlay sensitive content with blur
  }
})

7. Jailbreak/Root Detection (Guided)

// For high-security apps: detect compromised device
import JailMonkey from 'jail-monkey'
if (JailMonkey.isJailBroken()) {
  Alert.alert('Security Warning', 'This device may be compromised')
}
// Note: not foolproof, but adds friction

Expo-Specific Checks

8. app.json / app.config.js

// 🔴 Android: allowBackup allows full data extraction via adb backup
{
  "android": {
    "allowBackup": true  // 🔴
  }
}

// 🟢
{
  "android": {
    "allowBackup": false
  }
}

9. Expo Permissions

// Only request permissions actually needed
{
  "permissions": [
    "CAMERA",        // only if camera is used
    "READ_CONTACTS"  // 🔴 if not needed, remove!
  ]
}

10. OTA Updates Security

// Expo OTA updates: bundle updates pushed without App Store review
// 🔴 If OTA endpoint is compromised → malicious code pushed to all users
// 🟢 Use code signing for OTA updates (Expo EAS Update)
// 🟢 Never trust OTA update content without signature verification

Flutter-Specific Checks

11. Dart Secrets

// 🔴 Same issue: secrets in Dart code are extractable
const apiKey = 'sk_live_abc123';  // visible in compiled binary

// 🟢 Backend proxy for all sensitive API calls
// 🟢 Use flutter_secure_storage for local sensitive data
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
await storage.write(key: 'token', value: authToken);