Declare every Android component's android:exported value and export only what must be reachable; verify App Links and Universal Links but still validate their parameters. Keep tokens in the Keychain with a ThisDeviceOnly class or behind Android Keystore keys, exclude sessions and caches from backups, protect cached files, and hide sensitive screens from snapshots. Keep TLS validation on with no cleartext, request permissions only when needed, and leave every authorization decision to the server.
- Every exported component, link handler and extension is an API that other apps can call with any input.
- Link verification proves which app opens a URL, not that the user may act on the appointment it names.
- Protect every copy: logs, backups, notifications, snapshots, clipboard and caches as well as the credential store.
- Keep platform TLS validation on and test the failure path; certificate pinning adds rotation and recovery obligations of its own.
- Attestation from Play Integrity or App Attest is a risk signal for the server, never a replacement for its authorization checks.
| Setting | Value | Note | Source |
|---|---|---|---|
| Android components | An explicit android:exported on every activity, service and receiver with an intent filter; false unless other apps must call it | Required when targeting Android 12 (API 31) or later. | Android manifest documentation |
| Android App Links | android:autoVerify="true" plus https://<host>/.well-known/assetlinks.json listing the signing-certificate SHA-256 | — | Android: verify App Links |
| Android backups | android:dataExtractionRules (Android 12+) and android:fullBackupContent excluding session files and caches | — | Android: back up user data with Auto Backup |
| Android network | cleartextTrafficPermitted="false" and system trust anchors only | Cleartext is already off by default when targeting API 28 or later. | Android network security configuration |
| iOS Keychain | kSecAttrAccessibleWhenUnlockedThisDeviceOnly for refresh tokens | — | Apple: kSecAttrAccessibleWhenUnlockedThisDeviceOnly |
| iOS files and network | .completeFileProtection and isExcludedFromBackup for cached records; App Transport Security without NSAllowsArbitraryLoads | — | Apple: completeFileProtection; App Transport Security |
- Inspect the merged manifest and confirm only intended components are exported, each with a deliberate
android:exportedvalue. - Launch every exported activity and link with extra, missing and foreign parameters, for example with
adb shell am start -a android.intent.action.VIEW -d <url>, and confirm no action runs. - Create a backup or device transfer and confirm the session and cached records are absent after restoring to a second device.
- Lock the device and open the app switcher; no appointment details may appear in the snapshot or on the lock screen.
- Route traffic through a proxy with a user-installed CA and confirm release builds refuse the connection.
- Applies to
Android and iOS boundary review. Platform APIs and entitlements need device and OS-specific verification.
- Part of
- Workflow: Review a design
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Review links and components as external entry points.
- Trace credential and personal-data copies beyond secure storage.
- Verify account switching and permission-denied behavior.
Before you start
Android and iOS developers configuring manifests, entitlements and local storage, and reviewers running a MASVS-based assessment.
- Distinguish an app sandbox, exported component, deep link and platform credential store.
Refresh first: Review trust boundaries in mobile and native clients
Go to the guided practiceA mobile application communicates with the server, other apps, browser navigation, operating-system services and the user. Each path carries different evidence of identity or intent. An Android exported component can receive requests from another app; an iOS URL handler can receive externally supplied navigation. A verified app link or universal link helps associate a domain with an app, but the link's parameters still require validation and server authorization.
Consider a medical-appointment app that opens appointment A17 for its owner and stores a refresh credential. The feature needs an appointment id, not an arbitrary callback URL, filesystem path or immediate cancellation instruction. A link may open a review screen after authentication; cancellation requires the normal authorized confirmation flow. Platform association is evidence about routing the URL, not permission to act on the selected appointment. Keep domain association files, application entitlements and server permission rules as separate review items.
Use the platform's supported credential storage and choose access restrictions appropriate to the device and workflow. On Apple platforms, Keychain accessibility classes such as kSecAttrAccessibleWhenUnlockedThisDeviceOnly decide when items can be read and whether they migrate to a new device; on Android, Keystore-backed keys can protect cryptographic material, optionally in StrongBox hardware, and encrypt app-specific storage. The exact guarantee depends on configuration, hardware and operating-system version. Avoid asserting that any item labeled secure storage is unreadable under every device-compromise scenario.
Follow copies: notification text, recent-app snapshots, logs, crash reports, clipboard, backups, temporary files and cached API responses. If the refresh credential is carefully protected but copied into a debug log or URL, the primary storage choice did not protect that copy. Define account-switch and logout behavior for both credentials and cached appointments. Revoke refresh authority server-side where the session design supports it; deleting a local file alone does not invalidate a copied credential. Minimize offline data and set an explicit retention rule.
The Swift code below stores the refresh token with kSecAttrAccessibleWhenUnlockedThisDeviceOnly, deletes it at logout and account switch, and writes cached appointments with complete file protection, excluded from backups. It compiles against the iOS 27 SDK. The Simulator doesn't enforce Data Protection, so check file protection on a device. Keychain calls work in the Simulator from tests hosted in an app, which Xcode signs to run locally; a test bundle without a host app gets errSecMissingEntitlement (-34018). The iOS project on /labs/ runs the Keychain checks that way.
// TokenStore.swift (iOS 15+): local copies of the session, each with its own protection.
import Foundation
import Security
enum TokenStore {
static let service = "com.example.appointments.refresh"
// Refresh token in the Keychain: readable only while unlocked, never restored to another device.
static func save(_ token: Data, account: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
]
SecItemDelete(query as CFDictionary)
var item = query
item[kSecValueData as String] = token
item[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
let status = SecItemAdd(item as CFDictionary, nil)
guard status == errSecSuccess else { throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) }
}
static func load(account: String) -> Data? {
var result: AnyObject?
let status = SecItemCopyMatching([
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
] as CFDictionary, &result)
return status == errSecSuccess ? result as? Data : nil
}
// Logout and account switch: delete the local copy; the server revokes the token itself.
static func delete(account: String) {
SecItemDelete([
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
] as CFDictionary)
}
}
// Cached appointments: unreadable while the device is locked, and excluded from backups.
func writeAppointmentCache(_ data: Data, to url: URL) throws {
try data.write(to: url, options: [.atomic, .completeFileProtection])
var values = URLResourceValues()
values.isExcludedFromBackup = true
var file = url
try file.setResourceValues(values)
}A link handler should parse a fixed operation and id format, reject extra action parameters and route through the same authenticated API used by the normal screen. Do not implement a generic open-any-URL or execute-any-action bridge. For Android components and content providers, review exported status, required permissions, URI grants and whether callers can select arbitrary resources; apps targeting Android 12 or later must declare android:exported on every component with an intent filter, and anything another app does not need should say false. For iOS extensions and URL handlers, review the equivalent data-sharing and invocation boundaries rather than assuming the main app's UI guards apply.
Request only platform permissions the feature needs, at the point the user understands why. Denial and later revocation must produce a safe degraded mode, not a crash or broader fallback. A contacts permission is not needed to view a server-owned appointment. Treat a WebView as a browser with native capabilities attached: one that displays outside content should not inherit a bridge capable of reading credentials or making privileged native calls. Native bridges require typed methods, source validation and operation-specific checks. Keep TLS certificate validation enabled and test failure paths; certificate pinning adds operational rotation and recovery obligations and is not a universal replacement for standard trust validation.
The manifest excerpt below exports one activity, reachable only through a verified App Link to /view, keeps the FileProvider unexported so attachments leave only through temporary URI grants, and points to backup and network rules that exclude the session and cached records and refuse cleartext and user-installed certificate authorities. The manifest and its four XML resources linked without errors with aapt2 9.4.1 against the Android 37.1 platform, which rejects misspelled attributes such as android:exportd.
<!-- AndroidManifest.xml (excerpt): one exported entry point, nothing else reachable. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:networkSecurityConfig="@xml/network_security_config">
<!-- Verified App Links open the view screen; the activity still validates the id. -->
<activity android:name=".AppointmentActivity" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="appointments.example.test"
android:path="/view" />
</intent-filter>
</activity>
<!-- Shares one chosen attachment through a temporary URI grant, never a directory. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.appointments.files"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>The same requirement uses different mechanisms on each platform. The table pairs them and names the MASVS v2.1.0 control a reviewer checks; MASVS-STORAGE-2 is "the app prevents leakage of sensitive data" and MASVS-PLATFORM-1 is "the app uses IPC mechanisms securely". MASTG 2.0 tests cite MASWE weaknesses, which MASWE 1.0.0 renumbered in August 2026, so check an ID on mas.owasp.org before you cite it. Platform-specific detail is in /domains/appsec/platform-security/android-secure-coding/ and /domains/appsec/platform-security/ios-secure-coding/.
| Control | Android | iOS | MASVS |
|---|---|---|---|
| Entry points | Explicit android:exported; permissions on exported components | Only the URL schemes, Universal Links and extensions you need | MASVS-PLATFORM-1 |
| Verified links | android:autoVerify and assetlinks.json | Associated Domains applinks: and apple-app-site-association | MASVS-PLATFORM-1 |
| Sharing a file | FileProvider with FLAG_GRANT_READ_URI_PERMISSION for one URI | Share sheet with the single file; no shared container path | MASVS-PLATFORM-1 |
| Credentials | Android Keystore key protecting the token | Keychain with a ThisDeviceOnly class | MASVS-STORAGE-1 |
| Backups | dataExtractionRules and fullBackupContent exclusions | ThisDeviceOnly Keychain items; isExcludedFromBackup files | MASVS-STORAGE-2 |
| Screens and snapshots | FLAG_SECURE on sensitive windows | Cover sensitive content in sceneWillResignActive | MASVS-PLATFORM-3 |
| Transport | Network security config: no cleartext, system CAs | App Transport Security defaults | MASVS-NETWORK-1 |
| Device integrity signal | Play Integrity API | App Attest (DCAppAttestService) | MASVS-RESILIENCE-1 |
Inspect the application's behavior when opened through a link while signed out, signed in as another user, after permission revocation and after switching accounts. Check both network responses and local display. A denied server request can coexist with a stale appointment card from a previous account. Backup restoration and reinstall may preserve some data depending on platform settings; verify the actual policy for the supported OS versions instead of assuming uninstall is a universal erasure event.
Record synthetic identifiers and state transitions during testing without capturing real patient data or credentials. A platform security checklist provides coverage categories, while a feature-specific trace supplies the expected outcome. Treat attestation or device-integrity failures as risk signals with an explicit response. They may support high-risk transaction policy, but cannot replace server ownership checks or make an externally supplied link trustworthy.
Two local projects on /labs/ run these boundaries as tests against a vulnerable and a fixed version of one banking app. The iOS one runs an XCTest suite in the Simulator: Keychain storage, the built Info.plist's ATS settings, link parsing and a WKWebView bridge. The Android one runs Robolectric tests and Android Lint in a pinned container: links and intent redirection, exported components, network security configuration and token storage. Neither replaces the device checks above.
CVE-2021-30860 (FORCEDENTRY), a zero-click CoreGraphics integer overflow exploited by Pegasus spyware on iOS, shows that platform hardening does not remove the need to patch the libraries that parse untrusted input and to minimize how much code touches it.
The same product requirement can need different platform controls. An Android content provider sharing an attachment may grant temporary URI access to a chosen recipient; an iOS share extension follows a different container and entitlement model. Do not translate one platform's checklist item by name and assume the authority is equivalent. Record who can invoke the component, what data they can select, how long access lasts and which process verifies the selection.
For an attachment to A17, the useful property is that another app cannot request B22's file by changing a URI or message field. The server may already deny B22, but a local provider could still expose a cached copy. Validate ownership when issuing the share capability and scope it to the selected file. Avoid granting an entire directory when the user chose one attachment, and make temporary permissions expire according to the intended workflow.
Notification and recent-app previews are another separate presentation boundary. A locked device displaying a sensitive appointment description may violate the product's privacy expectation even when storage encryption works correctly. Decide which fields can appear in those surfaces and test the locked/unlocked transition with synthetic records. Platform documentation and MASVS organize the review, but the resulting verification must name the actual observable data and device state. This keeps platform security work tied to user-facing confidentiality instead of accumulating settings whose effects nobody has measured.
Guided practice
Review appointment links and local data with a complete policy.
Supplied scenario material
Server: appointment A17 belongs to Alice; B22 belongs to Bob. Alice session T1 is valid.
Link contract: https://appointments.example.test/view?id=<A or B followed by digits>; view only; no extra fields. A trusted platform association routes this origin to the app.
L1=https://appointments.example.test/view?id=A17
L2=https://appointments.example.test/view?id=B22
L3=https://appointments.example.test/view?id=A17&action=cancel
L4=https://appointments.example.test.attacker.invalid/view?id=A17
Local copies: secure credential store=T1; debug log contains T1; cache contains A17 at 09:00.
Logout policy: revoke T1 refresh authority and clear appointment cache. Next user Bob signs in. Contacts permission denied; appointment view does not require it.1. Separate routing from authorization
Evaluate L1 and L2 as Alice even though both use the associated origin.
Expected result and reasoning
L1 opens the authorized
A17view. L2 may route to the app but the server deniesB22to Alice. Domain association does not authorize the selected appointment.2. Reject expanded link capabilities
Evaluate L3 and L4 under the exact contract.
Expected result and reasoning
L3 rejects the extra
actionfield and cannot cancelA17. L4 has the wrong host and is not a trusted application link. Prefix matching would misclassify it.3. Find secondary credential exposure
Identify the control gap in the local-copy inventory and define the fix.
Expected result and reasoning
T1in the debug log remains a credential leak despite secure storage. Remove/redact that logging and follow the session response for any exposed live token; protect every copy, not just the designated store.4. Verify logout and permission denial
State what Bob sees after account switch and whether denied contacts access should prevent viewing Bob's own
B22.Expected result and reasoning
Bob must not see Alice's cached
A17;T1cannot refresh online after revocation. Contacts denial should not blockB22because the feature does not need contacts. No broader permission fallback is justified.
Paper exercise: links, local copies and policies are supplied. The manifest and Swift code above show the platform settings for the same review, and the iOS and Android projects on /labs/ test them.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Run a local project
Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.
Keep an iOS app's storage, links and web bridge in bounds
Repair a SwiftUI banking app until its XCTest suite passes in the iOS Simulator: Keychain storage, App Transport Security in the built Info.plist, link handling and a WKWebView bridge.
Needs: A Mac with Xcode 16 or later and an iOS Simulator; no signing or developer account.
In the extracted folder, run:
sh run-tests.sh19 regression tests · The starter intentionally fails some cases.
No dev container: the iOS SDK runs only on macOS.
Real Keychain, ATS and WebKit behavior in the Simulator. Data Protection and universal-link association need a device or a signed build. Verified with Xcode 27.0 on iOS 27.0 and 26.5.
Repair an Android app's links, components, network policy and storage
Repair a Kotlin banking app until its Robolectric tests and Android Lint pass in a pinned SDK container: links and intent redirection, exported screens, network security configuration and Keystore-encrypted storage.
Needs: Docker, running the image as linux/amd64 (Rosetta or emulation on Apple silicon), and about 1.2 GB of downloads on the first run.
In the extracted folder, run:
docker run --rm --init --platform linux/amd64 \
-v "$PWD":/project -w /project \
-v android-boundaries-gradle:/root/.gradle -v android-boundaries-m2:/root/.m2 \
-e EXERCISE_MODULE \
ghcr.io/cirruslabs/android-sdk:36@sha256:f9b3ea9ed2b5fc9522adae82c7b4622ab7aa54207ef532c8e615a347dca08f31 \
sh run-tests.sh20 regression tests · The starter intentionally fails some cases.
No dev container: the README's docker command runs the pinned SDK image.
JVM tests on Robolectric's Android 16 framework plus Lint, not an emulator: the Keystore and network configuration are checked as key specification and compiled resource.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- OWASP MASVS and MASTG
- Android security best practices
- Apple Platform Security: keychain data protection
- Android: verify App Links
- Android: back up user data with Auto Backup
- Android: network security configuration
- Apple: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
- Apple: supporting associated domains
- Apple: establishing your app's integrity (App Attest)
- Android: Play Integrity API
- OWASP Mobile Top 10 2024
- NVD: CVE-2021-30860, iOS CoreGraphics zero-click (FORCEDENTRY)
- OWASP MASWE v1.0.0 release (renumbered weaknesses). v1.0.0.
Reference information
- SDLC phase
- DesignDevelopment
- Role
- Developers