AI-coded apps

    How to Fix Exposed API Keys in Android Apps

    A decompiled Android APK revealing a hardcoded API key from BuildConfig, alongside a backend proxy architecture that keeps the secret server-side.

    If an API key is exposed in your Android app, the first move is to treat it as compromised and rotate it immediately, because anyone can decompile an APK and read the value. There is no safe place to hide a real secret inside the app: putting it in BuildConfig, strings.xml, or gradle.properties still compiles the value into the APK, where it is trivially extracted. The durable fix is to remove the secret from the app entirely and route the calls through a backend proxy you control, so the key lives on your server and never ships to the device. Keys designed to be public, paired with server-side rules like Supabase Row Level Security, are the only ones that belong in the app.

    Short answer

    Rotate the exposed key now, then move it off the device. Per Google's API key best practices, you should not embed keys in code, because a client binary can be inspected; an Android APK decompiles easily, so BuildConfig, strings.xml, and gradle-injected values are all readable. The fix is a backend proxy: your app calls your server, and your server holds the secret and calls the third-party API. Per the OWASP MASVS, secrets must not be stored in the client. Only keys meant to be public belong in the app, and only when a server-side boundary such as Supabase Row Level Security limits what they can do.

    Which keys are safe versus unsafe in an Android app

    The safe-versus-unsafe distinction comes down to whether a key is a privileged secret or a public identifier. A privileged secret grants access to billed services or private data on its own, and it must never ship in the app. Examples include an OpenAI or other third-party API secret, a Stripe secret key, cloud provider credentials, and a Supabase service_role or secret key that bypasses your database rules. If leaking the key lets someone spend your money or read your data, it is unsafe to embed.

    A public identifier is designed to be visible in a client and is safe to ship when it is properly restricted. A Supabase publishable or anon key is meant to be public, but only safe when Row Level Security governs what it can reach, and a client key like Google Maps is safe when restricted to your app's signature and package. The test is what the key can do by itself: if it is powerless without a server-side rule or restriction backing it, it can live in the app; if it carries privilege on its own, it cannot.

    Immediate step: rotate the exposed key

    The first action is containment through rotation, because an exposed key in a shipped APK is public and cannot be un-shipped. Revoke the compromised key at the provider and issue a new one, so the leaked value stops working even though it remains readable in copies of your app that are already out. Do this before anything else, since the app remains vulnerable for as long as the old key is valid, and decompiling a published APK takes minutes.

    Alongside rotating, check for abuse. Review the provider's usage and billing dashboards for unexpected activity that suggests the key was already found and used, and set or tighten spending limits and alerts so a future leak is capped. Rotation stops the bleeding, but it is only the first half: if you reissue a secret and put the new one back into the app the same way, you have simply exposed a fresh key. Rotation must be paired with actually moving the secret off the device, which is the architectural fix below.

    Why BuildConfig and strings.xml will not hide it

    A common mistake is believing that moving a key out of source code and into BuildConfig, strings.xml, or gradle.properties protects it, but it does not, because all of these end up in the APK. BuildConfig fields are compiled into the app as constants, string resources are packaged in the APK, and values injected at build time from gradle.properties or local.properties are baked into the compiled output. Any of them can be recovered by decompiling the APK with common tools, so the key is readable to anyone who looks.

    Obfuscation does not save you either. Code shrinkers and obfuscators rename symbols but do not reliably hide string literals, so an obfuscated APK still yields the key with a string search. Even native code in the NDK only raises the effort slightly, since the value can be extracted from the compiled library or observed at runtime. The honest conclusion is that there is no location inside the app where a real secret is safe, because everything the app can read at runtime, an attacker with the APK can read too. This is why the fix is architectural, not a better hiding spot.

    The fix: a backend proxy

    The durable fix is to put the secret on a backend and have your app call that backend instead of the third-party API directly. In this pattern, your app sends its request to a small server you control, that server holds the API secret and makes the privileged call to the third party, and it returns only the result to the app. The secret never leaves your server, so decompiling the app reveals nothing useful, and rotating the key is a server change rather than an app update.

    Build the proxy with its own protections, since it is now an internet-facing endpoint. Authenticate your app's users so the proxy is not an open relay anyone can call, add rate limiting so a leaked user credential cannot run up unlimited cost, and scope each request to what that user is allowed to do. This is more work than embedding a key, but it is the only approach that actually keeps the secret secret, and it gives you a single place to monitor usage, enforce limits, and rotate keys without shipping a new build.

    Supabase and the Row Level Security boundary

    Supabase is a common case in AI-built apps, and it illustrates the safe-versus-unsafe boundary clearly. The publishable or anon key is meant to be included in your app, but it is only safe because Row Level Security policies on your tables decide what any request using that key can read or write. Without Row Level Security enabled and written correctly, the anon key can reach data it should not, which is how many exposed-Supabase incidents happen: the key was public by design, but the database had no server-side rules limiting it.

    The service_role or secret key is the opposite. It bypasses Row Level Security entirely and has full access, so it must live only on a server, never in your Android app. If a service_role key is in your app, treat it as a serious exposure: rotate it immediately and move it to your backend. The general lesson applies beyond Supabase: a client-safe key is safe only because a server-side boundary constrains it, so enable and verify that boundary, and keep the privileged key server-side.

    Verify your APK is clean

    After rotating and re-architecting, verify rather than assume, because a stray copy of the key can remain. Build the release APK and inspect it the way an attacker would: decompile it with a tool such as jadx or apktool and search the output for the key patterns, your provider prefixes, and terms like secret or token. Also scan your source repository and its history with a secret scanner, since a key removed from the current code may still sit in an old commit that anyone with repo access can read.

    Make this a repeatable check rather than a one-time cleanup. Run a secret scan in your CI pipeline so a newly introduced key is caught before it ships, and re-inspect the APK when you change how configuration is injected. The goal is to confirm that the build you distribute contains no privileged secret and that only properly restricted public keys remain. Verification closes the loop: rotation stops the old key, the backend keeps new secrets off the device, and scanning proves the result.

    Safe versus unsafe keys

    Sorting your keys into the two categories tells you what to move. The table below compares them.

    Key typeShip in the app?Why
    Supabase publishable or anon keyYes, only with Row Level SecurityA public identifier limited by server-side rules
    Restricted client key, such as MapsYes, when restricted to your appScoped to your signature and package
    OpenAI or third-party API secretNoGrants billed, privileged access on its own
    Stripe secret keyNoFull account access
    Supabase service_role or secret keyNoBypasses Row Level Security, server-only

    Read the table by what a key can do alone: if it is powerless without a server-side rule, it can ship; if it carries privilege by itself, it must not.

    Remediation checklist

    Working through the steps in order contains the exposure and prevents the next one. The checklist below covers them.

    StepActionDone?
    Rotate nowRevoke the exposed key and issue a new one[ ]
    Check abuseReview usage and billing, set limits[ ]
    Remove from appDelete it from BuildConfig, strings.xml, gradle[ ]
    Add a backend proxyRoute the calls through your server[ ]
    Restrict remaining keysScope client keys, enable Row Level Security[ ]
    VerifyDecompile the APK and scan the repo for secrets[ ]

    The step people skip is the fourth: without a backend proxy, rotating just exposes a new key, so moving the secret off the device is what actually fixes it.

    Scan your build for exposed keys

    Because a leaked key is often hard to spot by eye, especially when a build tool or an AI code generator injected it, scanning the build is the reliable way to know what your app actually contains. AI app builders in particular have a habit of hardcoding provider keys directly into the client, so the exposure may not be something you wrote.

    A scanner like PTKD.com analyzes your build and reports issues such as leaked keys and secrets, insecure data storage, and over-broad permissions by severity, mapped to OWASP MASVS, so you can find an embedded key before it reaches users or, after a leak, confirm your rebuilt app is clean. To be clear about the boundary: PTKD does not rotate your keys at the provider or build your backend proxy. It finds the exposure so you know what to rotate and remove.

    What to take away

    • Treat an exposed API key as compromised and rotate it immediately, because a published APK is public and decompiles in minutes.
    • There is no safe place to hide a real secret in the app: BuildConfig, strings.xml, gradle-injected values, obfuscation, and even native code are all recoverable.
    • Fix it architecturally with a backend proxy, so the secret lives on your server, your app calls the server, and rotating a key is a server change.
    • Only keys designed to be public belong in the app, and only when a server-side boundary such as Supabase Row Level Security limits what they can do.
    • Verify the result by decompiling your APK and scanning your repo, and use a tool like PTKD.com to catch embedded keys before they ship.
    • #api keys
    • #android
    • #backend proxy
    • #secrets
    • #supabase

    Frequently asked questions

    What should I do first if my API key is exposed in an Android app?
    Rotate it immediately: revoke the compromised key at the provider and issue a new one, so the leaked value stops working even though it stays readable in copies of your app already distributed. Do this before anything else, since decompiling a published APK takes minutes. Then check the provider's usage and billing for abuse and set spending limits. Rotation is only the first half; you must also move the secret off the device.
    Does putting the key in BuildConfig protect it?
    No. BuildConfig fields are compiled into the app as constants, strings.xml resources are packaged in the APK, and values injected from gradle.properties or local.properties are baked into the build, so all of them can be recovered by decompiling the APK. Obfuscation does not reliably hide string literals, and even native NDK code only raises the effort slightly. There is no location inside the app where a real secret is safe from someone with the APK.
    How does a backend proxy fix exposed keys?
    Your app sends its request to a small server you control, that server holds the API secret and makes the privileged call to the third party, and it returns only the result to the app. The secret never leaves your server, so decompiling the app reveals nothing useful, and rotating the key becomes a server change rather than an app update. Protect the proxy with user authentication, rate limiting, and per-request scoping, since it is now internet-facing.
    Which API keys are safe to ship in an Android app?
    Only keys designed to be public and constrained by a server-side boundary. A Supabase publishable or anon key is safe only when Row Level Security governs what it can read or write, and a client key like Google Maps is safe when restricted to your app's signature and package. Privileged secrets, an OpenAI or third-party API secret, a Stripe secret key, cloud credentials, or a Supabase service_role key, must never ship, because they grant access on their own.
    How does Supabase Row Level Security fit in?
    The Supabase anon or publishable key is meant to be in your app, but it is safe only because Row Level Security policies decide what any request using it can reach. Without Row Level Security enabled and correct, that public key can access data it should not, which is how many exposed-Supabase incidents happen. The service_role or secret key bypasses Row Level Security entirely and must live only on a server, never in your Android app.
    How do I verify my APK has no exposed keys?
    Build the release APK and inspect it as an attacker would: decompile it with jadx or apktool and search for key patterns, provider prefixes, and terms like secret or token, then scan your source repository and its history with a secret scanner, since a removed key may remain in an old commit. Make it repeatable by running a secret scan in CI, and use a build scanner such as PTKD.com (https://ptkd.com) to catch embedded keys before they ship.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free