Skip to content
5 min read
Hardening React Native Apps Beyond JailMonkey

Neon Skies and Chrome Dreams

Most React Native apps that “do security” start the same way: install a library like jail-monkey, read some booleans, block the user if the device looks risky.

It feels like protection. In practice it is glorified logging. A determined attacker with Frida, Magisk, or a patched APK can flip those booleans to whatever values they want. If your business logic trusts them, you lose.

This writing treats the client as hostile and explains why root and hook detection are weak controls in that environment.

Start with the threat model

React Native runs inside a process fully controlled by the user. A real attacker can:

attacking Flow Diagram

Flow diagram showing how attacker controll app

If you do not assume that, you are not doing security work, you are doing theater.

From that point, a few things should be non-negotiable:

  • Any flag such as isRooted, isHooked, isDebugged is just data inside an untrusted process
  • Any conditional in JavaScript that gates money or privileges can be bypassed or rewritten
  • The real assets are on the backend: accounts, payments, promos, privileged actions, APIs

Why client-side checks are not authoritative

Typical pattern: install jail-monkey, collect a risk object, branch on it.

import JailMonkey from "jail-monkey";

export function getDeviceRiskSignal() {
  return {
    isJailBroken: JailMonkey.isJailBroken(),
    isDebuggedMode: JailMonkey.isDebuggedMode(),
    canMockLocation: JailMonkey.canMockLocation(),
    isOnExternalStorage: JailMonkey.isOnExternalStorage(),
    hookDetected: JailMonkey.hookDetected(),
  };
}

Somewhere else:

const risk = getDeviceRiskSignal();

if (risk.isJailBroken || risk.hookDetected || risk.isDebuggedMode) {
  // Block login, payment, or show warning
}

On a normal device this “works.” On a hostile device this is just an API the attacker controls.

A simple Frida hook on Android:

Java.perform(function () {
  const JailMonkey = Java.use("com.gantix.JailMonkey.JailMonkeyModule");

  JailMonkey.getConstants.implementation = function () {
    const original = this.getConstants();
    original.put("isJailBroken", false);
    original.put("isDebuggedMode", false);
    original.put("canMockLocation", false);
    original.put("isOnExternalStorage", false);
    original.put("hookDetected", false);
    return original;
  };
});

For more details example of Frida bypassing all checks, see this script

Run this in app and every call to JailMonkey lies in favor of the attacker. The app believes the device is clean while it is fully instrumented.

Anything computed purely on the client device is untrusted input. It can be useful as a signal, but never as the final authority for high-value actions.

Move enforcement to the backend

If the client cannot be trusted, enforcement moves to the backend. Client checks stop being judges and become hints.

For high-risk flows, the backend should answer three questions before it approves anything important:

  1. What device and app instance is this request claiming to come from
  2. Is the network channel reasonably protected against interception and tampering
  3. Has this specific request already been used before

Those map to three broad areas:

  1. Device and app integrity signals that are hard to forge
  2. Strong transport configuration
  3. Request design that resists replay

The details differ by platform and stack, but the direction is always the same. Server decides based on server-verified evidence, not on client-reported booleans.

Red team sanity check

A quick self-test is more honest than any slide deck.

Take your production build and:

  1. Install it on a rooted or jailbroken device or emulator
  2. Attach Frida and force all your “risk” flags to safe values
  3. Try to complete login, money flows, and voucher flows

If everything still works as usual, then your backend is not enforcing anything important beyond “client says it is safe.”

Next, simulate a basic MITM setup with a proxy tool such as Burp Suite:

  1. Install a custom root certificate on the device
  2. Proxy traffic and see if you can read and edit HTTPS requests from your app

If traffic is fully visible and modifiable, transport security is weak and interception is trivial.

Finally:

  1. Capture one successful privileged request
  2. Replay it without going through the normal UI

If the backend accepts it again, you have no effective replay protection. Anyone who can capture one valid request can repeat it.

Conclusion

This writing argues for stop trusting the client and stop letting it decide what is safe. Root checks, hook checks, and similar libraries can stay, but only as noisy sensors feeding the backend, not as gates around money or privileges.

The real work lives server-side: verify device and app integrity with attestation, protect transport with TLS pinning, and design sensitive flows around nonce-based replay protection so that a captured request cannot simply be reused.

No system is perfectly secure, but this stack of controls makes attacks noisier, more complex, and far more expensive than flipping a few booleans in a hooked client.