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 IPA | What it usually contains | Secret-leak surface |
|---|---|---|
| Main app binary | Compiled Mach-O of your main target | Anything imported by the app entry point |
| PlugIns/*.appex | Share, widget, notification service, intents | Anything imported by the extension target |
| AppClips/*.app | Lightweight entry-point app | Whatever the App Clip imports for its limited flow |
| Watch/*.app | Companion watchOS binary | Anything reachable from the watch target |
| Frameworks/*.framework | First and third-party dynamic libraries | SDK constants and embedded credentials |
| main.jsbundle or main.hbc | Minified or Hermes-compiled JavaScript | All JS string literals reachable from the entry chunk |
| Lazy Metro chunks | Code split by dynamic import | Anything referenced by the lazy chunk |
| GoogleService-Info.plist | Firebase configuration | Client 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.infoDictionaryfor 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.




