Skip to content
Featured Case Study

Mobile Application Penetration Testing Securing a digital-health flagship (iOS & Android) against insecure PHI storage, SSL-pinning bypass MITM, and hardcoded API keys ahead of a high-profile launch

AI image prompt — Ultra-realistic, eye-level photograph of a bright, minimalist modern product studio during the day. On a clean light-oak desk sit a flagship iPhone and an Android device side by side, both displaying a polished digital-health app UI with elegant cyan (#00e5ff) accent lines and a security audit overlay. Bright natural daylight from large windows, soft shadows, green potted plants, shot on Hasselblad, high-end professional commercial branding

Project Details

Client
Vitalis Health is a fast-growing digital-health startup whose flagship iOS and Android app, VitalisCare, delivers remote patient monitoring, secure clinician messaging, and lab-result delivery for over 240,000 patients across a network of partner clinics
Industry
Digital Health / HealthTech
Company Size
80 - 120
Headquarters
Boston, Massachusetts
Project Duration
1 month (Apr 2026 - May 2026)

A comprehensive grey-box mobile application penetration test of a digital-health flagship app (VitalisCare, iOS & Android) handling protected health information. The engagement combined static analysis, dynamic instrumentation, and network interception to prove insecure local storage of auth tokens, an SSL-pinning bypass enabling full MITM of PHI traffic, and a hardcoded API key recovered via decompilation — then delivered Keychain/Keystore migration, hardened certificate pinning, and secrets management to achieve HIPAA-aligned launch readiness.

Engagement Classification · TLP:AMBER

Project VitalShield / Mobile PHI Audit

Grey-box mobile penetration test of a digital-health flagship across iOS and Android. Six weeks: decompilation, runtime instrumentation, live API interception, and a remediation cycle that closed three PHI-exposure paths before the release shipped.

Secured

A Launch-Blocking Risk in Digital Health

Four commands. That is what stood between an attacker holding a VitalisCare device and a patient’s lab results: adb shell run-as, cat, a copy of the JWT, and a curl against the production API. No exploit chain, no memory corruption, no zero-day. A file that should have been in the Keystore was sitting in an XML file instead.

That is the structural problem with mobile, and it is not a coding-standards problem. A server binary lives somewhere you control. A mobile binary ships to the adversary — it can be decompiled at leisure, hooked at runtime, run on a device whose OS the attacker has already rooted, and pushed through a proxy on a network they operate. Every client-side control in the app is a control the attacker gets to study offline, for as long as they like, with the source in front of them.

Vitalis Health engaged us six weeks before a major VitalisCare release, with payer partners and clinician onboarding already scheduled against the date. The question they asked was the right one and unusually specific: could someone reverse-engineer the app, pull patient data out of local storage, or read PHI in transit? On the pre-release build the answer was yes to all three, and none of them were hard. This case study walks the three that mattered — token storage, pinning bypass, and a hardcoded key — through proof, fix, and re-test. The fixes were not all free, and where they cost something we say so.


Technical Audit Snapshot

Platforms Assessed
iOS + Android
Universal & native builds
API Endpoints Audited
38
REST + GraphQL
Vulnerabilities Found
11
CVSS v3.1 4.0 – 9.1
PHI Exposure Vectors
3
Closed pre-launch

4-Phase Mobile Assessment Methodology

We aligned the engagement to the OWASP Mobile Application Security Verification Standard (MASVS) and worked across four layers: the binary, the runtime, the network, and the backend the app talks to. The order matters. Static analysis tells you what the code intends; dynamic analysis tells you what it actually does with a real session and real data; the network layer tells you what leaves the device; the API tells you whether any of the client-side controls were ever load-bearing in the first place. Skipping straight to the proxy — the common shortcut when time is short — finds insecure communication and misses insecure storage entirely, because storage never crosses the wire.

01

Static Analysis (SAST & Reverse Engineering)

Decompiled the Android APK (jadx, apktool) and inspected the iOS IPA (class-dump, Hopper). Read the manifest for exported components, swept embedded strings for credential-shaped material, and audited third-party SDKs — which is where insecure defaults usually live, because nobody reviews a dependency the way they review their own code.

02

Dynamic Analysis (Runtime Instrumentation)

Ran the app on rooted Android and jailbroken iOS hardware, hooking methods with Frida and Objection to watch what it wrote to disk, what it put in the keychain, which crypto calls it made with which parameters, and how it behaved when those calls were tampered with mid-flight.

03

Network Interception (MITM)

Routed device traffic through an intercepting proxy (Burp/mitmproxy), defeated certificate pinning at runtime, then read every request and response for PHI in URLs, over-broad response bodies, and tokens with longer lives than the sessions they belonged to.

04

API Security Auditing & Remediation

Tested the backend for object-level authorisation flaws and PHI leakage with a legitimately obtained token, then co-authored the Keychain/Keystore migration, the hardened pinning configuration, and the secrets-management change — re-testing each fix on a fresh build rather than accepting a diff as evidence.

Interactive Methodology Checklist

Tap each control to mark it complete. This is the MASVS-aligned checklist we worked through on VitalisCare — six items, deliberately short. A mobile checklist that runs to eighty rows gets skimmed, and the rows people skim are the storage ones, because they are the least interesting to test and the most likely to be broken.


Mobile Architecture & MITM Attack Path

VitalisCare is a native iOS/Android client talking to a REST + GraphQL backend behind an API gateway. Transport security rested on TLS with certificate pinning, and pinning was doing more work than the team realised: because the client was trusted to validate the certificate, the backend accepted any bearer token that reached it, from anywhere. That is the assumption we set out to break. Defeating pinning was not the finding in itself — pinning is bypassable by design on a device you control. The finding was what became visible once it was gone, and what the API did not do about it.

%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '12px', 'primaryColor': '#06222b', 'primaryTextColor': '#e0fbff', 'primaryBorderColor': '#00e5ff', 'lineColor': '#00e5ff', 'secondaryColor': '#02080a', 'tertiaryColor': '#0a1417', 'background': 'transparent', 'clusterBkg': '#0a1417', 'clusterBorder': '#0e3a44', 'edgeLabelBackground': '#06222b', 'titleColor': '#67e8f9', 'nodeTextColor': '#e0fbff'}}}%% graph TD classDef attacker fill:#1c0d0d,stroke:#ef4444,stroke-width:2px,color:#fecdd3; classDef device fill:#06222b,stroke:#00e5ff,stroke-width:2px,color:#e0fbff; classDef proxy fill:#06222b,stroke:#00e5ff,stroke-width:3px,color:#e0fbff; classDef backend fill:#0c111d,stroke:#3b82f6,stroke-width:2px,color:#dbeaf8; App([VitalisCare App]):::device Frida([Frida Hook]):::attacker Frida -.bypass pinning.-> App App --> Proxy{Intercepting Proxy}:::proxy Proxy --> GW[API Gateway]:::backend GW --> DB[(PHI Datastore)]:::backend

Vulnerability Classification Matrix · OWASP Mobile Top 10 (2024)

Each finding was scored with CVSS v3.1 and mapped to the OWASP Mobile Top 10 (2024) and the relevant MASVS control group. The Exploit Complexity column exists because CVSS on its own misleads on mobile: OC-MOB-001 and OC-MOB-005 both need physical or root access to the device, which the CVSS vector already reflects, but the effort involved differs by orders of magnitude once the device is in hand. A developer reading a 9.1 next to a 5.3 needs to know which one is a one-line cat and which one requires a forensic image.

IDVulnerability / AssetPlatformCVSS v3.1Mobile Top 10Exploit ComplexityRemediation Status
OC-MOB-001Insecure Local Storage of Auth Tokens (PHI)iOS + Android9.1 (Critical)M9: Insecure Data StorageLow (Filesystem read)REMEDIATED
OC-MOB-002SSL Pinning Bypass → Full MITM of API TrafficiOS + Android8.6 (High)M5: Insecure CommunicationMedium (Runtime hook)REMEDIATED
OC-MOB-003Hardcoded API Key via DecompilationAndroid8.2 (High)M1: Improper Credential UsageLow (Static strings)REMEDIATED
OC-MOB-004Exported Android Activity Leaks SessionAndroid6.8 (Medium)M8: Security MisconfigurationMedium (Malicious app)REMEDIATED
OC-MOB-005Sensitive PHI Cached in App SnapshotsiOS5.3 (Medium)M6: Inadequate Privacy ControlsLow (Physical access)REMEDIATED
Advertisement

Static vs Dynamic Analysis

Neither technique is sufficient alone, and the reasons are asymmetric. Static analysis finds the code path but cannot tell you whether it runs, so it produces findings in dead code and abandoned SDK integrations that waste a developer’s afternoon. Dynamic analysis proves the behaviour but only on the paths you exercised — anything behind a feature flag, a specific locale, or an error branch you never triggered is invisible, and its absence looks exactly like safety. Toggle between the two workflows on the token-storage finding below.

analysis-workbench
jadx + apktool · reading the binary
# Decompile the APK and grep for token persistence
apktool d vitaliscare-release.apk -o out/
jadx -d jadx_out vitaliscare-release.apk

$ grep -rni "getSharedPreferences\|putString" jadx_out/sources/ | head
TokenStore.java: prefs = ctx.getSharedPreferences("vc_auth", MODE_PRIVATE);
TokenStore.java: prefs.edit().putString("jwt", token).apply();   # plaintext!

# The JWT is written to an unencrypted XML file on disk:
#   /data/data/com.vitalis.care/shared_prefs/vc_auth.xml

Static review located the exact class and preference key persisting the auth JWT into plaintext SharedPreferences. Note what it could not tell us: whether that code path was still live in the release build, or whether a later refactor had superseded it. That took the runtime hook.

frida + adb · watching it run
# Pull the live secrets file straight off a rooted device
$ adb shell run-as com.vitalis.care cat shared_prefs/vc_auth.xml
<string name="jwt">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...</string>

# Confirm at runtime with a Frida hook on the setter
frida -U -f com.vitalis.care -l hook_tokenstore.js
[TokenStore.putString] key=jwt value=eyJhbGciOiJ...  (PHI scope: read:labs)

The runtime hook closed it: the JWT was readable from disk on a live session and observable as it was written, carrying a read:labs scope. That is a working session in an attacker’s hands, and nothing server-side would have distinguished its replay from the patient’s own app.


Critical Finding OC-MOB-001 — Insecure Local Storage of Auth Tokens

VitalisCare persisted its session JWT — carrying PHI-read scopes — into unencrypted SharedPreferences on Android and UserDefaults on iOS. Both are the platform default for “somewhere to put a small value”, both are backed up, and neither has ever claimed to be secure storage. The token could be lifted off disk and replayed against the API from any machine on the internet.

The device-is-rooted caveat undersells this. Root or a jailbreak is one route; an unencrypted iTunes backup, a shared or resold handset, an MDM-managed device with a compromised admin, and any malware with the filesystem access that a sideloaded app on an older Android can still obtain are all others. Healthcare skews towards older devices, because patients on remote monitoring skew older and poorer, which is precisely the population least likely to be on a current OS with full-disk encryption behind a strong passcode. The threat model is not “a security researcher with a Pixel”. It is a second-hand phone sold with the account still on it.

Worse, the token had a 30-day life and no device binding. Once copied, it kept working for a month, from anywhere, and revocation depended on the patient noticing something they had no way of seeing.

Attack Path Sequence

← Swipe horizontally to view full sequence flow →

%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px', 'primaryColor': '#06222b', 'primaryTextColor': '#e0fbff', 'primaryBorderColor': '#00e5ff', 'lineColor': '#00e5ff', 'secondaryColor': '#02080a', 'tertiaryColor': '#0a1417', 'background': 'transparent', 'clusterBkg': '#0a1417', 'clusterBorder': '#0e3a44', 'edgeLabelBackground': '#06222b', 'titleColor': '#67e8f9', 'nodeTextColor': '#e0fbff'}}}%% sequenceDiagram autonumber participant Attacker as Attacker (Device) participant FS as App Sandbox (Disk) participant API as VitalisCare API participant PHI as PHI Records Attacker->>FS: run-as / read shared_prefs + UserDefaults FS-->>Attacker: Plaintext JWT (scope: read:labs) Attacker->>API: GET /v1/patients/me/labs (Bearer stolen JWT) API-->>Attacker: 200 OK API->>PHI: fetch records PHI-->>Attacker: Patient lab results (PHI exposed)

The Remediation Block (Before vs After)

secure-token-storage.swift
VULNERABLE (iOS)
// JWT stored in UserDefaults — unencrypted plist
func saveToken(_ token: String) {
UserDefaults.standard.set(
  token, forKey: "vc_jwt"
)
}

func loadToken() -> String? {
UserDefaults.standard
  .string(forKey: "vc_jwt")
}
SECURED (Keychain)
// Store in Keychain, hardware-backed where available
import Security

func saveToken(_ token: String) {
let data = Data(token.utf8)
let query: [String: Any] = [
  kSecClass as String: kSecClassGenericPassword,
  kSecAttrAccount as String: "vc_jwt",
  kSecValueData as String: data,
  kSecAttrAccessible as String:
    kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}

Two details in the Swift above carry most of the value. kSecAttrAccessibleWhenUnlockedThisDeviceOnly keeps the item out of backups and stops it migrating to a restored or cloned device — the plain WhenUnlocked variant does neither, and it is the one most sample code uses. The SecItemDelete before SecItemAdd is there because SecItemAdd returns errSecDuplicateItem on an existing account rather than overwriting; skip it and token refresh silently stops updating, leaving the app authenticating with a stale credential until the user reinstalls.

On Android we moved from raw SharedPreferences to EncryptedSharedPreferences with a Keystore-backed master key, so the token is encrypted at rest under a key that cannot be exported from the device.

The trade-off is real and worth stating. Keystore operations are slower than a plain preference read, which shows up on cold start where the app reads the token before its first API call — a few tens of milliseconds, invisible to most users and noticeable on low-end hardware. More importantly, Keystore keys are invalidated by events the app does not control: a lock-screen credential change, a biometric enrolment, and on some devices a system update. When that happens, decryption throws and the app must handle it by discarding the store and re-authenticating. Handle it badly and you ship a crash loop on exactly the users who just changed their PIN. The Jetpack Security library that provides EncryptedSharedPreferences has also had a long, slow release cadence; check its current status before committing, and treat the dependency as something to plan an exit from rather than a reason to keep plaintext.

// Android — EncryptedSharedPreferences backed by the Android Keystore
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val securePrefs = EncryptedSharedPreferences.create(
    context,
    "vc_auth_secure",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("jwt", token).apply()

Critical Finding OC-MOB-002 — SSL Pinning Bypass Enabling MITM

VitalisCare pinned certificates, and the team was rightly proud of it — most apps at their stage do not. The problem is what pinning is for. It defends the user against a hostile network: a coffee-shop access point, a compromised router, an enterprise TLS-inspection appliance the patient never consented to. It has never defended the app against the person holding the device, because that person can change the code that performs the check.

The implementation was OkHttp’s CertificatePinner on Android and a URLSessionDelegate trust callback on iOS. Both live in application code, and application code on a rooted device is editable at runtime. A twelve-line Frida script replaced the check on both platforms; total time from clean device to cleartext PHI in the proxy was under ten minutes, most of it spent provisioning the device.

So the interesting result is not “pinning was bypassed”. It is that once bypassed, nothing else objected. The captured bearer token replayed cleanly from a laptop with no device identifier, no client certificate, and no anomaly signal — the API had delegated its entire trust decision to a control that only ever worked against a different attacker.

MITM Attack Vector

%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px', 'primaryColor': '#06222b', 'primaryTextColor': '#e0fbff', 'primaryBorderColor': '#00e5ff', 'lineColor': '#00e5ff', 'secondaryColor': '#02080a', 'tertiaryColor': '#0a1417', 'background': 'transparent', 'clusterBkg': '#0a1417', 'clusterBorder': '#0e3a44', 'edgeLabelBackground': '#06222b', 'titleColor': '#67e8f9', 'nodeTextColor': '#e0fbff'}}}%% graph TD classDef vuln fill:#2d1414,stroke:#ef4444,stroke-width:2px,color:#fecdd3; classDef ok fill:#06181d,stroke:#00e5ff,stroke-width:2px,color:#e0fbff; Start[App initiates TLS handshake] --> Pin{Pinning check
hookable at runtime?} Pin -->|Yes: Frida overrides verify| Bypass[Trust attacker cert]:::vuln Bypass --> Intercept[Proxy reads PHI in cleartext]:::vuln Pin -->|No: native + attestation| Reject[Handshake aborted]:::ok

Frida Pinning-Bypass Script (Proof-of-Concept)

// frida-okhttp-unpin.js — neutralise OkHttp CertificatePinner at runtime
Java.perform(function () {
  const CertificatePinner = Java.use('okhttp3.CertificatePinner');

  // Force the pinning check to always succeed
  CertificatePinner.check.overload(
    'java.lang.String', 'java.util.List'
  ).implementation = function (hostname, peerCertificates) {
    console.log('[+] Bypassed pinning for: ' + hostname);
    return; // no exception thrown => pin "valid"
  };
});

// Run:  frida -U -f com.vitalis.care -l frida-okhttp-unpin.js --no-pause

Note what the script does not do: it never touches the certificate chain, the trust store, or the OS. It changes one method to return instead of throwing. That is why “we obfuscate the pinning code” is not an answer — obfuscation makes the method harder to find, not harder to replace once found, and it needs finding exactly once per release.

With pinning defeated, the proxy captured a live PHI request in cleartext:

GET /v1/patients/me/labs HTTP/2
Host: api.vitalis.example
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Device-Id: 9F3A-CC21-7741

HTTP/2 200 OK
Content-Type: application/json

{"patientId":"pt_88231","labs":[{"test":"HbA1c","value":"7.9%","date":"2026-05-02"}]}

Hardened Pinning Strategy

We moved pinning out of application code and into OS-enforced configuration, then stopped relying on it. Declarative pinning — Android’s Network Security Config, NSPinnedDomains in the iOS Info.plist — is validated by the platform’s networking stack rather than by a method the app owns, which puts it out of reach of a trivial Java-layer hook. It is not out of reach of a determined attacker with kernel-level access; it raises cost, it does not close the door.

<!-- Android — declarative Network Security Config (OS-enforced pinning) -->
<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">api.vitalis.example</domain>
    <pin-set expiration="2027-01-01">
      <pin digest="SHA-256">k3y0fThePr1maryLeafCert0000000000000000000=</pin>
      <pin digest="SHA-256">BackUpP1nF0rR0tati0n00000000000000000000000=</pin>
    </pin-set>
  </domain-config>
</network-security-config>

The expiration attribute and the backup pin are the two lines that stop this configuration becoming an outage. Pinning fails closed: if every pin in the set stops matching, the app cannot reach the API at all, and no amount of restarting fixes it — the fix is a store release, which on iOS means review time you do not control. A backup pin covering the next key in the rotation is what makes certificate renewal survivable. The expiration date is the deliberate escape hatch: past it, Android stops enforcing the pin set rather than bricking the app, which is the right default for a control whose worst realistic failure is self-inflicted denial of service against your own patients. Pin to the intermediate CA’s key rather than the leaf if your certificate rotation is automated and you value uptime over the last increment of strictness. That is a genuine trade, not a best practice with one right answer.

Alongside the config we shipped the layers that matter more: jailbreak and Frida detection feeding telemetry rather than hard app termination, short-lived access tokens with refresh bound to the device, and mutual TLS with a device-provisioned client certificate on the high-sensitivity PHI endpoints. The detection is explicitly not a security boundary — anti-tamper checks are themselves hookable, and an app that hard-exits on a false positive locks out legitimate users on customised ROMs. Its value is the signal: a session that reports integrity.frida_detected and then pulls forty patient records is an alert someone can act on. mTLS is what actually stopped the replay, because a stolen bearer token without the device’s client certificate is now inert.


Critical Finding OC-MOB-003 — Hardcoded API Key via Decompilation

A grep for AIza across the decompiled APK returned a production maps and analytics key on the first pass. This is the least sophisticated finding in the report and, in our experience, the most common: every mobile codebase accumulates keys in constants, in gradle.properties that get compiled in, in strings.xml, in a .env bundled as an asset. ProGuard and R8 do not help — they rename symbols, not string literals, so API_KEY becomes a while the key itself sits there in plaintext.

The quota abuse is the boring part of the impact. The real problem was scope: the key was unrestricted, so it worked from any origin and against every API enabled on the project, including two the mobile app had no reason to touch. An extracted key is not a leaked string, it is whatever that string is authorised to do — and nobody had ever asked what this one was authorised to do.

Recovery via Static Analysis

# Smali excerpt from the decompiled APK (com/vitalis/care/Config.smali)
.field public static final API_KEY:Ljava/lang/String; =
    "AIzaSyB-VitalisProd-3kf9whardcodedKEY8x21q"

# Or trivially, from the raw binary strings:
$ apktool d vitaliscare-release.apk -o out/
$ grep -rni "AIza" out/    # Google-style API key prefix
out/smali/com/vitalis/care/Config.smali: "AIzaSyB-VitalisProd-3kf9w..."

Remediation: Remove Secrets from the Client

There is no way to hide a secret in a binary you hand to the attacker. Encrypting it moves the problem to the decryption key, which is also in the binary; native code moves it to a disassembler; a remote fetch on first launch moves it to the proxy we already own. Every one of these is a delay, priced in hours.

So the fix is architectural: the client stops holding anything worth stealing. We rotated the key the same day, applied package-name and SHA-1 signing restrictions plus a per-API allowlist to what remained client-side, and moved the privileged calls behind our own endpoint that injects the key from a vault server-side.

The costs are worth naming. A backend proxy adds a network hop and a service to run, monitor and keep available — if the proxy is down, mapping is down, where previously the client talked to the upstream directly. It also puts your infrastructure in the path of the upstream’s rate limits, so one abusive user now consumes quota attributed to your service account rather than to a key you could revoke in isolation. For a key that is genuinely low-value and unavoidably client-side, restrictions alone are the proportionate answer. For anything that touches PHI or costs real money per call, the proxy earns its keep. Rotation, meanwhile, is the part teams skip: a key that has been in a public app store binary for eighteen months should be treated as known to everyone, and restricting it without rotating it leaves the old value valid.

secrets-remediation
VULNERABLE
// Secret compiled into the client binary
object Config {
const val API_KEY =
  "AIzaSyB-VitalisProd-3kf9w..."
}

val url = "$BASE/geo?key=" + Config.API_KEY
SECURED (Backend Proxy)
// Client holds NO secret — calls our backend,
// which injects the key server-side from a vault.
val res = api.geocode(
GeoRequest(address = q)
)   // Authorization: short-lived user token only

// Backend (never shipped to device):
//   key = vault.read("maps/api_key")
//   upstream.get(".../geo?key=$key")

PHI Exposure Risk Score · Before vs After

A composite score across local storage, transport security, secret exposure and platform configuration, tracked weekly through the engagement. Read it as a shape, not a measurement: the score is our own weighting of findings by severity and exploitability, so “93.7 to 2.0” is a way of showing that the two step changes came from the Keychain/Keystore migration in week three and the pinning-plus-mTLS release in week five, not from steady incremental effort. Composite risk scores are useful for showing an executive where the effort paid off and dangerous when anyone starts treating the number as an absolute. Two is not zero, and nothing on this chart says the next release cannot reintroduce all of it.

Systemic Risk Mitigation Velocity

Composite PHI-exposure score across the iOS and Android builds

Vulnerable StateHardened State
1007550250Week 1Week 2Week 3Week 4Week 5Week 693.7 BaselineKeychain/Keystore livePinning + mTLS shipped2.0 Hardened

Live MITM Interception Simulator

Replay a PHI request through the device and toggle between the two builds. The Frida hook is active in both — that is the point. The pre-engagement client hands the proxy a bearer token and a patient’s HbA1c result; the hardened build aborts the handshake at the OS layer, and just as usefully, emits telemetry saying an instrumented device tried. The second line is what turns a bypass attempt from an invisible event into an investigable one.

Build:
Proxy · Frida Hook Active
INTERCEPTED
GET /v1/patients/me/labs
Pinning check hooked → returns valid. Establishing proxy TLS… ●●●
⚠ Cleartext captured: Bearer eyJhbGci… + HbA1c 7.9%
PHI exposed: patientId pt_88231 readable in transit.
Proxy · Frida Hook Active
BLOCKED
GET /v1/patients/me/labs
OS-native pin validated outside hookable code path… ●●●
⚠ TLS handshake aborted: proxy certificate not in pin-set.
telemetry: tls.pin_failure + integrity.frida_detected · session revoked

Quantifiable Business Impact

Every PHI-exposure path we proved was closed and re-tested before the release reached a patient device. The compliance framing needs care, though: HIPAA has no certification and no auditor signs off a mobile app. What Vitalis gained is evidence — a documented assessment, findings with proof, remediation with re-test results — which is what the Security Rule’s risk-analysis requirement actually asks for and what a payer’s due-diligence questionnaire actually wants to see. That is worth a great deal in a partner conversation. It is not a certificate, and describing it as one is how organisations end up surprised during an enforcement action.

Security MetricPre-Audit StateHardened StateQuantified ROI
Auth Token StoragePlaintext on diskKeychain / Keystore-boundEliminated offline PHI session theft
Transport InterceptionPinning bypassable → full MITMOS-native pinning + mTLSPrevented live PHI exposure in transit
Secret ManagementAPI key hardcoded in binaryVault + backend proxy, key rotatedStopped quota abuse & key extraction
Runtime IntegrityNo tamper / root detectionJailbreak/Frida detection + attestationRaised cost of dynamic instrumentation
HIPAA Launch ReadinessNot launch-readyHIPAA-aligned, evidence-backedUnblocked the high-profile release

Strategic Takeaways

Three findings, one underlying mistake: the app was written as though the device were part of the trusted infrastructure. It is not. The binary, the runtime and the filesystem all belong to whoever is holding the phone.

  1. Treat the device as hostile, and be specific about who the attacker is. Cleartext on disk is already compromised on a rooted, jailbroken, backed-up, resold or malware-carrying device — and in healthcare, the resold handset is the realistic case, not the researcher’s Pixel. Tokens and PHI belong in Keychain/Keystore with the device-only accessibility attribute. Budget for the cost: Keystore keys are invalidated by credential and biometric changes, so re-authentication is a path you have to handle rather than an edge case you discover in crash reports.
  2. Client-side controls buy time and generate signal; they do not enforce anything. Pinning, obfuscation and jailbreak detection all raise the attacker’s cost, and all of them fall to someone with a rooted device and an afternoon. Move enforcement to the OS where you can, then assume it fails: short-lived tokens, device-bound mTLS and telemetry on tamper indicators are what still hold when the client is fully owned. And remember pinning fails closed — a pin set without a backup pin and an expiry is an outage waiting for your next certificate renewal.
  3. Nothing secret survives shipping. Encryption, native code and remote fetch all relocate the secret rather than protect it. Route privileged calls through a backend, scope whatever must stay client-side to the narrowest possible permissions, and rotate anything that has already shipped — restricting a key without rotating it leaves the exposed value working.
  4. The fix is verified on a rebuilt binary, not on a diff. Every remediation here was re-tested against a fresh release build on a fresh device. Two of them needed a second pass. A code review that looks correct and an artefact that behaves correctly are different claims, and only one of them is evidence.
Accelerated Integration

Ready to secure your architecture?

Initiate a full cryptographic security review, IAM baseline audit, and penetration testing engagement for your organisation.

Project Onboard? Secure Cryptographic Invitation Pipeline
Visual Showcase

System Schema & Architecture

Curated diagrams, interface snapshots, and architectural blueprints illustrating our core technical approach and environment mapping.

AI image prompt — A highly professional, ultra-realistic corporate photo of a bright mobile product team room in broad daylight. Smiling engineers review a flagship health app prototype on a wall-mounted light dashboard showing device security posture and PHI data-flow diagrams accented with vivid cyan (#00e5ff). Clean desks, green plants, bright daylight, premium commercial workspace aesthetic
AI image prompt — A clean, bright 3D isometric infographic diagram explaining a mobile MITM interception flow. Rendered on a minimalist off-white surface with soft studio shadows. Labeled blocks for Mobile Device, Intercepting Proxy, TLS Pinning, and API Gateway connected by glowing cyan (#00e5ff) flow lines. Elegant vector-art architectural render, professional corporate style
AI image prompt — A realistic candid photograph of a professional mobile security engineer working at a large high-end monitor in a bright office. Natural daylight floods the room. The screen shows a decompiler interface and a Frida instrumentation console. Potted plants, light-wood accents, premium corporate office, 8k resolution
AI image prompt — Ultra-realistic executive presentation scene in a bright boardroom with natural daylight. A professional female security architect presents a mobile hardening and HIPAA-compliance roadmap on a large white screen with elegant cyan (#00e5ff) line details. Executives listen around a sleek modern table. Shot on Hasselblad, premium corporate high-end aesthetic
Client Endorsement

Hear it straight from Vitalis Health

"We were weeks from our biggest launch, with payer partners and clinicians depending on us to protect highly sensitive patient data. The assessment team reverse-engineered our app, pulled auth tokens straight off a rooted device, bypassed our SSL pinning to read live API traffic, and recovered an API key we thought was safely buried in the binary. Every finding came with working proof and a precise, production-ready fix. They turned a launch-blocking risk into a HIPAA-aligned security story we now tell with confidence."

Webster Herzog

Webster Herzog

Co-Founder & CTO at Vitalis Health

Sponsored Link

Subscribe to my newsletter

Receive my case study and the latest articles on my WhatsApp Channel.

Warning

Ask CyberROX AI