Security

    How do you find hardcoded secrets in a React Native app?

    Terminal grep search inside an unpacked React Native index.android.bundle showing matches for AWS and Stripe key patterns

    A React Native bundle is a text file that ships inside the APK or IPA. Anyone with apktool and a text editor can read it. That is the security model you start with, so finding hardcoded secrets in your own build is mostly a search problem, and it pays to do that search before App Review or a bug bounty hunter does.

    Short answer

    Unzip the APK or IPA, pull index.android.bundle or main.jsbundle out of the assets folder, and grep it for known secret patterns: AKIA for AWS, AIza for Firebase and Google Maps, sk_live_ and pk_live_ for Stripe, plus the literal substrings token, secret, and apikey. If the app uses Hermes, run the bundle through hermes-dec or react-native-decompiler first to recover the readable string table. Anything sensitive that surfaces must move to a backend you control or to native secure storage before submission.

    What you should know

    • The JavaScript bundle is plaintext on disk by default. A standard React Native build ships index.android.bundle inside the APK at assets/index.android.bundle, and main.jsbundle inside the IPA at the app target root.
    • Hermes adds a binary wrapper, not encryption. Open source tooling like hermes-dec recovers the string table from a Hermes Bytecode file in seconds.
    • react-native-config and react-native-dotenv do not hide values. They inline the value at build time, which means the secret ends up baked into the bundle.
    • AsyncStorage stores cached tokens in cleartext. A jailbroken or rooted device exposes the AsyncStorage JSON file directly.
    • App Store and Google Play scanners flag hardcoded keys. Apple and Google both run static checks against uploaded binaries and can surface findings during review.

    Why are React Native apps so easy to scrape for secrets?

    The short answer is that the JavaScript bundle ships inside the binary as a data file the operating system has to read at startup. The runtime needs the code, so the code is on disk, and any reader with file access can dump it. That is true for every framework that ships a JS bundle, including Expo, React Native CLI, and Capacitor.

    In practice, the Android case is simpler than the iOS case. An attacker downloads the APK from a third-party mirror, extracts it with apktool d app.apk, and the bundle sits at assets/index.android.bundle. On iOS, an IPA pulled from a jailbroken device follows the same pattern at Payload/<AppName>.app/main.jsbundle. Either file opens in a text editor when Hermes is off, and the readable code includes string literals exactly as written in the JavaScript source. According to the React Native security documentation, anything included in your code can be accessed in plain text by anyone inspecting the app bundle.

    This matters because most teams discover hardcoded secrets the same way attackers do: by accident, after the app is already in store review. The 2024 CloudSEK research on exposed AWS keys reported approximately one in 200 mobile apps shipping a discoverable AWS access key in the bundle, which gives a rough sense of how often the leak survives into production builds.

    How do you pull the JavaScript bundle out of an APK or IPA?

    The honest answer is that any developer can do this in under five minutes with free tools. For an Android build, install apktool, then run:

    apktool d -s app-release.apk -o unpacked
    grep -E "AKIA|AIza|sk_live_|sk_test_" unpacked/assets/index.android.bundle
    

    The -s flag skips the smali decompile step, which is faster when the goal is only the JS bundle. For an iOS build, unzip the IPA with unzip app.ipa, then look in Payload/<AppName>.app/main.jsbundle. Both files are large, often in the 5 to 15 megabyte range, but a single grep pass through them is fast.

    Three regex patterns catch most credential leaks: AWS access keys match AKIA[0-9A-Z]{16}, Firebase and Google Maps keys match AIza[0-9A-Za-z_-]{35}, and Stripe live keys match sk_live_[0-9a-zA-Z]{24,}. Beyond those, grep for the substrings apikey, bearer, password, client_secret, and the name of any service called directly from JavaScript. The Payatu static analysis writeup on React Native documents the same pattern set, which has been stable for years because the formats come from the upstream providers themselves.

    What changes when an app uses Hermes bytecode?

    The short answer is that the bundle stops being human-readable JavaScript, but the string table inside it is still trivially recoverable. Hermes is the bytecode engine that ships by default on React Native 0.70 and later, and the build step converts the JS bundle into a binary .hbc file. That binary keeps every string literal in a flat table, in clear UTF-8, because the runtime needs to load those strings at execution time.

    Two tools cover the recovery side. P1 Security's hermes-dec is a disassembler and decompiler that produces readable pseudo-JavaScript from the HBC file, including all string literals. The second is react-native-decompiler, maintained by Numan, which targets the non-Hermes index.android.bundle and runs ESLint and Prettier over the output for readable code. Neither tool supports encrypted custom bundles, like the ones Meta ships in Facebook and Instagram, but those are the exception in the broader React Native universe.

    For builders, the takeaway is that turning on Hermes does not protect a secret. It adds roughly a 30-second step to the extraction process. Plan as if the bundle were plaintext, because functionally it is.

    Which tools actually scan for hardcoded secrets in React Native?

    Five tools cover most of the practical workflow.

    ToolTypeWhat it scansNotes
    MobSF (Mobile Security Framework)Static and dynamicAPK, IPA, AAB, plus the unpacked JS bundleOpen source, runs locally, generates an OWASP MASVS aligned report
    RNSECStatic, React Native specificSource repo and packaged bundleDetects secrets, insecure storage, and network misconfig
    TruffleHogStatic, secret scannerSource repos and binariesWorks on the extracted bundle as a plain text file
    GitleaksStatic, secret scannerGit history and working treeBest run as a pre-commit hook before the secret reaches the bundle
    hermes-dec plus grepManualCompiled HBC bundlesPairs well with MobSF for verification

    In a typical pre-submission flow, the order looks like this: Gitleaks on the repo, MobSF on the compiled APK or IPA, then a manual hermes-dec pass if anything looks off. The OWASP MASVS treats hardcoded credentials under the MASVS-STORAGE category, with the underlying weakness mapped to MASWE-0005 (API Keys Hardcoded in the App Package), so any scanner that claims MASVS alignment will include a check for this class of finding.

    For builders who want an external automated read of their compiled build before submission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning aligned with OWASP MASVS for no-code and vibe-coded apps, including React Native and Expo outputs.

    Where should the secret live if not in the app bundle?

    The short answer is on a backend you control, with the app calling that backend over TLS. The React Native security documentation states this directly: build an orchestration layer between the app and the resource, then forward the request from a serverless function or API gateway that holds the secret server side. AWS Lambda, Google Cloud Functions, Supabase Edge Functions, and Cloudflare Workers all fit this pattern.

    For values that must live on the device after a successful login, like a session token or a user-specific API key, use the platform secure storage. On iOS that is Keychain Services. On Android that is EncryptedSharedPreferences backed by the Android Keystore. Cross-platform libraries that wrap both APIs include react-native-keychain and expo-secure-store, and either is acceptable for a session token.

    What does not work is react-native-config or react-native-dotenv for live secrets. Those libraries inline the value at build time, which means the secret is in the bundle and visible to any of the tools above. Treat them as configuration tools for environment names (staging, production) and feature flag selectors, not as secret managers.

    What to watch out for

    A few traps come up often enough to flag.

    First, base64 is not encryption. Wrapping an API key in Buffer.from(key).toString('base64') does not hide it from a grep pass on the bundle, because the base64 string is still a literal. Same for ROT13, XOR with a fixed constant, and any other reversible scheme that ships its own key in the same binary.

    Second, the bundle includes test keys too. A staging Stripe sk_test_ key or a Firebase debug token is still a credential, and many automated scanners flag them. Strip test credentials from the production build the same way you strip live ones, because reviewers and bug bounty hunters do not always distinguish.

    Third, source maps shipped to crash reporters can leak. Sentry and Bugsnag both accept source maps for symbolication; if the symbolication endpoint is public or weakly protected, the original variable names and any embedded constants come back through that side channel. Set source map uploads to authenticated endpoints only.

    The persistent myth here is that minification or obfuscation makes a bundle secure enough. It does not. Minification shortens identifiers; it does not remove string literals. The OWASP guidance is direct on this point: under MASVS-STORAGE, hardcoded credentials are rated as easy to exploit and easy to detect, which together describe a vulnerability class with a very low cost to attackers.

    Key takeaways

    • Scan the compiled bundle, not just the source repo. The bundle is what ships, and the bundle is what attackers read.
    • Pair a secret scanner with a decompiler. Gitleaks on the repo plus hermes-dec on the build covers both the plaintext and Hermes cases.
    • Move live secrets to a backend. Lambda, Edge Functions, and API gateways exist exactly for this purpose, and the official React Native security docs recommend the pattern.
    • Use Keychain or EncryptedSharedPreferences for device-side tokens. AsyncStorage is for non-sensitive cache, and only that.
    • Some teams outsource the pre-submission scan. Platforms like PTKD.com (https://ptkd.com) run MASVS aligned checks against APK, AAB, and IPA before the build goes to App Store Connect or Google Play Console.
    • #react native
    • #hardcoded secrets
    • #api keys
    • #mobile security
    • #owasp masvs
    • #hermes
    • #static analysis

    Frequently asked questions

    Does using Hermes hide my API keys?
    No. Hermes compiles JavaScript into bytecode, but the string table inside the .hbc file keeps every literal in clear UTF-8 for the runtime to load. Tools like hermes-dec from P1 Security recover the strings in seconds, so a Hermes build is not meaningfully harder to scrape than a non-Hermes build. Plan as if the bundle were plaintext.
    Is react-native-config safe for storing API keys?
    Not for live secrets. react-native-config and react-native-dotenv inline the value into the JavaScript bundle at build time, which is exactly the place an attacker looks. They are appropriate for environment selectors like API base URLs and feature flag names, but any value worth a credential check ends up baked into the binary and visible to grep.
    What is the fastest way to scan an existing APK for secrets?
    Run apktool d -s on the APK, then grep the unpacked assets/index.android.bundle for AKIA, AIza, sk_live_, and the substrings token, secret, and apikey. The full scan takes under three minutes on a typical bundle. MobSF gives a cleaner report, but raw grep finds the same leaks and runs on any machine without setup.
    Do I need to scan iOS builds separately from Android builds?
    Yes, because the file paths differ. iOS bundles ship at Payload/<AppName>.app/main.jsbundle inside the IPA. Android bundles ship at assets/index.android.bundle inside the APK. The contents are usually similar but not identical, because conditional code paths (Platform.OS) include different strings. Scan both before submission to App Store Connect and Google Play Console.
    If I find a hardcoded key in a shipped build, what should I do first?
    Rotate the credential at the upstream provider immediately. Stripe, AWS, Firebase, and Google all support revoking and reissuing keys without downtime. After rotation, push a new build that fetches the value from a backend you control, then file a removal request with the original. Treat the leaked key as compromised; assume it has been seen.

    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