{"id":100006,"date":"2026-10-16T20:39:24","date_gmt":"2026-10-16T20:39:24","guid":{"rendered":"http:\/\/nandee-post-3"},"modified":"2026-10-16T20:39:24","modified_gmt":"2026-10-16T20:39:24","slug":"m1-improper-credential-usage-examples-fixes","status":"publish","type":"post","link":"https:\/\/www.nandee.ai\/blog\/m1-improper-credential-usage-examples-fixes\/","title":{"rendered":"M1 Improper Credential Usage: Examples and Fixes for Mobile Apps"},"content":{"rendered":"<p>If you could fix only one thing in your mobile app this quarter, make it this one. Improper credential usage sits at M1, the top of the <u>OWASP Mobile Top 10 (2024)<\/u>, and it is one of the easiest risks for an attacker to exploit. A key that ships inside your app is a key you have published.<\/p>\n<p>This article explains what M1 covers, the patterns that cause it in Android and iOS apps, and the fixes that work in production.<\/p>\n<p><strong>Quick answer:<\/strong> M1 Improper Credential Usage means hardcoding secrets in the app, sending credentials insecurely, or storing them where other apps or users can read them. The fix is to keep secrets on your server, give the app short-lived and narrowly scoped tokens, store user tokens in Android Keystore or iOS Keychain, and scan every build for leaked keys.<\/p>\n<h2>What M1 covers<\/h2>\n<p>OWASP groups three kinds of mistakes under M1:<\/p>\n<ol>\n<li>\n<p><strong>Hardcoded credentials.<\/strong> API keys, passwords, signing secrets, or service account files compiled into the app.<\/p>\n<\/li>\n<li>\n<p><strong>Insecure transmission.<\/strong> Credentials sent over cleartext connections, in URLs, or in logs.<\/p>\n<\/li>\n<li>\n<p><strong>Insecure storage.<\/strong> Session tokens and passwords saved in plain preferences, files, or databases.<\/p>\n<\/li>\n<\/ol>\n<p>The common thread is that the app treats something secret as if the device were a safe place to keep it. It is not. Anyone can download your app from the store and unpack it, and many users run rooted or jailbroken devices.<\/p>\n<h2>Why it is number one<\/h2>\n<p>M1 moved to the top of the list in 2024 because the impact is large and the effort for an attacker is small. A leaked key often gives direct access to a backend service, a cloud account, or a payment provider, with no need to break the app itself. Automated tools scan public app stores for exactly these patterns, so a leaked key is often found long before anyone reports it.<\/p>\n<h2>Common patterns in real apps<\/h2>\n<h3>Third-party API secrets in the client<\/h3>\n<p>The most frequent case is a secret key for a payment, messaging, email, or AI service compiled into the app so it can call that service directly.<\/p>\n<p><span style=\"color: rgb(96, 160, 176)\"><em>\/\/ Risky: any secret in the APK is readable<\/em><\/span><br \/><span style=\"color: rgb(0, 112, 32)\"><strong>object<\/strong><\/span> Config <span style=\"color: rgb(102, 102, 102)\">{<\/span><br \/>&nbsp;&nbsp;&nbsp; <span style=\"color: rgb(0, 112, 32)\"><strong>const<\/strong><\/span> <span style=\"color: rgb(0, 112, 32)\"><strong>val<\/strong><\/span> <span style=\"color: rgb(25, 23, 124)\">PAYMENTS_SECRET<\/span> <span style=\"color: rgb(102, 102, 102)\">=<\/span> <span style=\"color: rgb(64, 112, 160)\">&#8220;sk_live_&#8230;&#8221;<\/span><br \/>&nbsp;&nbsp;&nbsp; <span style=\"color: rgb(0, 112, 32)\"><strong>const<\/strong><\/span> <span style=\"color: rgb(0, 112, 32)\"><strong>val<\/strong><\/span> <span style=\"color: rgb(25, 23, 124)\">AI_API_KEY<\/span> <span style=\"color: rgb(102, 102, 102)\">=<\/span> <span style=\"color: rgb(64, 112, 160)\">&#8220;&#8230;&#8221;<\/span><br \/><span style=\"color: rgb(102, 102, 102)\">}<\/span><\/p>\n<p>Obfuscation does not help here. Tools like R8 rename classes and methods, but string values the app needs at runtime remain in the package.<\/p>\n<h3>Keys in build config and resources<\/h3>\n<p>Moving a key into BuildConfig, strings.xml, Info.plist, or a .env file bundled with a React Native or Flutter app does not hide it. These files ship inside the app package.<\/p>\n<h3>Cloud credentials and service account files<\/h3>\n<p>Some apps ship a full cloud access key or a service account JSON file to upload files or read data directly. If that credential has broad permissions, the whole cloud project is exposed.<\/p>\n<h3>Long-lived tokens in plain storage<\/h3>\n<p>User session tokens saved in SharedPreferences, NSUserDefaults, or a plain SQLite table can be read from backups, rooted devices, or by malware with elevated access.<\/p>\n<h3>Credentials in logs and URLs<\/h3>\n<p>Tokens passed as query parameters or printed in debug logs end up in analytics tools, crash reports, and proxy logs.<\/p>\n<h2>How to fix it<\/h2>\n<h3>1. Move secrets to your backend<\/h3>\n<p>The app should never hold a secret that grants access to a third-party service. Instead, the app calls your backend, your backend authenticates the user, and your backend calls the third-party service with the real key.<\/p>\n<p><span style=\"color: rgb(96, 160, 176)\"><em>\/\/ Better: the app asks your backend, which holds the real secret<\/em><\/span><br \/>suspend <span style=\"color: rgb(0, 112, 32)\"><strong>fun<\/strong><\/span> <span style=\"color: rgb(6, 40, 126)\">createPayment<\/span><span style=\"color: rgb(102, 102, 102)\">(<\/span><span style=\"color: rgb(25, 23, 124)\">amount<\/span><span style=\"color: rgb(102, 102, 102)\">:<\/span> <span style=\"color: rgb(144, 32, 0)\">Long<\/span><span style=\"color: rgb(102, 102, 102)\">):<\/span> <span style=\"color: rgb(144, 32, 0)\">PaymentResult<\/span> <span style=\"color: rgb(102, 102, 102)\">{<\/span><br \/>&nbsp;&nbsp;&nbsp; <span style=\"color: rgb(0, 112, 32)\"><strong>val<\/strong><\/span> <span style=\"color: rgb(25, 23, 124)\">token<\/span> <span style=\"color: rgb(102, 102, 102)\">=<\/span> session<span style=\"color: rgb(102, 102, 102)\">.<\/span>accessToken<span style=\"color: rgb(102, 102, 102)\">()<\/span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span style=\"color: rgb(96, 160, 176)\"><em>\/\/ short-lived user token<\/em><\/span><br \/>&nbsp;&nbsp;&nbsp; <span style=\"color: rgb(0, 112, 32)\"><strong>return<\/strong><\/span> api<span style=\"color: rgb(102, 102, 102)\">.<\/span>createPayment<span style=\"color: rgb(102, 102, 102)\">(<\/span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span style=\"color: rgb(96, 160, 176)\"><em>\/\/ your backend endpoint<\/em><\/span><br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; authorization <span style=\"color: rgb(102, 102, 102)\">=<\/span> <span style=\"color: rgb(64, 112, 160)\">&#8220;Bearer <\/span><span style=\"color: rgb(187, 102, 136)\">$token<\/span><span style=\"color: rgb(64, 112, 160)\">&#8220;<\/span><span style=\"color: rgb(102, 102, 102)\">,<\/span><br \/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; amount <span style=\"color: rgb(102, 102, 102)\">=<\/span> amount<br \/>&nbsp;&nbsp;&nbsp; <span style=\"color: rgb(102, 102, 102)\">)<\/span><br \/><span style=\"color: rgb(102, 102, 102)\">}<\/span><\/p>\n<h3>2. Use short-lived, narrowly scoped tokens<\/h3>\n<p>When the app must talk to a service directly (for example, uploading a file to storage), have your backend issue a token that expires in minutes and allows only that one action, such as a pre-signed upload URL.<\/p>\n<h3>3. Restrict keys that must ship<\/h3>\n<p>Some keys are designed to be public, such as map or analytics SDK keys. Restrict them in the provider\u2019s console to your app\u2019s package name and signing certificate (Android) or bundle ID (iOS), and limit them to the APIs you use.<\/p>\n<h3>4. Store user tokens securely<\/h3>\n<p>\u2022&nbsp;<strong>Android:<\/strong> encrypt tokens with a key held in the Android Keystore, so the key material never leaves secure hardware where available.<\/p>\n<p>\u2022&nbsp;<strong>iOS:<\/strong> store tokens in the Keychain with the strictest accessibility class the feature allows, such as kSecAttrAccessibleWhenUnlockedThisDeviceOnly.<\/p>\n<p><span style=\"color: rgb(0, 112, 32)\"><strong>let<\/strong><\/span> <span style=\"color: rgb(25, 23, 124)\">query<\/span><span style=\"color: rgb(102, 102, 102)\">:<\/span> <span style=\"color: rgb(102, 102, 102)\">[<\/span>String<span style=\"color: rgb(102, 102, 102)\">:<\/span> Any<span style=\"color: rgb(102, 102, 102)\">]<\/span> <span style=\"color: rgb(102, 102, 102)\">=<\/span> <span style=\"color: rgb(102, 102, 102)\">[<\/span><br \/>&nbsp;&nbsp;&nbsp; kSecClass <span style=\"color: rgb(0, 112, 32)\"><strong>as<\/strong><\/span> String<span style=\"color: rgb(102, 102, 102)\">:<\/span> kSecClassGenericPassword<span style=\"color: rgb(102, 102, 102)\">,<\/span><br \/>&nbsp;&nbsp;&nbsp; kSecAttrAccount <span style=\"color: rgb(0, 112, 32)\"><strong>as<\/strong><\/span> String<span style=\"color: rgb(102, 102, 102)\">:<\/span> <span style=\"color: rgb(64, 112, 160)\">&#8220;refresh_token&#8221;<\/span><span style=\"color: rgb(102, 102, 102)\">,<\/span><br \/>&nbsp;&nbsp;&nbsp; kSecValueData <span style=\"color: rgb(0, 112, 32)\"><strong>as<\/strong><\/span> String<span style=\"color: rgb(102, 102, 102)\">:<\/span> tokenData<span style=\"color: rgb(102, 102, 102)\">,<\/span><br \/>&nbsp;&nbsp;&nbsp; kSecAttrAccessible <span style=\"color: rgb(0, 112, 32)\"><strong>as<\/strong><\/span> String<span style=\"color: rgb(102, 102, 102)\">:<\/span> kSecAttrAccessibleWhenUnlockedThisDeviceOnly<br \/><span style=\"color: rgb(102, 102, 102)\">]<\/span><br \/>SecItemAdd<span style=\"color: rgb(102, 102, 102)\">(<\/span>query <span style=\"color: rgb(0, 112, 32)\"><strong>as<\/strong><\/span> CFDictionary<span style=\"color: rgb(102, 102, 102)\">,<\/span> <span style=\"color: rgb(0, 112, 32)\"><strong>nil<\/strong><\/span><span style=\"color: rgb(102, 102, 102)\">)<\/span><\/p>\n<h3>5. Use standard sign-in flows<\/h3>\n<p>Use OAuth 2.0 with PKCE and the system browser for sign-in rather than collecting third-party passwords in your own screens. Never store the user\u2019s password on the device.<\/p>\n<h3>6. Keep credentials out of URLs and logs<\/h3>\n<p>Send tokens in headers, never in query strings. Strip tokens and personal data from logs, crash reports, and analytics events in release builds.<\/p>\n<h3>7. Rotate anything that has ever shipped<\/h3>\n<p>If a secret has been in any released build, assume it is known. Rotate it, then remove it from the app. Removing it in the next version is not enough, because old versions stay installed and archived.<\/p>\n<h3>8. Scan every build<\/h3>\n<p>Add secret scanning to your repository and CI pipeline so a key is caught before it reaches a release. Scan both the source code and the built APK or IPA, because some secrets are injected at build time.<\/p>\n<h2>M1 checklist<\/h2>\n<p>\u2610 No third-party secret keys in source, resources, or build config<\/p>\n<p>\u2610 Public SDK keys restricted to your package name, signing certificate, or bundle ID<\/p>\n<p>\u2610 Backend issues short-lived, scoped tokens for direct service access<\/p>\n<p>\u2610 User tokens stored with Android Keystore or iOS Keychain<\/p>\n<p>\u2610 OAuth 2.0 with PKCE for sign-in; no stored passwords<\/p>\n<p>\u2610 No credentials in URLs, logs, or crash reports<\/p>\n<p>\u2610 Every previously shipped secret rotated<\/p>\n<p>\u2610 Secret scanning on every pull request and release build<\/p>\n<h2>FAQ<\/h2>\n<p><strong>What is OWASP M1 Improper Credential Usage?<\/strong> It is the first category in the OWASP Mobile Top 10 (2024). It covers hardcoded credentials, insecure transmission of credentials, and insecure storage of credentials in mobile apps.<\/p>\n<p><strong>Is it safe to hide API keys with obfuscation?<\/strong> No.&nbsp;Obfuscation makes code harder to read, but string values the app uses at runtime remain in the package. Keep secrets on the server instead.<\/p>\n<p><strong>Can I store API keys in BuildConfig or Info.plist?<\/strong> Only keys that are designed to be public, and only if they are restricted to your app in the provider\u2019s console. Secret keys must stay on your backend.<\/p>\n<p><strong>What should I do if a key already shipped in my app?<\/strong> Rotate it immediately at the provider, move the functionality behind your backend, and release an update. Treat the old key as compromised.<\/p>\n<p><strong>How do I find hardcoded secrets in my app?<\/strong> Use a secret scanner on your source code and on the built APK or IPA as part of CI. A mobile security testing platform can also flag hardcoded credentials and explain how to fix them.<\/p>\n<h2>Find leaked credentials before attackers do<\/h2>\n<p><span style=\"color: rgb(0, 0, 0)\">Nandee is an AI-assisted mobile application security testing platform for Android and iOS apps. It flags hardcoded keys and insecure token storage, ranks them by risk, and gives your developers fixes they can review.<\/span><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Improper credential usage is number one on the OWASP Mobile Top 10 (2024). Learn what it looks like in Android and iOS apps, why it happens, and the fixes that keep API keys and tokens out of attackers\u2019 hands.<\/p>\n","protected":false},"author":1,"featured_media":100005,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-100006","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/posts\/100006","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/comments?post=100006"}],"version-history":[{"count":0,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/posts\/100006\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/media\/100005"}],"wp:attachment":[{"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/media?parent=100006"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/categories?post=100006"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.nandee.ai\/blog\/wp-json\/wp\/v2\/tags?post=100006"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}