Email security@exodus.com. We will acknowledge within 2 business days. Do not file a public GitHub issue for security reports.
This library exists to load and execute a remote JavaScript bundle inside the host React Native app. That is, by construction, a remote code execution primitive with the host app's full bridge surface. Two consequences:
- The library has no business in a store-distributed binary unless protected by a build-flavor flag that is statically stripped (not just runtime-gated). A leaked release build with this library reachable is equivalent to a leaked signing key.
- Any defense the library offers is layered on top of that fundamental risk. It does not eliminate it.
| Surface | Upstream 0.1.0 |
This fork |
|---|---|---|
| Bundle integrity | None — bridge fetches whatever the URL serves | loadVerified(url, sha256) downloads bytes natively (iOS: NSURLSession, Android: HttpURLConnection), hashes with platform crypto (iOS: CommonCrypto CC_SHA256, Android: MessageDigest SHA-256), compares in constant-time, writes to app-private storage, and reloads the bridge from the local file — closing the TOCTOU window between fetch and load |
BundlePrompt component |
URL-typing UI wired to the unverified load() |
Removed entirely with the unverified path |
| Scheme enforcement | None — accepts http://, file://, etc. |
https:// required at the JS boundary; the native loadVerified implementation re-checks before touching the network |
Unverified load() path |
load(url) fetches and reloads any URL, no integrity |
Removed — only loadVerified(url, sha256) remains; the unverified native methods, JS export, and BundlePrompt UI are gone |
| Verified bundle on-disk protection (iOS) | n/a | Written with NSDataWritingFileProtectionComplete |
| Lockfile | Not shipped | yarn.lock committed; .yarnrc enforces --frozen-lockfile |
| Dependency version pinning | Carets (^) |
All direct deps pinned to exact versions; .npmrc save-exact=true |
Maven + on react-native |
Yes | Configurable via rootProject.ext.reactNativeVersion; default + is the only fallback |
Maven jcenter() |
In repo list | Removed (sunset 2021) |
| Husky 4 auto-postinstall | Yes | Husky removed entirely |
release-it publishing |
Yes | Removed; releases are manual npm publish after yarn preflight passes |
| Eclipse / Xcode user-state in repo | Committed | Removed and gitignored |
| Source maps shipped to npm | Yes (lib/**/*.map) |
Excluded via files array; tarball denied by verify-pack if any reappear |
| Tarball contents | Anything matching files blanket globs |
Strict .npm-tarball-allowlist exact-match enforced by yarn verify-pack |
example/public/ios.min.js (700kB blob) |
Committed; served from jsdelivr to any BundlePrompt |
Removed along with the rest of example/ |
| CircleCI / Node 10 build container | .circleci/config.yml shipped |
Removed |
- Removed the unverified
load()path — theload(url)native methods (iOS + Android), the JSloadexport, and theBundlePromptUI. Loading a remote bundle without native SHA-256 verification is an unauthenticated RCE primitive; onlyloadVerifiedremains. - Android verifies before install. The download streams to a per-call unique temp file (
File.createTempFile); the verified bytes are atomically promoted (same-directory rename) to the canonical path only after the hash matches, and the temp is deleted on mismatch or download error — the canonical path never holds unverified or partial content. Using a unique temp per call rather than a shared fixed path means overlappingloadVerifiedFromUrlcalls can never swap each other's file between verify and rename (so a call always promotes exactly the bytes it verified). - iOS bundle-size cap (64 MB), matching Android's, rejects oversized responses before they are hashed, written, or loaded.
- The verified bundle is handed to the host app to load, not installed via a private RN API. iOS writes the verified file URL to
NSUserDefaults(RNBundleLoaderPendingURLKey), which the host app'sloadSourceForBridge:override reads on reload; Android sets a one-shotSharedPreferencesflag and restarts the process so the host app'sgetJSBundleFile()serves the file. The library depends on the host app implementing that read side (see the integration notes); if the host omits it the swap silently no-ops rather than loading unverified code. The library deliberately does not reach into non-public RN internals to force the swap. - Session scoping is host-driven. The remote bundle is active for one session; the host app clears the pending URL / active flag on cold start. The library cannot do this itself because it is not in the app's cold-start entry point (it only runs once RN is up).
- Hash verification runs in native code, not JS.
loadVerifiedFromUrlusesCommonCrypto CC_SHA256(iOS) andMessageDigest SHA-256(Android) with a constant-time XOR comparison loop in native code. This avoids a HermesRangeError: Maximum regex stack depth reachedthat the previous JS-sideresponse.arrayBuffer()path hit on bundles ≥ ~70 MB. The trade-off is that the integrity contract is no longer auditable as TypeScript. timingSafeEqualis an inlined XOR loop in native code. Bothios/BundleLoader.mandandroid/src/main/java/com/reactnativebundleloader/BundleLoaderModule.javaXOR all byte pairs into an accumulator and reject the bundle if the accumulator is non-zero.
This package has no CI/CD. Maintainers cut releases manually from a developer machine:
yarn preflight # lint + typecheck + JS tests + Android JVM tests + iOS XCTests + verify-pack
npm publish --access publicyarn verify-pack runs npm pack and diffs the resulting tarball file list against .npm-tarball-allowlist. Any deviation (extra files, missing files, renames) fails the script. Adding a new shippable file to the package requires updating .npm-tarball-allowlist in the same commit so the change is reviewable.
The npm publish credential lives on the publishing maintainer's machine. There is no shared CI token, and no GitHub Actions workflow holds publish rights. The trust boundary is whoever has npm login access to @exodus.
- The host app's CI pipeline that mints the deep links carrying
(url, sha256)pairs is not covered by this library. Threats #1, #5, #6, #7, #8 in the originating threat model are the consuming app's responsibility. - Bundle code review and signing are out of scope. This library verifies what was hashed by your CI matches what the device fetched. It cannot tell you that the bundle does not contain malicious JS.