Security

    Can Apple see my hardcoded secrets in minified bundles?

    Nested iOS app bundles inside an IPA showing extensions, App Clip, and JavaScript chunks each carrying hardcoded API keys

    The minification anxiety gets sharper when the build process emits more than one bundle. A standard Expo or React Native iOS app rarely ships a single artifact, and each extra bundle inside the IPA is another place where a hardcoded key can live.

    Short answer

    Apple's App Review does not scan any of the bundles inside your IPA for hardcoded secrets. The main app binary, every extension, the App Clip, the watchOS target, and every minified JavaScript chunk are signed and indexed, but they are not searched for API keys. Anyone with the published IPA can unzip it, walk every nested bundle, and run strings against each binary or JS file. Minification rewrites identifiers; it leaves string literals intact in every bundle it touches.

    What you should know

    • Modern iOS apps are multi-bundle. Your IPA usually contains the main app, one or more extensions, sometimes an App Clip and a watchOS app, plus framework bundles inside Frameworks/.
    • Apple signs every nested bundle. The ITMS validator verifies signatures, entitlements, and Privacy Manifest entries on each one. It does not search any of them for credentials.
    • Each extension has its own bundle ID, target, and binary. That means the secret you import once in shared code may end up duplicated inside several Mach-O files in the same IPA.
    • JavaScript bundles split. Metro and Expo can emit a primary bundle plus dynamic imports, and Hermes can compile that JavaScript to bytecode. The minified strings persist across every chunk.
    • App Thinning does not protect secrets. The thinned variants delivered through the App Store CDN each contain the same string literals; thinning only removes assets and code for other device classes.

    Why does a single iOS app ship as several bundles?

    The IPA an attacker downloads is a ZIP archive holding a Payload/ directory, and inside that directory you find a .app bundle that itself contains nested PlugIns/, AppClips/, Watch/, and Frameworks/ directories. Each subdirectory holds its own .app or .appex bundle, complete with its own Info.plist, its own executable, and its own Privacy Manifest. The Apple App Extension Programming Guide documents this nesting: an extension is delivered inside its containing app, but at runtime it operates with a separate process, separate sandbox, and separate code signing.

    For a React Native or Expo app, the layout extends a level deeper. The native iOS shell wraps a JavaScript engine that loads a main.jsbundle, plus any lazy chunks produced by your Metro configuration. When Hermes is enabled the JavaScript file becomes a .hbc bytecode artifact instead of plain JavaScript. Either way, those JS bundles sit inside the same .app directory the attacker is already walking.

    The consequence: if you have pasted a token into shared code that gets imported by both the main app and a share extension, the credential ends up compiled into both binaries. Running strings twice against two different files inside the same IPA returns the same key.

    Does App Review actually inspect any of these bundles for secrets?

    No. The automated layer of App Review (the ITMS validator) does run static checks across every bundle inside the IPA. The Apple App Store Connect documentation describes those checks: missing or unjustified required-reason API calls (UserDefaults, file timestamps, system_boot_time, disk space), missing Privacy Manifest entries for SDKs on Apple's published list, entitlement mismatches between the parent app and its extensions, metadata that disagrees with the build. The validator iterates over PlugIns/, AppClips/, Watch/, and Frameworks/ to confirm signatures and required-reason coverage. It does not invoke a credential scanner.

    Human reviewers spend roughly fifteen minutes on the average submission and use that time to compare the live app to its store metadata. They do not run hexdump, they do not Frida-hook the runtime, and they do not inspect main.jsbundle for tokens. The submission volume per day rules out deep credential analysis at scale.

    If Apple did flag a hardcoded Mailgun key in your share extension, that would actually help. In practice the build passes review, the App Store ships every nested bundle, and the credential becomes an attacker problem the moment a stranger downloads your app.

    Where do secrets end up when your app has extensions and App Clips?

    In every bundle that imports the code that holds them. The mistake most teams make is reasoning about secrets as one place in the codebase. The iOS build pipeline compiles each target independently. The share extension you added so users could send a URL into your app has its own Xcode target, its own Build Settings, and its own compiled binary. Any constant string referenced from a Swift, Objective-C, or React Native module imported by that target gets baked in.

    A common pattern: a developer puts a Supabase service-role key into a shared Auth.swift file, then imports that file in the main app, the share extension, and the widget. The IPA now contains three independent copies of the same secret inside three signed Mach-O files. Each is reachable by the same strings command run against each binary in turn.

    For Expo and React Native apps the same dynamic applies on the JavaScript side. The main JS bundle is one artifact; a widget written using the Expo apple-targets plugin is a separate native target compiled in Swift. If the widget reaches into shared TypeScript via a native module bridge, any constants reachable from that bridge can end up duplicated inside both the JS bundle and the Swift compile of the widget.

    The Frida-driven dynamic-analysis methodology in Spaceraccoon's writeup on iOS credential hunting found SDK credential misuse in 68 of 100 popular iOS apps using only static methods against the binaries inside the IPA. The methodology generalises across nested bundles; the same grep that reads main.jsbundle reads the share extension Mach-O.

    Does App Thinning hide secrets across multiple variants?

    App Thinning slices a single submitted archive into device-specific variants for delivery through the App Store CDN. Slicing removes assets that the target device cannot use: an iPhone variant ships without iPad assets, an arm64 variant ships without code for other architectures, On-Demand Resources are downloaded later instead of bundled at install time.

    What slicing does not do is strip string literals. Every variant contains the same compiled string table for the executable it ships. If your release archive contained a hardcoded AWS access key in the main binary, every thinned variant downloaded by every user contains the same key. The attacker downloading the iPad variant gets the same credential as the attacker downloading the iPhone variant.

    Bitcode (when it was still supported, before Xcode 14) muddied this conversation, since the App Store could recompile the bitcode representation for a given device. Even then, string literals persisted across the recompile: no compiler pass would silently strip them, because that would break runtime equality checks and JSON parsing.

    The practical implication: do not think about the bundle as a single thing the attacker has to defeat. Think of it as a set of artifacts that all carry the same plaintext data, downloaded by every device.

    How can you audit every bundle in your IPA in one pass?

    Treat the IPA as a tree, not a file. The audit is a recursive scan over every nested .app, .appex, .framework, and JS bundle:

    unzip MyApp.ipa -d MyApp
    cd MyApp/Payload
    find . -type f \( -name "*.jsbundle" -o -name "*.hbc" -o -perm -u+x \) \
      -exec strings {} \; \
      | grep -E "(sk_|pk_|AIza|eyJ|AWS|xox|SUPABASE|service_role)"
    

    That loop walks the main app binary, every PlugIns/.appex, every Frameworks/.framework, every AppClips/*.app, and every JS or Hermes bundle inside any of them. Anything that surfaces is a credential present in at least one signed bundle that ships to every user.

    OWASP MASTG iOS security testing procedures describe the same workflow inside a formal mobile security testing framework. Automated tools such as MobSF, trufflehog, and gitleaks generalise the patterns across thousands of credential prefixes and apply them to every binary in the archive.

    For builders who want an external automated read of their full IPA, including every nested extension and JS chunk, before submission, PTKD.com (https://ptkd.com) is one of the platforms that runs OWASP MASVS-aligned analysis on the compiled IPA rather than the source repository. That distinction matters here, because a source-only scanner cannot see what the compiler actually packaged inside each .appex.

    Bundle inside your IPAWhat it usually containsSecret-leak surface
    Main app binaryCompiled Mach-O of your main targetAnything imported by the app entry point
    PlugIns/*.appexShare, widget, notification service, intentsAnything imported by the extension target
    AppClips/*.appLightweight entry-point appWhatever the App Clip imports for its limited flow
    Watch/*.appCompanion watchOS binaryAnything reachable from the watch target
    Frameworks/*.frameworkFirst and third-party dynamic librariesSDK constants and embedded credentials
    main.jsbundle or main.hbcMinified or Hermes-compiled JavaScriptAll JS string literals reachable from the entry chunk
    Lazy Metro chunksCode split by dynamic importAnything referenced by the lazy chunk
    GoogleService-Info.plistFirebase configurationClient identifiers (usually not secrets, but verify)

    What to watch out for

    A few patterns pass App Review and look fine to a glance, but still drop secrets into one or more nested bundles:

    • Reusing a Swift package across the main app and an extension when the package embeds a constant for a third-party service. The compiler links the constant into both binaries, so two separate strings runs return the same key.
    • Calling Bundle.main.infoDictionary for a token stored in Info.plist inside the extension. The extension has its own Info.plist; the token ends up duplicated inside the extension bundle as well as the parent.
    • Relying on Hermes bytecode as if it hides strings. Bytecode is not encryption; hermes-dec restores the string table in seconds.
    • Treating Firebase API keys as universally safe to embed. The Firebase web API key is intentionally client-side, but a Firebase Admin SDK service-account JSON is a full credential that must never ship anywhere in the IPA.
    • Assuming string obfuscation solves the problem. As NSHipster's review of iOS secret management puts it, client secrecy is impossible once attackers can run software on their own devices. Obfuscation raises static-analysis cost without removing the underlying credential, and Frida-style runtime hooking captures the secret at the moment it is reassembled to make a network call.

    Key takeaways

    • An IPA is a forest of bundles, not a single file. Audit every nested .appex, App Clip, watch target, framework, and JS chunk before you ship.
    • Apple's automated review verifies signatures and Privacy Manifests across every bundle, but does not scan any of them for hardcoded credentials.
    • Minification preserves string literals in every bundle it processes. Hermes bytecode and JS chunks behave the same way.
    • The fix is structural: keep secrets server-side and call a thin backend proxy from any client target that needs them. A backend that issues short-lived tokens removes the leak surface entirely.
    • For teams who want an external automated read of every nested bundle in a release IPA before submission, PTKD.com (https://ptkd.com) is one platform focused on OWASP MASVS-aligned scanning of compiled mobile builds across the whole IPA tree.
    • #ios
    • #app-store
    • #hardcoded-secrets
    • #app-extensions
    • #react-native
    • #minification
    • #owasp-masvs

    Frequently asked questions

    Why are there multiple bundles inside one iOS app?
    A modern iOS app is built from one or more Xcode targets, and each target produces its own signed bundle inside the IPA. The main app is one target, but the share extension, widget, App Clip, and watchOS app are separate targets that compile to separate .appex or .app binaries. Apple stores them all inside the same IPA, signs each, and delivers them together at install time.
    Will App Review reject my app if one extension contains a leaked Stripe key?
    Almost never as a direct rejection reason. The ITMS validator runs Privacy Manifest, entitlement, and required-reason API checks across every nested bundle, but it does not scan any of them for hardcoded credentials. Apple has issued post-publication takedowns when leaked credentials caused user-data breaches reported by third parties, but those are enforcement actions, not pre-submission catches before the build ships.
    Does Hermes bytecode hide my secrets better than minified JavaScript?
    Not in any meaningful sense. Hermes produces a binary .hbc artifact, but the string table is still readable. Tools like hermes-dec decompile Hermes bytecode and recover function names, string literals, and embedded credentials. The change versus a minified JS bundle is that an attacker needs one more command to extract the strings, not that the strings stop being inside the artifact.
    Are all secrets in a JavaScript chunk also in the native main binary?
    No, and that nuance matters when you audit. Secrets imported only into JavaScript stay in the JS bundle; secrets compiled into a Swift framework stay in the Mach-O. The strings command run against main.jsbundle and the main binary will return different sets, which is why an audit needs to walk the entire IPA tree, not just one file.
    What is the difference between a static analysis scan and Apple's review?
    Apple's automated review confirms that your build matches its declared metadata, entitlements, Privacy Manifests, and required-reason API list. A static analysis scan, such as the one performed by MobSF or scanning platforms aligned with OWASP MASVS, opens every nested bundle inside your IPA and matches its contents against known credential patterns, insecure storage paths, and SDK vulnerabilities. The two checks have almost no overlap.

    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