Securing Android Apps: OWASP Mobile Top 10 in Practice

Protect your Android app against the most common vulnerabilities. Covers secure storage, certificate pinning, code obfuscation, and runtime integrity checks.

Introduction

Android apps face unique security challenges: reverse engineering, insecure data storage, man-in-the-middle attacks, and more. This guide implements practical defenses against OWASP Mobile Top 10 vulnerabilities with production-ready code.

Secure Data Storage

Never store sensitive data in SharedPreferences or unencrypted databases. Use EncryptedSharedPreferences for small values. Use SQLCipher for encrypted Room databases. Leverage Android Keystore for key management.

Network Security

Enforce HTTPS with network_security_config.xml. Implement certificate pinning with OkHttp CertificatePinner. Use HSTS headers. Validate SSL certificates properly - never trust all certificates in production.

Code Obfuscation

Enable R8 with appropriate ProGuard rules. Use resource shrinking. Obfuscate class names but keep API models readable for debugging. Test obfuscated builds before release.

Runtime Integrity

Detect rooted devices with SafetyNet or Play Integrity API. Detect debuggers with Debug.isDebuggerConnected(). Implement tamper detection for production builds. Handle violations gracefully without crashing.

Frequently Asked Questions

How do I securely store API keys?

Don't embed API keys in the app. Use backend proxy for API calls. If unavoidable, store in EncryptedSharedPreferences with keys in Android Keystore. Understand that client-side secrets can always be extracted with enough effort.

Is certificate pinning worth it?

Yes for apps handling sensitive data. Certificate pinning prevents MITM attacks even if a CA is compromised. Implement pinning with backup pins and plan for key rotation.

SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC07:38:09
UP00:01
MOD · ARTICLE · SECURITYS/N · AX-ANDROIPUBLISHED
SecurityFeb 20, 202610 MIN

Securing Android Apps: OWASP Mobile Top 10 in Practice

Protect your Android app against the most common vulnerabilities. Covers secure storage, certificate pinning, code obfuscation, and runtime integrity checks.

By Rocky Elsalaymeh · Founder & Principal Consultant, Strategia-X

Why Mobile Security Requires Different Thinking

Web applications run on servers you control. Mobile applications run on devices you don't. The APK ships to the user's device where it can be decompiled, debugged, modified, and run in hostile environments. Your security model must assume the client is compromised. This doesn't mean security is futile -- it means defense in depth. Each layer of protection raises the cost of an attack. Most attackers are opportunistic: they target the easiest prey. A well-secured app sends them looking elsewhere. The OWASP Mobile Top 10 provides a framework for the most common vulnerabilities. Addressing these systematically covers the attack surface that accounts for the vast majority of real-world breaches in mobile apps.

Secure Data Storage

The number one mobile vulnerability is insecure data storage. Sensitive data stored in SharedPreferences, plain text files, or unencrypted databases is trivially accessible on rooted devices and through backup extraction. The Android Keystore system provides hardware-backed key storage that protects cryptographic keys from extraction. Use it with EncryptedSharedPreferences for key-value data and SQLCipher or encrypted Room databases for structured data.
kotlin
// EncryptedSharedPreferences: drop-in secure replacement
private fun getSecurePrefs(
    context: Context
): SharedPreferences {
    val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    return EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences
            .PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences
            .PrefValueEncryptionScheme.AES256_GCM
    )
}

// Usage: identical to regular SharedPreferences
val prefs = getSecurePrefs(context)
prefs.edit()
    .putString("auth_token", token)
    .putString("refresh_token", refreshToken)
    .apply()

// For tokens in memory: use char arrays, not Strings
// Strings are immutable and linger in the heap
class TokenHolder {
    private var tokenChars: CharArray? = null

    fun setToken(token: String) {
        clearToken()
        tokenChars = token.toCharArray()
    }

    fun getToken(): String? =
        tokenChars?.let { String(it) }

    fun clearToken() {
        tokenChars?.fill('\u0000')
        tokenChars = null
    }
}

Network Security: Certificate Pinning

HTTPS protects data in transit, but it relies on the device's trusted certificate store. On compromised devices or with a corporate proxy, an attacker can install their own CA certificate and intercept all traffic. Certificate pinning ensures your app only trusts specific certificates, defeating man-in-the-middle attacks. Android provides two pinning mechanisms: the network security config (XML-based, no code changes) and OkHttp's CertificatePinner (programmatic, more flexible). Use the network security config for production; use OkHttp pinning when you need to pin against specific public keys.
xml
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">
            api.example.com
        </domain>
        <pin-set expiration="2027-01-01">
            <pin digest="SHA-256">
                base64EncodedSHA256HashOfPublicKey=
            </pin>
            <!-- Backup pin for rotation -->
            <pin digest="SHA-256">
                backupBase64EncodedSHA256Hash=
            </pin>
        </pin-set>
    </domain-config>

    <!-- Debug: allow local proxy for development -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

Input Validation and Injection Prevention

Every piece of data that enters your app from external sources is untrusted: user input, deep links, intent extras, clipboard data, NFC payloads, QR codes, and API responses. Treat all external data as potentially malicious. SQL injection is mitigated by Room's parameterized queries (never build SQL strings with concatenation). Path traversal attacks through content providers require validating file paths. Deep link parameters must be validated before use in navigation or API calls.
kotlin
// VULNERABLE: SQL injection via string concatenation
@Query("SELECT * FROM users WHERE name = '${name}'")
fun findUserUnsafe(name: String): User  // NEVER DO THIS

// SAFE: Room parameterized queries prevent injection
@Query("SELECT * FROM users WHERE name = :name")
fun findUser(name: String): User

// Deep link validation
fun handleDeepLink(uri: Uri) {
    val userId = uri.getQueryParameter("userId")

    // Validate format before using
    if (userId == null ||
        !userId.matches(Regex("^[a-zA-Z0-9]{1,36}$"))
    ) {
        return // Reject malformed input
    }

    // Check for path traversal attempts
    val fileName = uri.lastPathSegment ?: return
    if (fileName.contains("..") ||
        fileName.contains("/")
    ) {
        return // Path traversal attempt
    }

    navigateToProfile(userId)
}

Code Obfuscation with R8

R8 (the successor to ProGuard) shrinks, optimizes, and obfuscates your code. Obfuscation renames classes, methods, and fields to meaningless names, making decompilation output difficult to read. It also removes unused code, reducing APK size. R8 is enabled by default for release builds but requires careful configuration of keep rules to prevent runtime crashes. The most common mistakes are obfuscating serialized classes (breaking JSON parsing) and obfuscating classes accessed via reflection.
kotlin
// build.gradle.kts
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile(
                    "proguard-android-optimize.txt"
                ),
                "proguard-rules.pro"
            )
        }
    }
}

// proguard-rules.pro
# Keep data classes used with serialization
-keep class com.app.model.** { *; }

# Keep Retrofit service interfaces
-keep,allowobfuscation interface com.app.network.api.** {
    @retrofit2.http.* <methods>;
}

# Keep Hilt-generated components
-keep class **_HiltModules* { *; }
-keep @dagger.hilt.android.lifecycle.HiltViewModel
    class * { *; }

# Keep Room-generated code
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *

# Crash reporting: keep line numbers
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile

Runtime Integrity Checks

Runtime checks detect when your app is running in a compromised environment: rooted devices, debuggers attached, repackaged APKs, or emulators. These checks don't prevent attacks -- a determined attacker can patch them out -- but they raise the bar significantly. Use the Play Integrity API (formerly SafetyNet) for server-verified device attestation. For client-side checks, verify the APK signature, check for debugger attachment, and detect common root indicators. Send the integrity token to your server for verification -- never make trust decisions on the client alone.
kotlin
// Play Integrity API: server-verified attestation
class IntegrityChecker @Inject constructor(
    private val context: Context,
    private val api: BackendApi,
) {
    suspend fun verifyDeviceIntegrity(): Boolean {
        val manager = IntegrityManagerFactory
            .create(context)

        val tokenRequest = IntegrityTokenRequest.builder()
            .setNonce(generateNonce())
            .build()

        val tokenResponse = manager
            .requestIntegrityToken(tokenRequest)
            .await()

        // Send token to YOUR server for verification
        // Never verify on the client
        val result = api.verifyIntegrity(
            token = tokenResponse.token()
        )

        return result.deviceRecognized &&
               result.appRecognized &&
               result.accountLicensed
    }

    private fun generateNonce(): String {
        val bytes = ByteArray(32)
        SecureRandom().nextBytes(bytes)
        return Base64.encodeToString(
            bytes, Base64.NO_WRAP
        )
    }
}
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1Assume the client device is compromised -- never trust client-side validation alone.
  2. 2Use EncryptedSharedPreferences and Android Keystore for sensitive data storage.
  3. 3Implement certificate pinning via network_security_config.xml with backup pins.
  4. 4Room parameterized queries prevent SQL injection -- never concatenate user input into queries.
  5. 5Enable R8 minification and obfuscation for release builds with proper keep rules.
  6. 6Use the Play Integrity API for server-verified device attestation.
MOD · FAQ2 ENTRIESANSWERED

Frequently Asked

How do I securely store API keys?

Don't embed API keys in the app. Use backend proxy for API calls. If unavoidable, store in EncryptedSharedPreferences with keys in Android Keystore. Understand that client-side secrets can always be extracted with enough effort.

Is certificate pinning worth it?

Yes for apps handling sensitive data. Certificate pinning prevents MITM attacks even if a CA is compromised. Implement pinning with backup pins and plan for key rotation.

MOD · BUILD · NEXTS/N · AX-CTA-0001READY

Ready to architect your next Android app?

ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.

ANDROID-ARCHITECT · CONSOLE
S/N · AX-1A-00001
STRATEGIA-X · ENGINEERED · IN · CALIFORNIA