Hardcoded Secrets in Mobile Apps

Compiling is Not Hiding: Hardcoded Secrets in Mobile Apps

Most of the secrets I’ve found in mobile apps were not stolen. They were shipped.

What do I mean by that? I mean nobody needs to break into the vault when the combination is printed on every door. Compiling is not hiding: it changes the format of the code, not who can read the strings inside it.

In mobile pentests, the pattern keeps repeating: service credentials stored inside the binary, well within the reach of anyone with access to an app store. All I need is an unzip and a strings command. What still surprises development teams, and, worse, security teams that review the backend with rigor but treat the app itself as a black box, is how little effort it takes.

The Binary Is a Public Document

Technically, the problem is simple: An APK is a zip file; so is an IPA. Inside the file we can find the bytecode, native libraries, configuration files, manifests and assets, and all of it travels to every device that installs the app. Obfuscation renames classes and methods, but it does not touch the string literals. An API key is still an API key; it just sits inside a file that anyone can open.

Organizationally, the problem is more stubborn. Perhaps a secret enters the code for convenience: a staging token that “will only stay until the sprint ends.” Maybe it’s an analytics key copied from a sample, or a service credential shared across environments because separating them was work. The code review checks the logic, the pipeline scans the dependencies. But nobody in the process opens the final artifact, the version that actually lands in the public app store and eventually reaches the user.

Then there is the human factor, the sentences I can recite by heart: “It’s only the maps key.” “The app is closed.” “We obfuscate.” But obfuscation is not protection, and delay is not control. Each of these sentences describes the same illusion of closed code; the belief that what is compiled is hidden.

The Attacker’s Point of View

After running so many mobile security assessments, I’ve learned to look at the artifact before I look at the API. From an attacker’s point of view, the app is the best scenario possible: it is published, it is free, and it ships with usage instructions built in.

If I’m an attacker, I don’t need a zero-day. I download the app from the store, open the package, extract the strings, and read them. Within minutes I have the production hosts, the staging ones nobody turned off, the internal addresses that leaked through configuration and, with luck, a key with too many permissions. Why attack a server with a WAF, monitoring, and someone on call, when I can use legitimate credentials that the company itself distributed?

That first minute looks like this:

unzip -o app.apk -d app/
strings -a -n 6 app/classes*.dex | \
grep -iE 'api[_-]?key|secret|token|https?://'


Then there’s scale: The same key sits in every installation. There is no “compromised user’s” key, there is everyone’s key. Revoking it means shipping a new version and waiting for thousands or millions of devices to update, while the attacker keeps using the old one and rotating it can break the app for everyone who has not updated. The key is not on the server. It is on every phone.

Triage Without Noise

The hard part of hunting secrets in binaries is rarely finding strings; it’s separating the ones that matter from those that don’t. A grep for “password”, “token”, or “secret” across an APK can return thousands of lines: screen labels, class names, error messages, SDK constants. When the list is too long, nobody reads it, and the real finding hides in the volume.

This is why I developed find_secrets.sh, which extracts strings from DEX files, native libraries, assets and plists, and organizes the result by signal, not volume. The full source is on GitHub.

Keywords alone do not make the cut. The script requires a keyword, an assignment and a value that looks like a secret, such as api_key followed by an equals sign and eight or more characters that end the string. A keyword alone describes an intention, while the trio describes a secret.

For well-known formats, the script trusts established patterns, which it decodes before judging them: AWS, Google, Stripe, GitHub and Slack keys, plus JWTs. Private keys get separate treatment: the script reads the context and annotates whether the hit looks like a TLS library constant or a real base64 body. It annotates; it does not hide. If the triage silently discards things, I don’t think you can trust it.

On the network side, the script follows the same logic. An app’s URL list is huge and full of garbage, such as https://token, https://:%d or hosts with template braces. The script validates each host before listing it. Each valid URL appears once, grouped by host, with the files where it was found, and the main report shows only unique hosts. Within seconds, you can answer the questions that matter most: where does this app talk to, and what does that reveal about my attack surface?

https://api.example.com  (2 URLs, 3 files)
https://cdn.example.org  (1 URL, 2 files)


The tool does not decide what’s a secret, instead it tells you where to look. Validation stays human: with openssl, with the provider’s API, with a real privilege test.

Where MobSF Fits

If you do mobile security, you know MobSF, and it deserves the respect it has earned. It is an open-source, all-in-one framework that combines static and dynamic analysis for Android, iOS and Windows apps, and it runs self-hosted, typically in a Docker container. Its static analyzer goes well beyond secrets: manifest and permission review, weak crypto, insecure code patterns, certificate analysis, and hardcoded secrets among the findings. I’m not positioning my script against it. If you must pick one tool for a full mobile assessment, pick MobSF.

So why write a bash script? Because the two tools are designed to answer different questions. MobSF asks “what’s wrong with this app?” and answers by delivering a broad report you then have to triage. The script asks one narrow question, “what did we ship that an attacker can read in five minutes?” and delivers a short list. It needs no server, no database, and no upload, only unzip, strings, and grep. That matters when the APK is under NDA and cannot leave your machine, when you are on a client’s laptop with no Docker, or when you want a pipeline gate that finishes in seconds. It also treats the network surface as a first-class finding: unique hosts, each URL once, files attached.

This script’s approach has specific limitations. It works at the string level, but does not decompile. It neither reads the manifest semantically, nor follows data flow. Its analysis is static, it does not run the app. A key assembled at runtime from a concatenation of three string fragments will slip past it, and MobSF’s code analysis and dynamic instrumentation have a far better shot in that scenario. Use the script for the first pass, follow with MobSF for depth, and finish with a human review for the verdict.

What to Do with the Findings

Treat every secret found in the binary as leaked. Not “potentially exposed,” but “leaked”, because it sits in every installation, and anyone could have copied it. It is public in perpetuity. The first remediation action is to rotate the keys, and the question that separates security maturity from security theater is simple: can you revoke that key within an hour? If not, you don’t control that key. Whoever reads it does.

Then, take the secret out of the app. If the app must talk to a third-party service with privileged credentials, have your backend do the talking: the app authenticates the user, the server holds the credential and makes the call. Remember that anything the app can read, an attacker can read. There is no hidden vault inside a device when an attacker controls it.

Some keys must be shipped in the app, such as maps, analytics, and push. For those, the goal changes from hiding to limiting: restrict by package name, signing certificate, or bundle ID, then restrict by API, and enforce quotas. A restricted key that gets stolen is an annoyance, where an unrestricted one could be a bill or an impending incident.

Finally, scan the artifact, not just the source. Secrets enter through the build: generated configuration files, variables injected in the pipeline, SDKs that bring their own credentials. A repository scan sees none of that. Running a triage over the final APK or IPA on every release closes the gap between what was reviewed and what was distributed. A crude gate is enough to start, and you can tune it from there:

./find_secrets.sh -o report.txt -u urls.txt app-release.apk
! grep -q '^\[' report.txt || { echo "Hardcoded secrets found"; exit 1; }


That gate is deliberately blunt. It fails the build on any finding, low-severity ones included, so expect to tune it before the team trusts it. And even a tuned gate answers only one question: does this release ship a secret? It says nothing about why the secret got there, which is where process comes in.

Secrets are not a one-time fix. Every release is a new chance to ship one, so the check must be part of every release, not a once-over and an “LGTM.” Frameworks like OWASP MASVS and MASTG are powerful, but only if they are operationalized. A checklist item that says “no hardcoded secrets,” ticked after reviewing the source while the APK carries the key, is compliance theater. Used correctly, these frameworks guide what you test in the artifact. Used poorly, they become a rubber stamp.

The app is not a black box. It is a file the company published, signed, and distributed to anyone who wants to open it. Consider every secret that travels in it as already published; it only needs someone to read it. If the app has to carry a “secret” key to work, it was never a secret.

Wondering what’s hiding in your mobile app?

Our mobile application penetration testers extract and analyze the same artifacts an attacker would. If you ship a mobile app, we can tell you what’s exposed before someone else does.

Contact Us

More Posts

Send Us A Message

Get the Debrief in Your Inbox

New findings, series updates, and usually some bad puns. Delivered when we actually have something worth saying.

No spam. Unsubscribe whenever. We’re evil, not rude.

This website uses cookies

We use cookies to personalize content, provide social media features, and analyze our traffic. We also share information about your use of our site with our analytics partners. You can change your preferences at any time. For more information, please see our Privacy Policy