Why hardcoded OAuth credentials in APKs keep showing up, how an attacker finds them, and what to check for in your own apps.
The pattern
An Android app ships with secrets baked in. Sometimes they live in plain Java or Kotlin strings. Sometimes they are tucked into a native library in lib/arm64-v8a/. Sometimes they are in a config file bundled in assets/. In every case the result is the same. The APK is a signed zip file. Anyone can download it from the Play Store, unpack it, and read what is inside.
The specific secrets we see over and over:
- OAuth 2.0 client secrets used for first-party token flows
- API keys for backend services
- Private signing keys for Proof of Possession (PoP) tokens
- Hardcoded credentials for third-party services (analytics, push, maps, CDN, SMS)
- Database connection strings and shared symmetric encryption keys
Any of these in a shipped APK is a credential leak. The ones that let an attacker speak to your production backend as a trusted client are the ones that lead to full backend compromise.
Why it keeps happening
A few reasons we see across the industry:
- Teams reason about mobile apps as “private” because the code is compiled. It is not. Compiled is not encrypted.
- The OAuth 2.0 spec distinguishes between confidential and public clients. Mobile apps are public clients. They should not have client secrets. In practice, teams use the confidential-client flow because it is simpler, then ship the secret with the app.
- Push, analytics and crash SDKs often require a server-side credential during setup. Developers paste it into the app and move on.
- Code is copied from a backend prototype into the mobile client and the secret comes along.
- Native code (
.sofiles) is treated as safe because “no one reads assembly”.stringsdisagrees.
Finding a hardcoded secret, end to end
This is the general method. In practice you adapt based on how the app is built.
1. Get the APK
Pull from a tested app repository, from a lab target you built yourself, or from a device you own.
adb shell pm path com.example.myapp
# package:/data/app/com.example.myapp/base.apk
adb pull /data/app/com.example.myapp/base.apk
2. Look at the manifest and metadata
apktool d base.apk -o app_decoded
cat app_decoded/AndroidManifest.xml | less
cat app_decoded/apktool.yml
Target SDK, permissions, and intent filters tell you what the app can do and what endpoints it talks to.
3. Decompile the Java and Kotlin
jadx -d app_src base.apk
Then grep for the common tells:
cd app_src
grep -rni "client_secret\|clientSecret\|apiKey\|BEGIN RSA\|BEGIN PRIVATE" .
grep -rni "Bearer \|Basic \|Authorization" .
grep -rni "https://[a-z0-9.-]*\.(amazonaws\|cloudfront\|azurewebsites\|googleapis)" .
Treat every hit as a hypothesis, not a conclusion. A string called apiKey might be a public key fingerprint or a legitimate client identifier.
4. Look inside the native libraries
If the Java side looks clean, the secrets are often one layer deeper.
cd app_decoded/lib/arm64-v8a
strings libsomething.so | grep -Ei "secret|key|-----BEGIN|AKIA|AIza|ya29|eyJ"
For anything that looks like a key you cannot interpret as a string, feed the library to a disassembler and look at the functions that use it. The call sites tell you what the key is for.
5. Look at the assets
find app_decoded/assets -type f | xargs file
JSON configs, properties files, and bundled certificates all end up here. Open them.
6. Prove the secret is live
A string in an APK is a finding. A string that unlocks a production API is a vulnerability.
Build the smallest request that uses the credential. For an OAuth client secret, that is a token request:
POST /oauth/token HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=extracted_id&client_secret=extracted_secret
If a token comes back, the credential is live. If the token can call authenticated endpoints you were not supposed to reach, the credential is a privilege boundary bypass, not just a leak.
Stop there. Document the finding. Do not pivot further without permission from the asset owner.
Why “obfuscated” is not a fix
Common responses when we flag this in a report:
- “We obfuscate the string.” Obfuscation is reversible. If the app can decode it at runtime, so can we. We just run the same code.
- “The key is encrypted in the APK.” It is decrypted by code that is also in the APK. Both halves ship together.
- “We use a native library.”
strings,objdump, and a disassembler get through this quickly. - “The user has to root their device.” No. Pulling an APK does not require root. Decompiling it does not require root. The whole analysis happens on an attacker’s laptop with a copy of the Play Store APK.
None of these change what an attacker can do with ten minutes and a public download link.
What to do instead
- Treat the mobile app as a public client. No client secrets. Use PKCE for OAuth 2.0 authorization code flows.
- Move anything that needs a server-side credential to a backend you control. The app calls your backend. Your backend calls the third party with the credential. This is sometimes called the backend-for-frontend pattern. It also lets you log and rate-limit the calls.
- For device identity, use attestation (Play Integrity, SafetyNet’s successor, or an equivalent). An attested device is a much stronger signal than a baked-in secret and cannot be lifted out of the APK.
- Scan every APK in your release pipeline for the patterns above. Open source tools are good at this. MobSF catches common string patterns, apkleaks catches credential patterns, and semgrep rules can catch bad OAuth usage in source.
- Rotate anything that has ever been shipped in an APK. Shipped once equals leaked forever. There is no way to “unship” a secret.
Defender’s quick checklist
If you ship an Android app:
- Pull your current production APK from the Play Store.
- Run MobSF or apkleaks against it.
- Run
stringsagainst every file inlib/andassets/. - For every hit, ask one question. If an attacker had this, what could they do?
- For anything that can speak to your backend, rotate it today and move the credential server-side.
- Add the same scan to your release pipeline so the next version does not ship a new one.
The research angle on this is boring in the best way. The vulnerabilities are not clever. The fixes are not clever. Teams keep shipping secrets in APKs because no one is checking. If you check, you find them.
If your organization ships a mobile app or exposes APIs that mobile apps consume, mobile application testing and API testing will find these issues before an attacker does.