Back to Posts
Mobile App Security Best Practices
By Lumina Software•
securityreact-nativemobile-developmentbest-practices
Mobile App Security Best Practices
Mobile apps handle sensitive data, process payments, and access personal information. Security isn't optional—it's fundamental. Here's how to secure React Native apps against common threats.
Threat Model
Common Threats
- Data interception: Man-in-the-middle attacks
- Insecure storage: Sensitive data in plaintext
- Code tampering: Reverse engineering and modification
- Insecure communication: Unencrypted network traffic
- Authentication flaws: Weak or missing authentication
- Authorization issues: Insufficient access controls
Secure Data Storage
1. Use Secure Storage
import * as SecureStore from 'expo-secure-store';
class SecureStorage {
async save(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value, {
requireAuthentication: true, // Require biometrics
authenticationPrompt: 'Authenticate to access secure data',
});
}
async load(key: string): Promise<string | null> {
try {
return await SecureStore.getItemAsync(key, {
requireAuthentication: true,
});
} catch (error) {
console.error('Failed to load secure data:', error);
return null;
}
}
async delete(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
}
}
2. Encrypt Sensitive Data
import CryptoJS from 'crypto-js';
class EncryptedStorage {
private encryptionKey: string;
constructor() {
// Get key from secure storage or keychain
this.encryptionKey = this.getEncryptionKey();
}
encrypt(data: string): string {
return CryptoJS.AES.encrypt(data, this.encryptionKey).toString();
}
decrypt(encryptedData: string): string {
const bytes = CryptoJS.AES.decrypt(encryptedData, this.encryptionKey);
return bytes.toString(CryptoJS.enc.Utf8);
}
async save(key: string, data: string): Promise<void> {
const encrypted = this.encrypt(data);
await SecureStore.setItemAsync(key, encrypted);
}
async load(key: string): Promise<string | null> {
const encrypted = await SecureStore.getItemAsync(key);
if (!encrypted) return null;
return this.decrypt(encrypted);
}
}
3. Never Store Sensitive Data in AsyncStorage
// ❌ Bad: Plaintext in AsyncStorage
await AsyncStorage.setItem('token', authToken);
await AsyncStorage.setItem('password', userPassword);
// ✅ Good: Use SecureStore
await SecureStore.setItemAsync('token', authToken);
// Never store passwords - use tokens instead
Secure Network Communication
1. Always Use HTTPS
// ✅ Good: HTTPS only
const API_URL = 'https://api.example.com';
// ❌ Bad: HTTP
const API_URL = 'http://api.example.com'; // Insecure!
2. Certificate Pinning
import { fetch } from 'react-native';
class SecureApiClient {
async request(url: string, options: RequestInit): Promise<Response> {
// Certificate pinning configuration
const pinnedCertificates = [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
// Your server's certificate fingerprint
];
// Use library like react-native-cert-pinner
return fetch(url, {
...options,
// Certificate pinning handled by library
});
}
}
3. Request Signing
import CryptoJS from 'crypto-js';
class SignedApiClient {
private apiKey: string;
private secretKey: string;
async request(
method: string,
endpoint: string,
body?: any
): Promise<Response> {
const timestamp = Date.now().toString();
const nonce = this.generateNonce();
const signature = this.signRequest(method, endpoint, timestamp, nonce, body);
return fetch(`${this.baseUrl}${endpoint}`, {
method,
headers: {
'X-API-Key': this.apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
}
private signRequest(
method: string,
endpoint: string,
timestamp: string,
nonce: string,
body?: any
): string {
const message = `${method}${endpoint}${timestamp}${nonce}${body ? JSON.stringify(body) : ''}`;
return CryptoJS.HmacSHA256(message, this.secretKey).toString();
}
}
Authentication Security
1. Use Secure Token Storage
class AuthManager {
async saveTokens(accessToken: string, refreshToken: string): Promise<void> {
// Store in secure storage
await SecureStore.setItemAsync('access_token', accessToken);
await SecureStore.setItemAsync('refresh_token', refreshToken);
}
async getAccessToken(): Promise<string | null> {
return await SecureStore.getItemAsync('access_token');
}
async refreshAccessToken(): Promise<string | null> {
const refreshToken = await SecureStore.getItemAsync('refresh_token');
if (!refreshToken) return null;
const response = await fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
const { access_token } = await response.json();
await SecureStore.setItemAsync('access_token', access_token);
return access_token;
}
async logout(): Promise<void> {
await SecureStore.deleteItemAsync('access_token');
await SecureStore.deleteItemAsync('refresh_token');
}
}
2. Biometric Authentication
import * as LocalAuthentication from 'expo-local-authentication';
class BiometricAuth {
async isAvailable(): Promise<boolean> {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
return compatible && enrolled;
}
async authenticate(): Promise<boolean> {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to access the app',
fallbackLabel: 'Use passcode',
disableDeviceFallback: false,
});
return result.success;
}
}
3. Session Management
class SessionManager {
private sessionTimeout = 15 * 60 * 1000; // 15 minutes
private lastActivity: number = Date.now();
startSession(): void {
this.lastActivity = Date.now();
this.startActivityMonitor();
}
private startActivityMonitor(): void {
setInterval(() => {
if (Date.now() - this.lastActivity > this.sessionTimeout) {
this.endSession();
}
}, 60000); // Check every minute
}
updateActivity(): void {
this.lastActivity = Date.now();
}
async endSession(): Promise<void> {
await this.authManager.logout();
// Navigate to login screen
}
}
Input Validation
1. Validate All Inputs
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email(),
password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
age: z.number().int().min(18).max(120),
});
function validateUserInput(input: unknown): User {
return UserSchema.parse(input); // Throws if invalid
}
2. Sanitize Outputs
import DOMPurify from 'isomorphic-dompurify';
function sanitizeHtml(html: string): string {
return DOMPurify.sanitize(html);
}
function sanitizeUserInput(input: string): string {
// Remove potentially dangerous characters
return input
.replace(/[<>]/g, '') // Remove HTML tags
.replace(/javascript:/gi, '') // Remove javascript: protocol
.trim();
}
Code Protection
1. Obfuscation
// Use tools like:
// - react-native-obfuscator
// - jscrambler
// - proguard (Android)
// Obfuscate sensitive code
const API_KEY = process.env.API_KEY; // Don't hardcode!
2. Root/Jailbreak Detection
import JailMonkey from 'jail-monkey';
class SecurityCheck {
checkDeviceSecurity(): SecurityStatus {
return {
isJailBroken: JailMonkey.isJailBroken(),
isOnExternalStorage: JailMonkey.isOnExternalStorage(),
canMockLocation: JailMonkey.canMockLocation(),
};
}
shouldBlockAccess(): boolean {
const status = this.checkDeviceSecurity();
return status.isJailBroken || status.canMockLocation;
}
}
3. Debug Detection
class DebugDetection {
isDebuggingEnabled(): boolean {
// Check for debugger
if (__DEV__) {
return true; // Development mode
}
// Additional checks for production
return false;
}
handleDebuggingDetected(): void {
// Log security event
// Optionally block access or show warning
console.warn('Debugging detected - security risk');
}
}
Secure Configuration
1. Environment Variables
// ✅ Good: Use environment variables
const API_URL = process.env.EXPO_PUBLIC_API_URL;
const API_KEY = process.env.EXPO_PUBLIC_API_KEY;
// ❌ Bad: Hardcoded secrets
const API_KEY = 'sk_live_1234567890abcdef'; // Never do this!
2. Secure Config Files
// config.ts
export const config = {
apiUrl: process.env.EXPO_PUBLIC_API_URL || 'https://api.example.com',
// Never include secrets in config files
// Use secure storage or environment variables
};
Best Practices Checklist
- Secure storage: Use SecureStore for sensitive data
- HTTPS only: Never use HTTP
- Certificate pinning: Prevent MITM attacks
- Input validation: Validate all user inputs
- Output sanitization: Sanitize data before display
- Token management: Secure token storage and refresh
- Session timeout: Auto-logout after inactivity
- Biometric auth: Use when available
- Root detection: Block on jailbroken devices
- No hardcoded secrets: Use environment variables
- Error handling: Don't leak sensitive info in errors
- Logging: Don't log sensitive data
Security Testing
1. Static Analysis
# Use tools like:
# - ESLint security plugins
# - SonarQube
# - Snyk
npm run lint:security
2. Dependency Scanning
# Check for vulnerable dependencies
npm audit
npx snyk test
3. Penetration Testing
Test for:
- SQL injection
- XSS vulnerabilities
- Insecure data storage
- Weak authentication
- Authorization bypass
Conclusion
Mobile app security requires:
- Secure storage: Encrypt sensitive data
- Secure communication: HTTPS + certificate pinning
- Strong authentication: Tokens + biometrics
- Input validation: Validate and sanitize
- Code protection: Obfuscation + root detection
- Secure configuration: No hardcoded secrets
Security is not a feature—it's a requirement. Build security in from the start, and your users' data will be protected.
