Mobile App Security: iOS & Android Security Checklist

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

OWASP MASVS v2.0 Security Checklist

#CategoryKey ControlsiOS ImplementationAndroid Implementation
1MASVS-STORAGESecure sensitive data at rest; no secrets in logs/backups/clipboardKeychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly; Data Protection APIAndroid Keystore; EncryptedFile; exclude from backups via android:allowBackup="false"
2MASVS-CRYPTOUse approved algorithms; secure key management; no hardcoded keysCommonCrypto/CryptoKit with AES-GCM; Secure Enclave for key storageAndroid Keystore with setUserAuthenticationRequired(true); AES/GCM/NoPadding
3MASVS-AUTHStrong authentication; session management; biometric with fallbackLAContext with evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics); server-side token validationBiometricPrompt with setNegativeButtonText for fallback; server-side session binding
4MASVS-NETWORKTLS 1.2+ enforced; certificate pinning; no cleartext trafficApp Transport Security (ATS) enforced by default; URLSession delegate for pinningNetwork Security Config XML; cleartextTrafficPermitted="false"; pin-set with backup pins
5MASVS-PLATFORMSecure IPC; WebView hardening; input validation from deep linksUniversal Links validation; WKWebView (never UIWebView); URL scheme input sanitizationIntent filter validation; WebView.setJavaScriptEnabled(false) by default; deep link validation
6MASVS-CODEDependency management; no debug code in production; input validationStrip debug symbols; disable logging in release; dependency auditProGuard/R8 obfuscation; BuildConfig.DEBUG checks; dependency scanning
7MASVS-RESILIENCEAnti-tampering; anti-debugging; integrity verificationApp Attest API (DeviceCheck framework); ptrace detection; code signing validationPlay Integrity API (replaces SafetyNet); signature verification; Debug.isDebuggerConnected() checks
8MASVS-PRIVACYData minimization; consent management; anonymizationATT (App Tracking Transparency) framework; Privacy Manifest; on-device processingPrivacy Sandbox APIs; runtime permissions; PURPOSE_ declarations in manifest

Decision Tree

START: What is your app's risk profile?
├── Handles financial/health/government data?
│   ├── YES → Implement ALL 8 MASVS categories + certificate pinning + RASP + App Attest/Play Integrity
│   └── NO ↓
├── Handles user credentials or PII?
│   ├── YES → Implement MASVS-STORAGE + CRYPTO + AUTH + NETWORK + PLATFORM + CODE
│   └── NO ↓
├── Public data only, no user accounts?
│   ├── YES → Implement MASVS-NETWORK (TLS) + PLATFORM (WebView) + CODE (dependencies)
│   └── NO ↓
├── Which platform?
│   ├── iOS → Use Keychain + ATS + App Attest + CryptoKit
│   ├── Android → Use Keystore + Network Security Config + Play Integrity + Jetpack Security
│   └── Cross-platform → Apply native security on each side; DO NOT share crypto code
└── DEFAULT → Implement at minimum: MASVS-STORAGE + MASVS-NETWORK + MASVS-CODE

Step-by-Step Guide

1. Secure local data storage

Store all sensitive data (tokens, credentials, PII) in platform-provided secure storage. Never use plaintext files, SharedPreferences, or UserDefaults for secrets. [src1]

// iOS: Keychain with device-only access
import Security

func saveToKeychain(key: String, data: Data) -> OSStatus {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]
    SecItemDelete(query as CFDictionary)
    return SecItemAdd(query as CFDictionary, nil)
}
// Android: EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val securePrefs = EncryptedSharedPreferences.create(
    context, "secure_prefs", masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()

Verify: Run MobSF static analysis -- no findings under "Insecure Data Storage" category.

2. Implement certificate pinning

Pin your server's public key (SPKI hash) to prevent MITM attacks even with compromised CAs. Always include a backup pin. [src4]

<!-- Android: res/xml/network_security_config.xml -->
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <domain-config>
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2027-01-01">
            <pin digest="SHA-256">AAAA...primary-pin...AAAA=</pin>
            <pin digest="SHA-256">BBBB...backup-pin....BBBB=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

Verify: Use a proxy tool (Charles/mitmproxy) -- requests should fail when proxy's cert is injected.

3. Implement biometric authentication with server binding

Biometric auth must be tied to a server-side session, not just a local gate. A jailbroken device can bypass local-only biometric checks. [src3]

// iOS: LAContext biometric + Keychain-bound token
import LocalAuthentication

let context = LAContext()
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
    context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Authenticate to access your account"
    ) { success, error in
        if success {
            let token = loadFromKeychain(key: "auth_token")
            // Send token to server for validation
        }
    }
}

Verify: Attempt to access protected resources without biometric -- should be denied both locally and server-side.

4. Harden WebViews

WebViews are a major attack surface. Disable JavaScript unless required, validate all URLs, and never expose native bridges to untrusted content. [src2]

// Android: Secure WebView configuration
val webView = WebView(context)
webView.settings.apply {
    javaScriptEnabled = false      // Enable ONLY if required
    allowFileAccess = false        // Prevent file:// access
    allowContentAccess = false     // Prevent content:// access
    domStorageEnabled = false
    setGeolocationEnabled(false)
}

Verify: Attempt to load javascript:alert(1) or file:///etc/passwd -- both should be blocked.

5. Enable runtime integrity checks

Detect tampering, debugging, and compromised environments. Use platform attestation APIs for server-side verification. [src7]

// Android: Play Integrity API
val integrityManager = IntegrityManagerFactory.create(context)
val request = IntegrityTokenRequest.builder()
    .setNonce(generateNonce()).build()
integrityManager.requestIntegrityToken(request)
    .addOnSuccessListener { response ->
        val token = response.integrityToken()
        // Send to YOUR server for verification
    }

Verify: Run on rooted/jailbroken device or emulator -- integrity check should fail.

Code Examples

Swift: Secure Keychain Storage with Biometric Access Control

import Security
import LocalAuthentication

// Input:  Secret string to store securely
// Output: OSStatus indicating success/failure

func storeSecretWithBiometric(key: String, secret: String) -> OSStatus {
    guard let data = secret.data(using: .utf8) else { return errSecParam }
    let access = SecAccessControlCreateWithFlags(
        nil,
        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        .biometryCurrentSet,
        nil
    )!
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data,
        kSecAttrAccessControl as String: access
    ]
    SecItemDelete(query as CFDictionary)
    return SecItemAdd(query as CFDictionary, nil)
}

Kotlin: Android Keystore AES-GCM Encryption

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import javax.crypto.Cipher
import javax.crypto.KeyGenerator

// Input:  Plaintext byte array
// Output: Encrypted data + IV pair

object SecureStorage {
    private const val KEY_ALIAS = "app_secret_key"

    fun generateKey() {
        val gen = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
        gen.init(KeyGenParameterSpec.Builder(KEY_ALIAS,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setUserAuthenticationRequired(true)
            .setUserAuthenticationParameters(300,
                KeyProperties.AUTH_BIOMETRIC_STRONG)
            .build())
        gen.generateKey()
    }
}

Anti-Patterns

Wrong: Storing tokens in plaintext SharedPreferences

// BAD -- plaintext storage readable on rooted devices
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", "eyJhbGciOiJIUzI1NiIs...").apply()
// Any app with root access can read /data/data/com.app/shared_prefs/

Correct: Use EncryptedSharedPreferences or Keystore

// GOOD -- encrypted at rest with Keystore-backed key
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val securePrefs = EncryptedSharedPreferences.create(
    context, "secure_prefs", masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()

Wrong: Disabling SSL verification for testing (left in production)

// BAD -- disables ALL certificate validation
class InsecureDelegate: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        completionHandler(.useCredential,
            URLCredential(trust: challenge.protectionSpace.serverTrust!))
    }
}

Correct: Implement proper certificate pinning

// GOOD -- validates server certificate against pinned key hash
class PinnedDelegate: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard let trust = challenge.protectionSpace.serverTrust,
              SecTrustEvaluateWithError(trust, nil),
              validatePin(trust: trust) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        completionHandler(.useCredential, URLCredential(trust: trust))
    }
}

Wrong: Logging sensitive data

// BAD -- tokens and PII visible in logcat
Log.d("Auth", "User token: $authToken")
Log.d("Payment", "Card number: ${card.number}")

Correct: Conditional logging with sanitization

// GOOD -- no sensitive data; disabled in release
if (BuildConfig.DEBUG) {
    Log.d("Auth", "Token received: [REDACTED]")
}
// ProGuard rule: -assumenosideeffects class android.util.Log { *; }

Wrong: Hardcoding API keys in source code

// BAD -- extractable via strings command on binary
let apiKey = "sk_live_abc123xyz789"

Correct: Retrieve keys from secure storage at runtime

// GOOD -- key stored in Keychain, never in source code
guard let apiKeyData = loadFromKeychain(key: "api_key"),
      let apiKey = String(data: apiKeyData, encoding: .utf8) else {
    fatalError("API key not provisioned")
}

Common Pitfalls

Diagnostic Commands

# Static analysis with MobSF (Docker)
docker run -it --rm -p 8000:8000 opensecurity/mobile-security-framework-mobsf:latest

# Extract SPKI hash for certificate pinning
openssl s_client -connect api.example.com:443 2>/dev/null | \
  openssl x509 -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  openssl enc -base64

# Check APK for plaintext secrets
apktool d app.apk -o decoded && grep -rn "api_key\|secret\|password\|token" decoded/

# Verify iOS binary for hardcoded strings
strings MyApp.app/MyApp | grep -i "key\|secret\|password\|token"

# Check Android obfuscation
jadx -d output app.apk && grep -rn "class.*Activity" output/

# Verify network security config is applied
adb shell dumpsys package com.example.app | grep networkSecurityConfig

Version History & Compatibility

Standard/ToolVersionStatusKey Changes
OWASP MASVSv2.1.0Current (2024)Added MASVS-PRIVACY; removed L1/L2/R levels; MAS Testing Profiles
OWASP MASVSv1.5.0DeprecatedThree verification levels (L1, L2, R); MSTG companion
Android Jetpack Security1.1.0-alpha06CurrentEncryptedSharedPreferences + EncryptedFile
Play Integrity API1.3Current (2025)Replaces SafetyNet Attestation (deprecated 2024)
Apple App AttestiOS 14+CurrentDevice attestation; replaces DeviceCheck for assertions
Apple ATSiOS 9+EnforcedTLS 1.2+ required by default
CryptoKitiOS 13+CurrentModern Swift crypto API (AES-GCM, SHA-256, P-256)

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
App handles user credentials, PII, or payment dataBuilding a static content app with no user dataBasic HTTPS enforcement only
App communicates with backend APIsApp is entirely offline with no network callsFocus on MASVS-STORAGE only
App processes financial or health dataBuilding an internal prototype/demoSimplified security for development
App is distributed via public app storesApp is only used on managed/MDM devicesEnterprise MDM security policies
Regulatory compliance required (GDPR, HIPAA, PCI DSS)No regulatory requirements applyRisk-based subset of MASVS

Important Caveats

Related Units