All resources
// Resources

Android App Links and Deep Links Security Review

Published July 8, 2026

An Android deep link is attacker-controlled navigation input. A custom scheme can be claimed by more than one app; an HTTPS App Link adds verified association between a web domain and a signed Android package. Neither condition proves that a URI, account, object identifier, or requested action is safe. Treat routing and authorization as separate controls.

Start with route inventory

Read every VIEW intent filter and record scheme, host, path rules, categories, destination activity, and action reached after parsing. App Links need VIEW, DEFAULT, and BROWSABLE; android:autoVerify="true" asks Android to verify eligible web hosts. Keep host and path scope deliberate. A handler for /orders/* should not accidentally accept /admin/* because broad patterns were convenient during development.

<activity android:name=".OrderLinkActivity" 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="shop.example.test" android:pathPrefix="/orders" />
  </intent-filter>
</activity>

The activity must parse data defensively. Reject unexpected scheme, host, path shape, duplicate query names, encoded separators, missing identifiers, and state-changing commands without authenticated confirmation. Server authorization still decides whether signed-in user may read order 42; client-side route matching never grants that right.

Verify domain association

Publish Digital Asset Links JSON at exact HTTPS location https://shop.example.test/.well-known/assetlinks.json. Use release signing fingerprint, not debug fingerprint. Review every package and certificate listed because each is an allowed relationship.

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.shop",
    "sha256_cert_fingerprints": ["AA:BB:CC:...:FF"]
  }
}]

Fetch path without redirect, check valid JSON, package name, certificate fingerprint, and every production host. Android 12 and later generally send unverified web links to browser instead of app; verified links can open app directly, while user preferences can still affect handling. Do not claim verification makes application input trusted.

Exercise installed behavior

Test generic resolver behavior and explicit package launch. Capture command output, app version, device API level, URI, observed screen, authenticated user, and network evidence. Example:

adb shell am start -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE \
  -d "https://shop.example.test/orders/42?tab=receipt" \
  com.example.shop
adb shell pm get-app-links com.example.shop

Repeat with wrong host, wrong path, duplicate id, empty value, percent-encoded delimiters, foreign object ID, expired session, and action link from another app. Pass means safe rejection or normal sign-in then authorized route. A crash, silent privileged action, or disclosure is fail.

Review record

ArtifactEvidenceOwnerResultExceptionRemediation
Manifest filtermerged release manifestAndroid ownerpass/failapproved public routenarrow host/path
Association fileHTTPS response and release fingerprintweb ownerpass/failstaged hostpublish exact JSON
URI parsernegative adb casesapp ownerpass/faildocumented legacy formatallowlist parser
Destination accessforeign-object testAPI ownerpass/failnoneenforce server authorization

Decision rule: App Link may choose a screen. It must not bypass authentication, object authorization, transaction confirmation, or input validation.

Remediate and retest

Remove unsupported filters, use HTTPS App Links for web-owned routes, keep custom schemes away from sensitive handoffs, and centralize URI validation before navigation. Fix association data at source when release signing rotates. Retest fresh install and update paths because verification state and user choices matter. Mobile testing can correlate manifest, domain, runtime, and backend evidence.

Preserve evidence with build number, signing certificate digest, manifest extract, downloaded association response, test URI, device version, timestamp, and expected result. Assign application owner for parser fix and web owner for domain file fix. Mark exception only where documented business route needs broader scope, expiry date exists, and compensating authorization test passes. Reopen failed item after release candidate confirms change on device.

Worked verification and closure

Use an isolated test device and a release-signed build. First record the association response with curl -i --max-redirs 0 https://shop.example.test/.well-known/assetlinks.json; expected result is HTTP 200, an application/json content type, no redirect, and one target whose package name and SHA-256 fingerprint match release metadata. A 301, HTML login page, stale certificate digest, or debug package entry is a failed web artifact, even when a browser appears to open the app.

Then run adb shell pm get-app-links com.example.shop. Expected output identifies shop.example.test as verified for installed package; record raw output rather than copying a status label into report. Launch each approved URI without explicit package to test resolver selection, then with package only to test parser. For https://shop.example.test/orders/42?id=43, expected behavior is rejection before navigation because ambiguity must not be resolved by parameter order. For an authenticated user lacking access to order 42, expected behavior is same authorization result reached through normal in-app navigation, with no order data shown first.

Close review only after manifest snapshot, HTTPS response hash, device output, approved and rejected URI results, and backend authorization trace refer to same build. Verification can be invalidated by signing rotation, host migration, path expansion, or an Android update; reopen affected evidence instead of assuming prior green status transfers.

Resolver evidence

Run adb shell pm verify-app-links --re-verify com.example.shop before reading current association state. Expected package output lists configured domain as verified; a numeric or unverified state requires checking host response, package certificate, device connectivity, and user link preference. Store command output with Android version. Never mark parser pass from verification result alone: package association and route authorization are different tests.

Sources

Have a system that needs testing?