0006. Root/jailbreak detection approach
Date: 2026-07-23
Author: Eugene Nagorny
Stakeholders: Mobile team, Security (pen-test remediation), QA
Status: Accepted
Context
The Bulletproof penetration test (PT81969-6974, finding R3837, severity Low) reported that the Engineer app installs and runs on rooted Android and jailbroken iOS devices with no detection. The report frames root/jailbreak detection explicitly as defence-in-depth — "not very effective by itself" — and offers two acceptable responses depending on app risk profile: block outright (financial-grade apps) or warn and let the user proceed. APY-2186 tracks the remediation; the warn-vs-block choice was left to refinement and never recorded.
Three implementation options were evaluated (and two of them actually built) on the APY-2186 branch:
expo-device'sisRootedExperimentalAsync()— already a dependency, zero native change.- jail-monkey — a community RN library of root/jailbreak heuristics.
- Talsec freeRASP — a native RASP SDK with root/jailbreak, hooking, tampering, and emulator detection.
How they compare:
| expo-device | jail-monkey | freeRASP | |
|---|---|---|---|
| Detection method | 3 Android checks (test-keys build tags, Superuser.apk, /system/xbin/su), 4 iOS checks (cydia:// scheme, suspicious paths, sandbox-escape write) | Same family, broader path list plus which su exec | Compiled native SDK correlating many signals: su/Magisk artifacts, SELinux state, system properties, root-hider traces (Magisk DenyList, Shamiko, Dopamine) |
| False positives | Low but unowned — test-keys fires on some budget OEM firmware; cydia:// scheme has historic FPs from App Store apps registering it | Same profile as expo-device | Lowest on stock devices; commercial customers block on this signal so OEM FPs get fixed via Talsec's telemetry. Flags devices with root traces (leftover artifacts, custom ROMs) — correct per policy but disputable by users |
| Bypass resistance | None — Magisk DenyList / jailbreak hiders defeat file-path checks trivially | Effectively none | Detects the hiders themselves; the only option likely to still fire at a pen-test retest |
| Integration cost | Zero — already installed | New JS dependency | New native SDK via config plugin; certificate-hash upkeep (upload key vs Play app-signing key); Talsec telemetry (device-level, no PII) needs a GDPR nod; watcherMail ownership |
| Maintenance | Expo-owned, marked experimental (deliberately un-guaranteed) | Sporadic community updates | Actively updated against Magisk/Dopamine releases |
freeRASP was fully implemented on the branch (commit 943990a5b: config plugin, privilegedAccess-only subscription, upload-key certificate hash extracted from the production AAB) and then reverted (d5466b8eb) after team review.
Decision
Use expo-device's isRootedExperimentalAsync(), wired as useCompromisedDeviceCheck in src/utils/device/, called once from the root layout on launch.
- Warn, don't block. On detection the engineer gets a dismissible alert; the app continues. The finding is Low, the report itself says detection is bypassable, and the Engineer app is not financial-grade — a false positive that blocked would lock an engineer out mid-job, which costs more than the marginal deterrence of blocking. Flipping to block is a one-line change if policy changes.
- Observability on every detection. PostHog event
compromised_device_detectedmeasures the real-world firing rate, so the policy can be revisited from data within a release or two;$os_name/$os_versioncome from the SDK anddevice_modelnarrows down OEM firmware that trips the test-keys heuristic. The Sentry tagcompromised_devicesends nothing on its own — it decorates the scope so a later crash on that device is identifiable. - Skip on emulators/simulators (
Device.isDeviceguard). Emulator images run test-keys builds that read as rooted; without the guard every local dev session would false-positive and E2E runs would wedge behind a native alert.
The proportionality argument decided it: for a warn-only response to a Low, defence-in-depth finding, a native RASP SDK buys detection strength the response doesn't use, at the cost of third-party telemetry, certificate-hash upkeep, and a standing dependency on Talsec. The heuristic satisfies the finding's letter ("check on execution, alert the user") with zero new dependencies.
Alternatives Considered
jail-monkey — rejected: adds a dependency and a native change to get essentially the same detection quality and false-positive profile as the API we already ship, with less predictable maintenance than Expo's.
freeRASP (implemented, then reverted) — rejected for now as disproportionate, not as wrong. It is the only option that survives root hiders and the likely pen-test retest methodology, and the reverted commit is a working reference: config plugin setup, threat subscription, app identifiers, Apple team ID, and the upload-key certificate hash are all in commit
943990a5b, listed on PR #4773. Read it from the PR rather than from a local checkout — the branch is deleted on merge, and a squashed merge leaves the commit reachable only through GitHub. Revisit triggers:- a pen-test retest hides root (e.g. Magisk DenyList) and the heuristic misses it, failing the finding;
- the response policy hardens from warn to block (blocking on a heuristic is not defensible; blocking on freeRASP is);
- we decide to act on adjacent MASVS-RESILIENCE findings (hooking/Frida, tampering, repackaging) that freeRASP covers in the same integration.
Before restoring it: obtain the Play app-signing certificate SHA-256 (Play Console → App integrity) — the hash in branch history is the EAS upload key, sufficient for direct-install builds but not for Play-distributed ones — and agree a team-owned
watcherMailaddress plus a GDPR position on Talsec telemetry.Blocking on detection — rejected: false positives (budget OEM test-keys firmware) would lock engineers out of their work tool with no recourse, and the underlying detection is trivially bypassed anyway, so blocking punishes honest users more than attackers.
Consequences
- The finding's remediation ("check on execution, alert the user") is satisfied with no new dependencies, no native change, and no third-party data flow.
- Detection will not survive a retest that actively hides root. That is a conscious, documented risk accepted for a Low finding — the fallback is restoring freeRASP from branch history, not redesigning.
- The
compromised_device_detectedevent gives an empirical baseline (expected well under Talsec's ~0.5–1% population figure for UK trade engineers on stock hardware) before any future hardening decision. - Detection can only be verified on a real rooted/jailbroken device (AC4) — emulators are deliberately excluded by the
Device.isDeviceguard, so there is no way to exercise the alert on a simulator or in E2E.
Relevant Jira Tickets
- APY-2186 — Add root/jailbreak detection to the Engineer app (this decision; PR #4773)
- DEP-102 — [Pen Test Eng App] Root/Jailbroken Detection (source finding R3837)
- Sibling pen-test hardening under the APY-832 tech-debt epic: APY-2185 (Android manifest), APY-2187 (TLS pinning, parked), APY-2188 (iOS keychain), APY-2189 (R8 obfuscation), APY-2190 (app-switcher privacy, done)