SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC22:22:50
UP00:01
MOD · ARTICLE · SECURITYS/N · AX-ANDROIPUBLISHED
All Articles
Security · Updated 10 MIN

Securing Android Apps: OWASP Mobile Top 10 in Practice

Protect your Android app against the most common mobile risks. Covers credentials, secure storage, certificate pinning, input validation, and integrity checks.

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

Why Mobile Security Requires Different Thinking

A web application runs on servers you control. A mobile application runs on a device you do not. The APK is in the user's hands, where it can be unpacked, read and modified. Anything the app contains, an attacker can eventually read, and any check the app performs, an attacker can eventually remove. That is not a reason to give up. It is a reason to put decisions where they can be enforced: on your server. The device-side work in this article reduces what can leak and raises the cost of an attack. It does not replace server-side authorization. This article was rewritten in October 2026. The earlier version recommended a storage API that has since been deprecated, described the wrong edition of the OWASP list, and said yes to certificate pinning without the caveats Android's own documentation attaches to it.

What is on the OWASP Mobile Top 10 today?

The current list is the 2024 release (OWASP). This article used to call insecure data storage the number one mobile vulnerability. It was never first: it was second in the 2014 and 2016 editions. In the 2024 list it is ninth:
RankRiskWhere this article covers it
M1Improper Credential UsageCredentials and keys
M2Inadequate Supply Chain SecurityNot covered here
M3Insecure Authentication/AuthorizationServer-side, and integrity checks
M4Insufficient Input/Output ValidationInput validation
M5Insecure CommunicationNetwork security
M6Inadequate Privacy ControlsNot covered here
M7Insufficient Binary ProtectionsCode shrinking and integrity
M8Security MisconfigurationNetwork security, debug settings
M9Insecure Data StorageStorage
M10Insufficient CryptographyCredentials and keys
The change at the top is the useful part. The first risk on the list is now about credentials: keys and secrets that are hardcoded or handled badly. That is where to start.

Credentials, Keys and Storage

Do not ship secrets. A key compiled into the APK is published. Android's guidance for hardcoded secrets is to "consider using the KeyChain API when you want system-wide credentials, or the Android Keystore provider to let an individual app store its own credentials that only the app itself can access" (Android). A third-party API key that must stay secret belongs behind your own backend, not in the app. Use the Keystore for keys. The Android Keystore system "protects key material from unauthorized use in two ways". The first is "preventing the extraction of the key material from application processes and from the Android device as a whole". EncryptedSharedPreferences is deprecated. The earlier version of this article built its storage advice on it. The class reference now reads: "This class is deprecated. Use android.content.SharedPreferences instead" (Android). It was deprecated in security-crypto 1.1.0, which is the current stable release. That is less alarming than it sounds, because of what the platform already does. "By default, files that you create on internal storage are accessible only to your app. Android implements this protection, and it's sufficient for most applications" (Android). So the working rules are: - Keep app data in internal storage. For settings, use DataStore. - Store as little as you can. A token you do not persist cannot be extracted from a backup. - When data needs protection beyond the sandbox, encrypt it with a key held in the Keystore, and exclude it from backup, since the key will not be restored with it.

Network Security: Certificate Pinning

Start with what is already true. "Starting with Android 9 (API level 28), cleartext support is disabled by default" (Android). An app that targets a modern API level and does not opt out is already HTTPS-only. Pinning goes further, and the documentation describes it without recommending it: "Normally, an app trusts all pre-installed CAs. If any of these CAs were to issue a fraudulent certificate, the app would be at risk from an on-path attacker. Some apps choose to limit the set of certificates they accept by either limiting the set of CAs they trust or by certificate pinning." "Some apps choose" is the right register. Pinning trades one risk for another: if the pinned key changes and no backup pin matches, the app cannot reach its own server until users install an update. The documentation is specific about both safety valves: - Backup pin. With pinning, "you should always include a backup key so that if you are forced to switch to new keys or change CAs", "your app's connectivity is unaffected. Otherwise, you must push out an update to the app to restore connectivity." - Expiration. Also, "it is possible to set an expiration time for pins after which pinning is not performed. This helps prevent connectivity issues in apps which have not been updated. However, setting an expiration time on pins may enable attackers to bypass your pinned certificates." Pin when the data justifies an outage risk and someone owns the rotation calendar. Otherwise the default trust store with cleartext disabled is a sound position. The debug-overrides block in the sample is the documented way to let a debugging proxy work: those CAs "are trusted only when android:debuggable is true".
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-10-01">
            <pin digest="SHA-256">base64HashOfCurrentPublicKey=</pin>
            <!-- Backup pin: a key you hold but have not deployed -->
            <pin digest="SHA-256">base64HashOfBackupPublicKey=</pin>
        </pin-set>
    </domain-config>

    <!-- Trusted only in debuggable builds -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

Input Validation and Injection Prevention

Everything that enters the app from outside is untrusted: typed input, deep links, intent extras, files and API responses. SQL. Android's mitigation for SQL injection is replaceable parameters, which bind "the user input directly to the query rather than interpreting it as part of a SQL statement" (Android). Room's :name bind parameters do this for you, which is one more reason to keep database access in Room; see the Room migrations guide. Never assemble a query by concatenating input. File paths. A file name taken from a URI can climb out of the directory you intended. Checking for ".." in the string, which the earlier sample did, is not the documented mitigation. This is: "Canonicalize the path using File.getCanonicalPath() and compare the prefix with the expected directory" (Android). Deep links. A deep link is a public entry point that any app or web page can trigger. Validate every parameter against the format you expect before it reaches navigation or a network call, and verify your links so that another app cannot claim them: "implement the android:autoVerify attribute in intent-filter" (Android).
kotlin
// SAFE: Room binds the value; it is never parsed as SQL
@Query("SELECT * FROM users WHERE name = :name")
suspend fun findUser(name: String): User?

// File paths: canonicalize, then compare with the expected directory
@Throws(IllegalArgumentException::class)
fun saferOpenFile(path: String, expectedDir: String): File {
    val file = File(path)
    require(file.canonicalPath.startsWith(expectedDir))
    return file
}

// Deep links: reject anything that is not the shape you expect
private val USER_ID = Regex("^[a-zA-Z0-9]{1,36}$")

fun handleDeepLink(uri: Uri) {
    val userId = uri.getQueryParameter("userId") ?: return
    if (!USER_ID.matches(userId)) return

    navigateToProfile(userId)
}

Binary Protection and Integrity Checks

Two measures address the binary itself. Both raise cost; neither is a guarantee. Code shrinking and obfuscation. R8 removes unused code and renames what remains, which makes a decompiled app harder to read. It is not on unless you turn it on. Android's wording is "You should always enable optimization for your app's release build" (Android), and the earlier claim here that it is enabled by default was wrong. The settings and the keep rules that go wrong are covered in Baseline Profiles and R8. Obfuscation hides names. It does not hide a secret string, which is one more reason not to ship one. Integrity verdicts. The Play Integrity API tells your server whether a request comes from "your genuine app, installed by Google Play, running on a genuine and certified Android device". The decision belongs to the server: with the verdict, "your backend server can respond with appropriate actions to prevent abuse and unauthorized access". An app that checks its own integrity and then trusts the answer has been asked to grade its own exam. The request flow, the verdict fields and the quota are in the Play Integrity guide.
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1Assume the device is hostile. Put every trust decision on the server.
  2. 2The 2024 OWASP Mobile Top 10 leads with Improper Credential Usage. Insecure Data Storage is M9, not M1.
  3. 3Do not ship secrets in the APK. Hold keys in the Android Keystore.
  4. 4EncryptedSharedPreferences is deprecated. Internal storage is private to the app by default; encrypt with a Keystore key when you need more.
  5. 5Cleartext traffic is off by default from API 28. Pin certificates only with a backup pin and an owner for rotation.
  6. 6Bind SQL parameters, canonicalize file paths and validate every deep link parameter.
MOD · FAQ3 ENTRIESANSWERED

Frequently Asked

How do I securely store API keys?

Do not put them in the app. Anything compiled into an APK can be read, and obfuscation does not hide string values. Keep third-party keys on your backend and have the app call your server. For keys the app itself generates, use the Android Keystore, which prevents the key material from being extracted.

Is certificate pinning worth it?

Sometimes. Android documents pinning as something some apps choose, and warns that without a backup key a certificate change forces an app update to restore connectivity. Pin when the data justifies that outage risk and someone owns key rotation. Otherwise rely on the default trust store with cleartext traffic disabled.

Should I still use EncryptedSharedPreferences?

Not for new code. The class was deprecated in security-crypto 1.1.0 and its reference page says to use SharedPreferences instead. Internal storage is already private to your app. Store less, keep settings in DataStore, and when data needs more protection, encrypt it with a key held in the Android Keystore.

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