Treat the client as untrusted: anyone can read its storage, change its requests or invoke its links and components directly. Keep every permission decision on the server, keyed to the authenticated caller and the requested object. On the device, protect every copy of a credential, clear cached data at logout and account switch, keep TLS validation on, and give deep links a small view-only vocabulary that still loads data through the authorized API.
- Hidden buttons, local role flags and obfuscated strings are not authorization; the server checks every resource.
- Deep links are untrusted input: parse them strictly and let them open screens, never approve actions.
- Track every copy of a credential or record: logs, backups, snapshots, caches and exported files.
- Keep TLS certificate validation on and test the failure path; never disable checks to make a request work.
- Offline access is a deliberate window in which revocation cannot reach data already delivered.
- Change only the report id in an authenticated API request and confirm the server denies another user's report.
- Open links with extra parameters, unsupported operations and the wrong scheme and confirm the app opens nothing.
- Log out, switch accounts and restore a backup, then confirm no data from the previous account is visible.
- Search logs, crash reports and analytics payloads for tokens and personal data.
- Proxy the traffic through an untrusted certificate and confirm the app refuses the connection.
- Applies to
Mobile and desktop trust modeling. Device possession and a hidden UI do not establish server authorization.
- 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
- Locate server authority independently of client UI.
- Trace stored credentials and cached data through logout and restore.
- Constrain deep links and offline capabilities.
Before you start
Mobile and desktop client developers, the backend teams that serve them, and reviewers assessing client trust assumptions.
- Distinguish client-controlled state from server-authoritative identity and data.
Refresh first: Enforce authorization for every object action
Go to the guided practiceA mobile or desktop client runs in an environment the application does not fully control. A person can inspect stored files, alter requests, invoke exported components or replace application code. That does not mean every device is compromised; it means server authorization cannot rely on a hidden button, an obfuscated string or a client-computed isAdmin flag. Treat the client as a presenter of credentials and requested actions. The server determines the caller's current permissions.
Follow a concrete expense application. Alice belongs to tenant red and can view her own report R8. The app stores a session credential and displays R8. A deep link expense://report?id=R9 opens a report screen. If the server checks only a valid session, changing R8 to another person's R9 exposes the report regardless of the app's local validation. Device integrity signals may influence a risk decision, but they cannot establish Alice owns R9. The permission check belongs at the API's resource boundary; /domains/appsec/authorization/object-level-authorization/ shows it in code.
Draw the credential's path from login to refresh, secure platform storage, memory, network use, logs, backup and logout. Platform credential facilities can reduce exposure compared with ordinary preferences, but their protection depends on access settings, device state and threat model. A token in secure storage is still available to the app when legitimately used; it does not authorize arbitrary server actions. Avoid placing credentials in URLs, analytics, crash reports or exported files.
Next enumerate entry points that bypass the normal interface: app links, custom URI schemes, share targets, exported activities, webviews, local IPC and privileged helpers. A link is untrusted input even when it looks like your product's URL. Parse it structurally, accept a small operation vocabulary and let the destination screen load only through authenticated, authorized APIs. Opening a review screen is a safer link action than immediately approving a payment or replacing an account credential. For webviews, keep untrusted navigation separate from native bridges and authenticated application content.
The Swift function below is a strict parser for the expense app's only link. It accepts exactly expense://report?id=R<digits> and returns nothing for an extra parameter, a duplicated id, a different host such as approve, a path, user info, a port, a fragment or a different scheme, so a link can open a review screen but never trigger an action. It deliberately accepts R9: whether Alice may see that report is the server's decision, not the parser's. The file compiles against the iOS 27 SDK, and with 11 test cases in a main.swift it runs on macOS with swiftc. Android and iOS configuration for the same boundaries, such as exported components, verified links and keychain classes, is in /domains/appsec/platform-security/android-ios-security/.
import Foundation
enum LinkAction: Equatable {
case viewReport(id: String)
}
/// Accepts exactly expense://report?id=R<1-9 digits>. Anything else opens nothing.
/// The report screen still loads the id through the authenticated, authorized API.
func parseLink(_ url: URL) -> LinkAction? {
guard let parts = URLComponents(url: url, resolvingAgainstBaseURL: false),
parts.scheme == "expense", parts.host == "report",
parts.user == nil, parts.password == nil, parts.port == nil,
parts.path.isEmpty, parts.fragment == nil,
let items = parts.queryItems, items.count == 1,
items[0].name == "id", let id = items[0].value,
id.range(of: #"^R[0-9]{1,9}$"#, options: .regularExpression) != nil
else { return nil }
return .viewReport(id: id)
}A useful test changes only one trust assumption. Alter a report id while preserving Alice's valid session: the server must deny the foreign report. Remove the session while keeping a legitimate deep link: the app may show login, but must not show report data. Forge a premium=true local flag: the server must still enforce purchase entitlement. After logout, verify the refresh credential is revoked according to policy and sensitive local cached reports are cleared or inaccessible to the next account.
Use synthetic records and inspect both client-visible state and server response. A screen that hides an error while a background API response contains the foreign report still fails. Conversely, a denied API call does not prove a previously cached screen stopped displaying private information. Device snapshots, backup restoration and account switching are especially useful boundary cases because they reintroduce data after the normal happy path has finished.
The iOS and Android projects on /labs/ run the same link contract through the platforms' own parsers, URLComponents and android.net.Uri: look-alike hosts, user info, extra parameters and encoded separators open nothing, a valid link only opens a review screen, and payment needs the user's tap there.
Offline features create an explicit period during which the device cannot learn about server-side revocation. Decide whether an offline report is permitted, what fields it includes and when the local copy expires. Do not promise immediate revocation for data already delivered and stored on an offline device. Minimize retained data and require fresh server authorization before new high-impact operations when connectivity returns.
Obfuscation, root or jailbreak checks, attestation and anti-debugging may raise the cost of tampering. They add support and availability tradeoffs and can produce false denials on legitimate devices. Document the response to an unavailable attestation service rather than allowing a network error to grant extra authority. A practical review gives each control its job: secure storage limits credential exposure, transport validation protects the connection, server policy controls resources, and narrow entry points constrain what outside content can ask the client to do. Keep the platform's TLS certificate validation enabled, and test that the app fails safely on an invalid or intercepted certificate rather than disabling checks to make a request succeed. Certificate pinning, MASVS-NETWORK-2, adds rotation and recovery obligations of its own.
These controls map onto OWASP MASVS v2.1.0: MASVS-STORAGE-1 and MASVS-STORAGE-2 for local copies, MASVS-PLATFORM-1 and MASVS-PLATFORM-2 for links, IPC and WebViews, MASVS-NETWORK-1 for transport, MASVS-AUTH-1 for the server-side decisions and MASVS-RESILIENCE-1 to MASVS-RESILIENCE-4 for tamper resistance. In the OWASP Mobile Top 10 2024 they correspond mainly to M1 Improper Credential Usage, M3 Insecure Authentication/Authorization and M9 Insecure Data Storage.
CVE-2017-13156 (Janus) let an attacker modify an Android APK's code without breaking its signature, because the v1 signature scheme ignored bytes prepended to the file. The missing control was verifying integrity with APK Signature Scheme v2 or later.
Three scenarios require different controls. First, an attacker copies a refresh credential from a log: storage hygiene, restricted diagnostics, revocation and credential lifetime limit that exposure. Second, Alice modifies her own client to request Bob's report: only the server's resource authorization prevents that request from succeeding. Third, Alice legitimately downloads her own report and later loses membership while offline: the server cannot retroactively un-deliver those bytes. A review that labels all three 'device security' can choose an expensive control that addresses none of the actual missing boundaries.
For a high-impact operation such as expense reimbursement, keep the decision online and bind approval to the displayed transaction details. The client can submit report id and intended amount, but the server reloads the current approved report and checks the actor's authority. A locally cached approval from yesterday should not approve an edited report today. If the server issues a short-lived authorization challenge, bind it to the relevant report revision and operation rather than a generic 'user passed authentication' flag.
Verification can stay precise without using a compromised production device. Supply the normal API request, change only report id or revision, and inspect the synthetic server state. Then test the separate local lifecycle with account switch and restored cache. This division lets the team identify whether a failure belongs to server policy, credential handling or local privacy rather than treating every visible symptom as an anti-tampering problem.
Guided practice
Review an expense app with all identities, records and transitions supplied.
Supplied scenario material
Alice=(A,red); Bob=(B,red). R8 owner=A; R9 owner=B. Server rule: valid session AND matching owner.
Alice's device: credential T1; cached R8 total=40; local premium=false.
Links: L1 expense://report?id=R8; L2 expense://report?id=R9; L3 expense://approve?id=R9.
Original API: validSession(T1) -> return report(id).
Logout rule: revoke T1 refresh authority; remove cached reports before another account uses the app.
Offline rule: previously authorized reports may be shown for 24 hours, no approvals offline. Snapshot S contains T1 and cached R8 from one hour before logout.1. Trace the ordinary link
Evaluate L1 with T1 and explain the authority at each hop.
Expected result and reasoning
The link selects
R8but grants no permission.T1authenticates Alice; the server ownership comparison authorizesR8; the app may display total 40.2. Find client-only fixes
Evaluate L2 after merely hiding Bob's report in the interface. Then apply the server rule.
Expected result and reasoning
The original API still returns
R9. The repaired API denies because B differs from A. Hiding the screen or using opaque ids does not repair the missing comparison.3. Constrain unsolicited actions
Decide how L3 should behave under a view-only deep-link contract and the offline policy.
Expected result and reasoning
Reject the unsupported
approveoperation. It must not directly approveR9or queue an approval merely because a link was opened. Offline operation cannot add this capability.4. Analyze logout and restoration
After logout restore
Sand try to refreshT1online; then consider the cachedR8bytes offline.Expected result and reasoning
The server rejects refresh because revocation is authoritative. The snapshot may still contain
R8, illustrating why immediate erasure of all historical offline copies cannot be promised. The stated offline retention policy and local account separation need explicit implementation.5. Test an altered entitlement
Change local
premiumtotrueand request a premium export. State the additional server check required.Expected result and reasoning
The server looks up Alice's current entitlement and denies if absent. A local boolean is a display hint; it cannot become a purchase authorization.
Paper exercise: identities, links, cached data and policies are supplied, so no device or app build is needed. The Swift parser above checks the same link cases with swiftc on macOS.
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.
Reference information
- SDLC phase
- DesignDevelopment
- ASVS 5.0.0 requirements
- v5.0.0-8.3.1 · v5.0.0-2.2.2