Can an attacker read a React Native release app's JavaScript when it ships as Hermes bytecode inside the APK or IPA?
answer
- APK and IPA are zip files
- assets/index.android.bundle
- main.jsbundle in the .app folder
- hermesc output keeps the bundle's name
- string table, disassemblers, decompilers
basics
~10 sYes. The bundle is a plain file in the package — assets/index.android.bundle on Android, main.jsbundle on iOS — and Hermes bytecode keeps strings readable and can be disassembled or decompiled back into readable logic.
solid answer
~40 sAn APK and an IPA are both zip archives. On Android, React Native's Gradle plugin writes the bundle as `assets/index.android.bundle`; on iOS, the build script writes `main.jsbundle` into the `.app` folder inside the IPA's `Payload` directory. With Hermes, the default engine, both files hold **Hermes bytecode**: `hermesc` compiles the Metro bundle at build time and the build moves the bytecode back over the original file name. Bytecode is not encryption. String literals and property names sit in a string table that a plain `strings` dump prints, and open-source disassemblers and decompilers rebuild readable control flow. Hermes precompiles for faster startup and lower memory use, not for secrecy, so treat everything in the bundle as public.
go deeper
Remember that the bundle is a file inside the app package and that anything in it, including strings, can be read by someone who downloads the app.
Name the default paths, index.android.bundle and main.jsbundle, and explain that hermesc turns the bundle into bytecode for startup speed while strings stay readable.
Draw the consequence: client checks can be patched and embedded values read, so entitlement, pricing and secret-bearing calls must be enforced on the server.
Weigh how much to invest in client hardening against moving trust to the server, knowing that bytecode, obfuscation and store encryption only change an attacker's cost.
## Where the bundle sits in the package A React Native release build ships its JavaScript as one **bundle** file next to the native code. Both package formats are ordinary zip archives, so getting at that file needs no special tooling. | Platform | Package | Default bundle path | Written by | |---|---|---|---| | Android | `.apk` (or an APK generated from an `.aab`) | `assets/index.android.bundle` | the React Native Gradle plugin (`bundleAssetName` defaults to `index.android.bundle`) | | iOS | `.ipa` | `Payload/<App>.app/main.jsbundle` | `react-native-xcode.sh` during the Xcode build phase | On Android, an APK is easy to obtain: pull it from a device you own, or download it from a mirror. An IPA takes more effort, typically a jailbroken device, but more effort is not protection: Apple's store encryption targets the native executable, and the bundle is a resource file beside it. ## How extraction works in practice 1. **Get the package.** Pull the installed APK from a device over `adb`, or download it; for an app published as an Android App Bundle, the store delivers split APKs and the bundle asset sits in the base one. 2. **Unzip it.** Any archive tool opens an APK or IPA. 3. **Find the bundle** at the default path, or simply the largest file in `assets/` or the `.app` folder. 4. **Read it.** A `strings` dump shows the literals in seconds; a Hermes disassembler or decompiler shows the logic. None of these steps needs a rooted phone, a paid tool or knowledge of your source code. ## What Hermes changes about the file Since React Native 0.84, **Hermes V1** is the default engine on both platforms, and the built-in JavaScriptCore was removed in 0.81. In a release build with Hermes the pipeline is: 1. **Metro** bundles all JavaScript modules into one file. 2. **`hermesc`** compiles that file with `-emit-binary` (and `-O` by default on Android) into Hermes bytecode. 3. The build **moves the bytecode over the original name**, so `index.android.bundle` and `main.jsbundle` keep their names but now contain bytecode, not text. React Native's build scripts also skip Metro's own minification when Hermes is on — the iOS script's comment reads "Hermes doesn't require JS minification" — because the compiler optimizes the bytecode itself. The point of precompiling is **startup speed and memory**: the engine loads ready-made bytecode instead of parsing JavaScript on the device. Nothing in that goal involves hiding code. ## What an attacker can recover - **Strings.** Every string literal — URLs, API keys, error messages, feature-flag names, object property names — lives in the bytecode's string table. Running `strings` over the file surfaces most of them without any Hermes knowledge. - **Structure.** Hermes is open source, so its bytecode format is public, and community disassemblers and decompilers turn it back into function-by-function pseudo-JavaScript. - **Behaviour.** From the decompiled output an attacker can find a paywall check, a hidden debug menu or the endpoint a feature calls, and patch or replay it. - **Not recovered:** comments and original formatting. The output is harder to read than your source, which slows a casual reader and nobody else. A new bytecode version can temporarily break community tools after an engine upgrade. That is a delay, not a defence. ## What does not help - **Obscure file names.** The bundle name is configurable, but the file is still in the package and easy to find by size. - **App-store distribution.** The store's executable encryption does not cover resource files. - **Relying on the engine.** A project that swaps in the community JavaScriptCore package ships the Metro bundle as JavaScript text — even easier to read. ## What this means for your design - Treat everything in the bundle as **public**: code, strings and embedded configuration. - Keep **secrets and authorization decisions** on the server; a client-side check is a user-experience feature, not an access control. - Assume a motivated attacker can **patch** the JavaScript logic, which is why server-side verification matters for anything valuable. - Source maps turn bytecode offsets back into your original files and names, so they belong in your crash tooling, never inside the package. In an interview, the strong answer is a confident "yes, and here is exactly where the file is and why bytecode does not hide it", followed by what you keep off the device as a result.
- If a React Native app runs a paywall check in JavaScript, what can an attacker do with the extracted bundle?Read the decompiled check to learn which flag or response unlocks the content, then patch the bundle and re-sign a modified package, or replay the unlocking request. A client-side check only shapes the user interface. Entitlements must be verified by the server, which returns premium content only to accounts that paid.
- Why does a React Native release build with Hermes skip Metro's minification step?Hermes compiles the bundle to bytecode, and `hermesc` performs its own optimizations, so React Native's build scripts pass `--minify false` to the bundler for Hermes release builds. The result is that shortening identifiers with Terser, which never hid strings anyway, is not even part of a default Hermes build.
saying these in an interview costs you the question
- Hermes bytecode is compiled machine code, so it cannot be read back.
- The JavaScript bundle is encrypted inside a signed APK or IPA.
- Only a jailbroken iPhone could ever expose an iOS app's bundle.
- Hermes exists to protect source code from reverse engineering.
- Renaming the bundle file keeps attackers from finding it.