The three layers a mobile app penetration test covers
A mobile engagement is three separate bodies of work that usually arrive as one line on a quote. The first is the client package, the compiled artefact you publish: hardcoded endpoints and keys, debug flags that survived the release build, exported components, and deep link handlers. The second is the device state, whatever the app leaves on hardware you do not control: local databases and preference files, the platform keychain or keystore, cached WebView data, and the screenshot the operating system takes when the app goes to the background. The third is the API, and that is where nearly every exploitable finding lives, because the client is only a caller.
That ordering decides what you are buying. Reading the package is fast and mostly reconnaissance: it hands the tester endpoint names, parameter shapes, feature flags and the admin routes nobody documented. Exploitation then happens against the server, the only participant that can be made to hand over another customer's records. An engagement that stops at the binary produces a report about obfuscation and pinning while missing the authorisation flaw that leaks the customer table.
Ask a provider how the days split across those three layers before you compare quotes, because two proposals with the same day count can be entirely different engagements. The API side is a discipline of its own and deserves to be scoped as API work, so what follows stays on the parts specific to mobile.
What does the tester need from you before day one?
The installable build is the item teams underestimate, and a copy pulled from the store is not a working starting point. On iOS the tester needs a development or ad hoc build with their device identifier registered on the provisioning profile, or an invitation to your beta channel, and that request goes in with the booking. On Android an APK is straightforward, and an app bundle with no signing configuration is not, because whoever generates the installable package signs it with a different key, and everything bound to the production signing certificate then behaves differently. Agree in advance which checks the delivered build cannot answer.
The pinning decision changes the shape of the engagement. Certificate pinning is exactly what stops a tester's intercepting proxy, and with no way through it the layer where findings concentrate goes untested. There are two clean bypasses. One is a debug build whose network security configuration carries a debug overrides block trusting a certificate authority the tester supplies, which the platform honours only while the build is debuggable and which is needed at all because apps have not trusted user-added authorities by default since Android 7.0. The other is a device the tester can root or jailbreak, where the check is hooked at runtime. Declining both is defensible, and it is recorded in the report as a limitation agreed at scoping.
- An installable build outside the store channel, with the tester's device registered where the platform requires it.
- A build configured like production, because a debug build with logging turned up tells you about the debug build.
- A device the tester can root or jailbreak, or a build with the device integrity check disabled.
- Two accounts per role in two separate tenants, so a cross-tenant read can be demonstrated in the report.
- A written decision on whether pinning gets a debug bypass, and how the tester obtains that build.
- The endpoint inventory, so the tester spends the time on undocumented routes instead of rediscovering the documented ones.
What if the app is a shell around a web application?
Plenty of apps scoped as a native mobile test are a WebView loading a web application, with a thin native layer for notifications and hardware access. When that is true, most of the assessment is web application and API testing with platform-specific handling on top, and saying so at scoping changes both the skill mix and the day count. Treating a hybrid app as two full native assessments buys duplicate work against one codebase.
The genuinely mobile part is the bridge between web content and native code. On Android an injected JavaScript interface exposes annotated native methods to whatever page the WebView has loaded, and on iOS a script message handler does the same. The test case follows directly: can the WebView be made to load content an attacker controls, through a deep link parameter or an open redirect on your own domain, and what does the bridge hand that content when it does. A bridge method that reads a file or returns a stored token is a full compromise once someone else's page is holding it.
The WebView's own configuration is a quick second pass: whether file access and script access from file URLs are enabled, whether mixed content is allowed, and whether the app overrides a TLS error and continues anyway. That last one appears in production regularly, added during development against a self-signed certificate and never removed. Web content in a native shell also loses the protections a browser gives the user, starting with the address bar, so a page loaded from somewhere unexpected is indistinguishable from yours.
What is on the device after ten minutes of use
The device pass is simple to run and consistently productive. Use the app normally for ten minutes, log out, then pull the application container and read everything in it. What turns up most often is a session token sitting in a preference file instead of the platform keystore or keychain, a local database holding every record the user was authorised to see and retaining it after logout, and a cache directory of raw API responses. Verbose logging that prints request bodies or tokens survives into release builds more often than teams expect, because it lives in a networking interceptor nobody reviews as application code.
Two storage details are easy to get almost right. Moving a token into hardware-backed key storage is a small change with a large difference in the report, and on iOS the accessibility class matters as much as the decision to use the keychain at all, because an item stored with a this-device-only class does not migrate to a new device and is absent after a backup is restored onto other hardware, while the default class travels with the backup. Application backup is the second. App data participates in the platform's automatic backup unless you declare otherwise, controlled on Android by the manifest backup attribute and a backup rules resource, and on iOS by the directory a file sits in and the exclude from backup flag, since the documents directory is captured and the caches directory is not. Ask what restoring your app's backup onto an attacker-controlled device yields, because almost nobody tests that path.
The last device item is the background snapshot. When the app moves to the background the operating system writes the current screen to disk so the task switcher has something to show. If the screen showing was an account balance or a patient record, so is the image. Android suppresses the capture with the secure window flag on the activity, and iOS has no equivalent flag, so the app has to cover its own interface with a blank view as the scene resigns active. Severity depends on what your app puts on screen, which is why the screens carrying health or financial data need a deliberate decision.
What are pinning, root detection and biometric gates worth?
These three appear on nearly every enterprise mobile requirement list, usually written as though they protect the API, which they do not. What they buy is attacker time in an attack that already begins with physical possession of the device, and the honest way to value them is in hours of that time. That buys something against opportunistic interception on a hostile network, it does nothing about a determined person running your app on their own phone, and it never substitutes for a check on the server.
Pinning also carries maintenance that teams discover at the worst moment. On Android a pin set declared in the network security configuration takes an optional expiry date, and once that date passes pinning stops being performed. The attribute exists so a certificate rotation cannot cut off every installed copy, and it is also a control that switches itself off quietly on a schedule nobody is tracking. Include a backup pin for the next key before you rotate, keep pin management in the certificate renewal runbook, and find out what the expiry is set to in the version most users are running.
The biometric gate is where the defect is most consistent. A prompt whose only output is a success flag the app then checks guards a screen and nothing else, because the session token behind that screen is already valid and already sitting on disk, so someone holding the device reads the token and never sees the prompt. The implementation worth having binds the biometric to key material: the secret sits in the keystore or keychain under an access control that requires user authentication, and the platform does not release it until the biometric succeeds.
Root and jailbreak detection sits in the same category. The OWASP Mobile Application Security Verification Standard keeps resistance to reverse engineering and tampering in a category of its own, apart from the storage, cryptography, authentication, network and platform controls, which is the right way to read it: hardening for an app whose threat model includes a hostile user of the device, and no part of the answer for anything the server should be deciding. If the reason for adding root detection is that a secret has to live in the client, the secret is the finding and detection does not remove it.
Platform behaviours worth naming in the scope
Deep link verification deserves an explicit line, because it fails silently and invisibly in your own testing. Android App Links and iOS Universal Links both prove domain ownership from the server side: Android reads a digital asset links file listing the package name and the signing certificate fingerprint, and iOS reads an association file served over HTTPS from a well-known path with no redirects. Verification commonly breaks because the file sits behind an authentication gate or a bot filter, returns the wrong content type, or, on the Android side, still lists the fingerprint of a key you no longer sign with. The platform then stops treating those links as yours and they fall back to the browser, and the custom URL scheme you also registered can be claimed by any other application on the device.
What the handler does with the link is the other half. A deep link is untrusted input arriving from any application on the device or any web page the user taps. The routine findings are a handler that loads a URL parameter straight into a WebView, and one that fetches a record identifier without checking the current session's entitlement to it. Every deep link route belongs in the scope document as an entry point, with the parameters it accepts.
Exported components are the Android version of the same question. Any activity, service, broadcast receiver or content provider reachable from another application is an entry point into your process, and the test for each one is what an unprivileged application installed alongside yours can make it do. Apps targeting Android 12 or higher have to declare the exported attribute explicitly on every activity, service and broadcast receiver carrying an intent filter, which forces the decision to be written down without changing what was already exported on purpose. A provider exported without a permission, or a file provider with a path definition broader than intended, hands parts of the app's private directory to anything on the device.
Three build configuration items are cheap to check and repeatedly present in shipped software: a release build still marked debuggable, an entitlement that permits a debugger to attach, and cleartext traffic permitted for a hostname that stopped being temporary years ago. All three are readable from the package alone, and all three close in a single release.
Remediation when the fix ships through a review queue
A web fix is a deploy, and a mobile fix is a build, a store review, a staged rollout, and then a long tail of users who stay on the vulnerable version for months, some of whom never update at all. Settle the plan before the report lands, because the question a customer or an insurer asks is when the finding closes for every user, and the date the code changed does not answer it.
Anything fixable on the server should be fixed on the server, because a server change reaches every installed version at once. If the finding is that the client trusts a value the server should have validated, a client-side fix protects only the users who update. Two mechanisms have to exist before the incident that needs them: a server-controlled flag that can disable a capability across all clients, and a minimum supported version gate that refuses to serve clients below a floor and presents a blocking upgrade screen. On Android the store's in-app update API can present that blocking flow inside the app, iOS has no equivalent mechanism so the screen is yours to build, and in both cases the server has to be the authority on where the floor sits.
Retesting has one mobile-specific rule: verify the fix against the artefact your users actually receive, installed from the channel they install from, instead of against a branch or a local build. The failure that keeps recurring is a fix that is correct in the repository and absent from the released binary, because it missed a release cut, sat behind a flag nobody enabled, or landed in a build variant you do not ship.
How we scope and report mobile work
Black Shard does offensive security and builds production software, and mobile work sits across both. That shapes the scoping call, which asks build questions before testing questions: what the release pipeline produces, how the app is signed, what it stores locally and why, and whether a server-controlled kill switch already exists. Those answers set the shape of the engagement, and they usually surface two or three things worth fixing before any testing starts.
It also shapes the report, because findings written by people who ship software describe the fix in terms of the release cycle it has to survive. We put our own products through the same questions first. Aurii, our clinical software venture, is Australian-hosted on Azure and handles tenant health data with tamper-evident audit trails, so what gets stored, where it is stored, and what can be proven afterwards about who touched it are decisions we have already had to defend. Our approach and trust pages set out that posture in full.
If nobody in your organisation can say today which build a tester would install, on what device, with which accounts, and whether they would be able to see the traffic, those four answers are what to settle first. They come out of one conversation with whoever owns the mobile release, and they decide whether the engagement spends its opening days testing or waiting on access.
