Mobile App Security: Comprehensive Best Practices for 2026
Mobile app security differs fundamentally from web security because the client device is untrusted territory. Users can jailbreak devices, intercept traffic, decompile binaries, and modify runtime behavior. This guide covers essential security measures that protect your users and business from real attacks, and just as importantly, it explains the threat model behind each control so you apply them where they actually matter.
The core principle is straightforward: never trust the client. Anything shipped in your binary — API keys, business logic, validation rules — can be extracted by a motivated attacker with free tools like Frida, Objection, and apktool. Therefore, the device controls described below are about raising the cost of an attack and protecting data at rest, not about creating an impenetrable client. The real security boundary always lives on your server.
Secure Storage: Protecting Sensitive Data
The first rule of secure storage is to store as little as possible. Tokens, not passwords; short-lived credentials, not permanent ones. When you must persist a secret, use the platform’s hardware-backed keystore so the key material is bound to the Secure Enclave (iOS) or the Trusted Execution Environment / StrongBox (Android) and never leaves it.
// iOS: Keychain Services (hardware-backed)
class SecureStorage {
static func save(key: String, data: Data) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAttrAccessControl as String: SecAccessControlCreateWithFlags(
nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet, nil)!
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }
}
}
// Android: EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setUserAuthenticationRequired(true)
.build()
val prefs = EncryptedSharedPreferences.create(
context, "secure_prefs", masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
Two attribute choices carry most of the weight. The ThisDeviceOnly accessibility class prevents the secret from syncing to iCloud Keychain or surviving in an encrypted backup, which closes a common exfiltration path. Meanwhile, .biometryCurrentSet ties access to the currently enrolled biometrics, so adding a new fingerprint or face invalidates the key — a strong defense against an attacker who coerces enrollment. Notably, EncryptedSharedPreferences was deprecated in newer Jetpack Security releases, so for new Android projects evaluate the DataStore-based successors while keeping the same hardware-backed key scheme.
Certificate Pinning Against MITM Attacks
Certificate pinning prevents MITM attacks by verifying the server certificate matches a known good certificate. Without it, an attacker who installs a rogue CA on the device — common on corporate or jailbroken phones — can transparently decrypt your TLS traffic. Pinning rejects any chain that does not match your expected public key, even if it is otherwise validly signed.
// Android: OkHttp certificate pinning
val client = OkHttpClient.Builder()
.certificatePinner(CertificatePinner.Builder()
.add("api.example.com",
"sha256/AAAA...=", // Primary
"sha256/BBBB...=") // Backup
.build())
.build()
// Also use network_security_config.xml for defense in depth
// <network-security-config>
// <domain-config cleartextTrafficPermitted="false">
// <domain includeSubdomains="true">api.example.com</domain>
// <pin-set expiration="2027-01-01">
// <pin digest="SHA-256">AAAA...=</pin>
// </pin-set>
// </domain-config>
// </network-security-config>
Always pin to the public key (SPKI hash) rather than the leaf certificate, and always ship a backup pin. The reason is operational: certificates rotate, and if you pin a single leaf that expires while users run an old app version, every one of them is locked out with no remote fix. For this reason, many teams pin to an intermediate CA they control or include the next rotation’s key as the backup pin. Furthermore, set a pin expiration so the app fails open to normal TLS validation rather than bricking itself if pins drift.
Code Obfuscation and Tamper Detection
Obfuscation does not make code secure; it makes reverse engineering slower and noisier. Combined with runtime integrity checks, it raises the bar enough to deter casual attackers and automated scanners. The checks below detect the environments an attacker typically uses — rooted devices, debuggers, emulators, and re-signed builds.
// Runtime integrity checks
object IntegrityChecker {
fun isDeviceCompromised(context: Context): Boolean {
return isRooted() || isDebuggerAttached() ||
isEmulator() || !isSignatureValid(context)
}
private fun isRooted(): Boolean {
val paths = listOf("/system/app/Superuser.apk",
"/system/xbin/su", "/system/bin/su")
return paths.any { File(it).exists() }
}
private fun isDebuggerAttached() = Debug.isDebuggerConnected()
private fun isEmulator(): Boolean =
Build.FINGERPRINT.contains("generic") ||
Build.MODEL.contains("Emulator")
private fun isSignatureValid(context: Context): Boolean {
val expected = "expected_sha256_hash"
val info = context.packageManager.getPackageInfo(
context.packageName, PackageManager.GET_SIGNATURES)
val digest = MessageDigest.getInstance("SHA-256")
.digest(info.signatures[0].toByteArray())
return digest.toHexString() == expected
}
}
Be honest about what these checks buy you. A determined attacker running Frida can hook isDeviceCompromised and force it to return false, so client-side detection is evidence, not enforcement. Consequently, the right pattern is to report the integrity signal to your server and let the server decide whether to allow a sensitive operation. For example, a banking app might permit browsing on a rooted device but block a wire transfer, degrading gracefully instead of crashing and frustrating power users.
API Security and Device Attestation
Because client checks can be bypassed, the strongest signal of device integrity comes from the platform itself. Play Integrity (Android) and App Attest / DeviceCheck (iOS) produce a cryptographically signed token, generated with Google or Apple keys, that your backend verifies server-side. An attacker cannot forge it because the signing keys never touch the device.
// Android: Play Integrity API
class ApiClient(private val context: Context) {
suspend fun makeRequest(url: String): Response {
val token = IntegrityManager
.createStandardIntegrityManager(context)
.requestIntegrityToken(
StandardIntegrityTokenRequest.builder()
.setRequestHash(hashPayload(url))
.build()
).await()
return httpClient.newCall(Request.Builder()
.url(url)
.addHeader("Authorization", "Bearer ${getAuthToken()}")
.addHeader("X-Integrity-Token", token.token())
.build()
).execute()
}
}
The setRequestHash binding is critical and often skipped. It ties the attestation token to the specific request payload, which prevents a replay attack where a token harvested from one legitimate request is reused to authorize a different, malicious one. On the server, you must verify the token’s nonce, the package name, the certificate digest, and the freshness window. Moreover, treat verdict fields probabilistically rather than as a hard gate, because legitimate users on older or rooted-but-benign devices can produce ambiguous results, and false positives drive away paying customers.
OWASP MASVS Checklist
Rather than improvising, anchor your program to the OWASP Mobile Application Security Verification Standard. MASVS defines tiered requirements so you can match rigor to risk: a note-taking app does not need the same controls as a payments wallet. The tiers below summarize where each control belongs.
Level 1 (Every App):
- No sensitive data in logs/backups
- TLS for all network traffic
- Secure authentication and sessions
- Secure data storage (Keychain/Keystore)
Level 2 (Banking, Health):
- Certificate pinning
- Biometric authentication
- Device attestation (Play Integrity/App Attest)
- Root/jailbreak detection
- Token rotation
Level R (DRM, Payments):
- Code obfuscation (ProGuard/R8)
- Debugger detection
- Integrity verification
- Emulator detection
- Anti-instrumentation (Frida detection)
The tiering matters because every control has a cost in engineering time, support burden, and user friction. For instance, aggressive root detection on a Level 1 app mostly punishes developers and enthusiasts while doing little to protect low-value data. In contrast, skipping attestation on a financial app is negligent. The discipline of MASVS is deciding deliberately, and you can connect these decisions to your wider posture using the principles in API Security Best Practices.
When NOT to Over-Engineer Security
Security controls are a trade-off against usability, support cost, and shipping velocity, and over-engineering is a real failure mode. Hard root detection that blocks the app entirely will lock out developers, accessibility tooling users, and the growing population of privacy-conscious users on de-Googled ROMs, while a skilled attacker bypasses it in minutes. Therefore, prefer graceful degradation: limit risky actions on suspect devices instead of denying access outright.
Similarly, never put your only line of defense in the client. If a feature’s security depends on the app refusing to do something, an attacker who controls the device wins by definition. Spend your effort where it compounds — server-side authorization, short-lived rotating tokens, anomaly detection on the backend, and minimizing the secrets you ship at all. The client controls then become a useful additional layer rather than a false sense of safety.
For further reading, refer to the Android developer docs and the Apple developer docs for comprehensive reference material.
Key Takeaways
- Treat the device as untrusted; the real security boundary is your server
- Use hardware-backed storage and store the minimum secret material possible
- Pin to public keys with a backup pin and an expiration to avoid lockouts
- Report integrity and attestation signals to the backend, not as client gates
- Tier controls with OWASP MASVS and prefer graceful degradation over hard blocks
In conclusion, Mobile App Security Practices succeed through layered defense and honest threat modeling rather than any single silver bullet. Implement hardware-backed storage, certificate pinning, runtime checks, and platform attestation, but always anchor enforcement on the server and tune each control to the value of what it protects. Start with the MASVS fundamentals, iterate as the threat landscape shifts, and continuously measure both your security posture and its impact on real users.