Mobile App Security: OWASP Mobile Top 10
Mobile apps face unique security challenges — the device is in the user’s hands, data can be extracted, reverse engineering is straightforward, and the network is untrusted. The OWASP Mobile Top 10 provides a ranked list of the most critical security risks.
OWASP Mobile Top 10 (2024)
| Rank | Risk | Description |
|---|---|---|
| M1 | Improper Credential Usage | Hardcoded keys, tokens, passwords |
| M2 | Inadequate Supply Chain Security | Vulnerable libraries, SDKs |
| M3 | Insecure Authentication/Authorization | Weak auth, broken session management |
| M4 | Insufficient Input/Output Validation | Injection, path traversal |
| M5 | Insecure Communication | Cleartext traffic, weak TLS |
| M6 | Inadequate Privacy Controls | Over-collection, mishandled PII |
| M7 | Insufficient Binary Protections | Code tampering, reverse engineering |
| M8 | Security Misconfiguration | Debug enabled, excessive permissions |
| M9 | Insecure Data Storage | Plaintext data on device or cloud |
| M10 | Insufficient Cryptography | Weak algorithms, bad key management |
M1: Improper Credential Usage
Never hardcode API keys, tokens, or passwords. They’re trivially extracted from decompiled apps.
// BAD — hardcoded API key
let apiKey = "sk_live_abc123xyz"
// GOOD — fetch from secure backend at runtime
class CredentialService {
static func getAPIKey() async throws -> String {
// Fetch from your backend, not stored in the app
let response = await URLSession.shared.get("/api/v1/credentials")
return response.apiKey
}
---// BAD — hardcoded in Android
object Config { const val API_KEY = "sk_live_abc123xyz" }
// GOOD — use Android Keystore
class SecureKeyStore {
fun storeKey(alias: String, key: ByteArray) {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
keyStore.setEntry(alias, KeyStore.SecretKeyEntry(secretKey), protParams)
}
---M2: Inadequate Supply Chain Security
Vulnerable third-party libraries are a leading cause of mobile breaches.
// Regularly audit dependencies
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0") // Stay updated
implementation("com.google.code.gson:gson:2.10.1")
---
// Use dependency scanning
// ./gradlew dependencyCheckAnalyze
// npx audit-ci --moderate- Pin dependency versions (don’t use
+) - Monitor CVEs for your libraries
- Use a Software Bill of Materials (SBOM)
- Remove unused dependencies
- Enable automated dependency updates with Dependabot or Renovate
M3: Insecure Authentication/Authorization
// BAD — local authentication check (easily bypassed)
if enteredPin == "1234" { grantAccess() }
// GOOD — server-side auth with biometrics
class AuthManager {
func authenticate(user: String, password: String) async throws -> Token {
let response = try await api.login(user: user, password: password)
storeTokenSecurely(response.token)
return response.token
}
func biometricAuth() async -> Bool {
let context = LAContext()
return await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Log in")
}
---Implement:
- Token-based auth (JWT) with short expiration
- Biometric + PIN fallback
- Server-side session validation
- Rate limiting on login endpoints
- Multi-factor authentication for sensitive operations
M4: Insufficient Input/Output Validation
// BAD — directly evaluating JavaScript from a URL parameter
webView.evaluateJavaScript(userInput) // XSS vulnerability
// GOOD — validate and sanitize
func sanitizeInput(_ input: String) -> String {
return input
.replacingOccurrences(of: "<", with: "<")
.replacingOccurrences(of: ">", with: ">")
.replacingOccurrences(of: "\"", with: """)
---
// Use parameterized queries for local databases
let stmt = try db.prepare("SELECT * FROM users WHERE id = ?")
try stmt.bind([userId])Common vectors:
- SQL injection in local databases
- XSS in WebViews
- Path traversal in file operations
- Command injection in shell calls
M5: Insecure Communication
<!-- Android: enforce TLS -->
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>// Certificate pinning (OkHttp)
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()On iOS, configure App Transport Security (ATS) in Info.plist to enforce TLS 1.2+ and reject insecure connections. Use NSAppTransportSecurity with NSAllowsArbitraryLoads set to false in production.
M6: Inadequate Privacy Controls
- Collect only data necessary for the app’s core function
- Provide clear privacy notices and consent flows
- Allow users to delete their data
- Comply with GDPR, CCPA, and app store requirements
- Implement data retention policies that automatically purge old data
- Avoid sharing data with third-party SDKs unless explicitly required
M7: Insufficient Binary Protections
# Android: enable ProGuard/R8
# gradle.properties
android.enableR8=true
# proguard-rules.pro
-keep class com.example.app.model.** { *; }
-dontwarn okhttp3.**// iOS: enable compiler optimizations
// Build Settings → Optimization Level → Fastest, Smallest [-Os]
// Strip Debug Symbols → YES
// Enable Bitcode → YESConsider:
- Code obfuscation
- Integrity checks (checksum the binary at runtime)
- Jailbreak/root detection
- Debugger detection
- Anti-tampering mechanisms
M8: Security Misconfiguration
<!-- AndroidManifest.xml — secure defaults -->
<application
android:allowBackup="false" <!-- Prevent ADB backup extract -->
android:debuggable="false" <!-- Never ship debuggable -->
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false">
<activity
android:exported="false" <!-- Explicitly set for each activity -->
android:screenOrientation="portrait" />
</application>On iOS, disable NSHTTPCookieAcceptPolicy, limit NSAppTransportSecurity exceptions, and never ship with a debugger attached. Review all Info.plist entries before release.
M9: Insecure Data Storage
// BAD — storing in UserDefaults or SharedPreferences
UserDefaults.standard.set("secret123", forKey: "token")
// GOOD — iOS Keychain
class KeychainService {
static func store(key: String, value: Data) {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecValueData: value,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
] as CFDictionary
SecItemAdd(query, nil)
}
---// GOOD — EncryptedSharedPreferences
val prefs = EncryptedSharedPreferences.create(
"secure_prefs", masterKey, context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
prefs.edit().putString("token", jwt).apply()Also:
- Never store sensitive data in logs
- Clear sensitive data from memory after use
- Don’t cache API responses with PII on disk
- Use
NSFileProtectionCompletefor iOS file storage
M10: Insufficient Cryptography
// BAD — weak algorithm
let hashed = MD5(string) // DO NOT USE
// GOOD — modern cryptography
import CryptoKit
func encrypt(_ data: Data, using key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined!
---// BAD — ECB mode
Cipher.getInstance("AES/ECB/PKCS5Padding") // INSECURE
// GOOD — modern authenticated encryption
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val iv = ByteArray(12).apply { SecureRandom().nextBytes(this) }
cipher.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(128, iv))Always use authenticated encryption (GCM or ChaCha20-Poly1305). Use a key derivation function (Argon2 or PBKDF2) for password-based keys. Never implement custom cryptographic algorithms.
Secure Session Management
Implement token-based authentication with short-lived access tokens (15-60 minutes) and long-lived refresh tokens (7-30 days). Store refresh tokens in platform secure storage (Keychain/Keystore). Validate all tokens server-side on every request. Implement token revocation for logout and suspicious activity. Use HTTP-only cookies for web views and bearer tokens for native API calls. Monitor for token reuse anomalies that might indicate token theft.
Runtime Protection
Implement runtime application self-protection (RASP) checks: detect debugging tools (Frida, Xposed), prevent screenshots on sensitive screens, verify the app’s code signature at runtime, and check for emulator or simulator environments. These measures raise the bar for attackers without providing absolute security — combine them with server-side validation and monitoring for defense in depth.
Summary
Mobile app security requires a layered approach spanning secure data storage, network encryption, authentication, and runtime protection. Follow the OWASP Mobile Top 10 as a baseline, test with both static and dynamic analysis tools, and stay current with emerging threats. Security is an ongoing process, not a one-time implementation.
Mobile security requires defense in depth. Secure data storage, encrypted communication, proper authentication, dependency auditing, and binary protections form the foundation. The OWASP Mobile Top 10 provides a systematic checklist — address each risk to build a robust security posture for your mobile app.
Related: Mobile App Testing Guide | iOS Development Guide
FAQ
How often should I perform security audits on my mobile app?
Perform a comprehensive security audit before every major release and at least quarterly. Use automated scanning tools (MobSF, QARK) for continuous monitoring. Penetration testing by a third party should be done annually or before launching in regulated industries like finance and healthcare.
What is the most common mobile security vulnerability?
Insecure data storage is the most common vulnerability found in mobile apps. Developers frequently store API tokens, user credentials, or PII in plaintext in SharedPreferences, UserDefaults, or local files. Using the platform’s secure storage (Keychain/Keystore) eliminates this risk.
Should I implement root/jailbreak detection?
Yes, for apps handling sensitive data or payments. Root/jailbreak detection adds a layer of defense, but never rely on it as the sole security measure — determined attackers can bypass it. Combine it with server-side validation, certificate pinning, and encryption for defense in depth.
How do I handle API keys securely in a mobile app?
Never embed API keys in the app binary. Fetch them from a backend endpoint at app startup, store them in the platform secure storage (Keychain/Keystore), and use short-lived tokens. For third-party SDKs that require keys, use the backend proxy pattern where the server holds the key and relays requests.
What should I include in a mobile app privacy policy?
Clearly state what data you collect, why you collect it, how it is stored, who it is shared with, and how users can request deletion. Include contact information for privacy inquiries. Follow GDPR requirements if you serve EU users, and CCPA requirements for California users.