---
key: "android-ios-security"
title: "Review mobile storage, links and platform permissions"
url: "https://softwaresecurity.com/domains/appsec/platform-security/android-ios-security/"
api: "https://softwaresecurity.com/api/v1/articles/android-ios-security.json"
domain: "appsec"
topic: "Application security"
type: "Guide"
identifiers: ["CWE-926","CWE-939","CWE-922","CWE-532","CWE-295"]
tags: ["Mobile apps","Threat modeling"]
stages: ["design","build"]
roles: ["developer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Review mobile storage, links and platform permissions

Keep local artifacts and platform entry points from exposing authority that belongs on the server.

Canonical URL: https://softwaresecurity.com/domains/appsec/platform-security/android-ios-security/

## In short

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.

## Recommended defaults

| 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 |

## Verify it

1. Inspect the merged manifest and confirm only intended components are exported, each with a deliberate `android:exported` value.
2. 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.
3. Create a backup or device transfer and confirm the session and cached records are absent after restoring to a second device.
4. Lock the device and open the app switcher; no appointment details may appear in the snapshot or on the lock screen.
5. Route traffic through a proxy with a user-installed CA and confirm release builds refuse the connection.

## 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.

Audience: Android and iOS developers configuring manifests, entitlements and local storage, and reviewers running a MASVS-based assessment.

Before you start:

- Distinguish an app sandbox, exported component, deep link and platform credential store.

Time: About 26 minutes: 8 to read and 18 for the exercise

## Model the operating system as a set of entry points and storage policies

A 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.

## Trace sensitive data beyond the primary credential store

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.

```swift
// 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)
}

```

## Narrow links, permissions and inter-app requests

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`.

```xml
<!-- 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>

```

## Map each control to its Android and iOS setting

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 |

## Verify on the supported device and account lifecycle

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.

## A real-world failure

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.

## Validate platform-specific assumptions with targeted evidence

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 material

```text
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.

Worked answer: L1 opens the authorized `A17` view. L2 may route to the app but the server denies `B22` to Alice. Domain association does not authorize the selected appointment.

### 2. Reject expanded link capabilities

Evaluate L3 and L4 under the exact contract.

Worked answer: L3 rejects the extra `action` field and cannot cancel `A17`. 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.

Worked answer: `T1` in 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`.

Worked answer: Bob must not see Alice's cached `A17`; `T1` cannot refresh online after revocation. Contacts denial should not block `B22` because the feature does not need contacts. No broader permission fallback is justified.

Deliverable: A link decision table and a storage/account-switch checklist with exact expected visible and server state.

Scope: 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.

## Executable practice

- [Keep an iOS app's storage, links and web bridge in bounds: project ZIP](https://softwaresecurity.com/downloads/projects/ios-platform-boundaries-5d4c4cd9417f9c64.zip) — Swift 6 · Xcode 16+ · iOS Simulator
- [Repair an Android app's links, components, network policy and storage: project ZIP](https://softwaresecurity.com/downloads/projects/android-platform-boundaries-52934d7ed9eeeff3.zip) — Kotlin · Android Gradle Plugin 9.4 · Robolectric 4.17 · Docker

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OWASP MASVS and MASTG](https://mas.owasp.org/MASVS/)
- [Android security best practices](https://developer.android.com/privacy-and-security/security-best-practices)
- [Apple Platform Security: keychain data protection](https://support.apple.com/guide/security/keychain-data-protection-secb0694df1a/web)
- [Android: verify App Links](https://developer.android.com/training/app-links/verify-applinks)
- [Android: back up user data with Auto Backup](https://developer.android.com/identity/data/autobackup)
- [Android: network security configuration](https://developer.android.com/privacy-and-security/security-config)
- [Apple: kSecAttrAccessibleWhenUnlockedThisDeviceOnly](https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlockedthisdeviceonly)
- [Apple: supporting associated domains](https://developer.apple.com/documentation/xcode/supporting-associated-domains)
- [Apple: establishing your app's integrity (App Attest)](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity)
- [Android: Play Integrity API](https://developer.android.com/google/play/integrity)
- [OWASP Mobile Top 10 2024](https://owasp.org/projects/mobile-top-10)
- [NVD: CVE-2021-30860, iOS CoreGraphics zero-click (FORCEDENTRY)](https://nvd.nist.gov/vuln/detail/cve-2021-30860)
- [OWASP MASWE v1.0.0 release (renumbered weaknesses)](https://github.com/OWASP/maswe/releases/tag/v1.0.0). v1.0.0.