Lab 02 — Android Deep-Link Security Validator

Difficulty: 3/5 | Runs locally: yes

Pairs with the Phase 04 WARMUP Chapter 7 (intents, pending intents, deep links).

Why This Lab Exists (Purpose & Goal)

A deep link is a URL that opens a specific screen in your app — myapp://pay?to=… — and it is one of the most under-appreciated attack surfaces on mobile. A deep-link handler is an exported entry point that takes attacker-controlled parameters from outside the app, and developers routinely treat those parameters as trusted. The goal of this lab is to build the validator that makes deep links safe: exact host/path matching, single-valued parameters, tenant-bound object IDs, allowed actions, and explicit confirmation for anything sensitive.

The Concept, In the Weeds

The two foundational problems:

  • Custom-scheme links have no ownership proof. Any app can register to handle myapp://, so a custom-scheme deep link can be hijacked by a malicious app. The fix is Android App Links — verified https:// links backed by a assetlinks.json digital-asset-links file on your domain — which cryptographically prove your app owns the domain. The validator therefore rejects custom-scheme fallback for sensitive flows.
  • Parameters are untrusted input. A deep link must validate exactly like an HTTP request body: reject duplicate parameters (the same parser-differential idea as Phase 01/02), enforce single-valued parameters, bind object IDs to the authenticated tenant (no cross-tenant references), allow only known actions, and never initiate a sensitive action (a payment, a transfer) without in-app confirmation and server-side authorization tied to the authenticated user.

The canonical bug the validator prevents: a payment deep link that lets an attacker set an arbitrary recipient or cross a tenant boundary. Treat the deep link as navigation that may suggest, never as authority to act.

Why This Matters for Protecting the Company

Deep links are advertised, embedded in emails, and shared — so a malicious one reaches users through trusted-looking channels. A weak deep-link handler turns "the user tapped a link" into account takeover, fraudulent transactions, or cross-tenant data access. When you can build and review deep-link validation — origin proof via App Links, strict parameter handling, no silent sensitive actions, server-side authorization — you protect every user-facing flow the mobile app exposes through URLs.

Build It

Implement the validator: exact host/path, single-valued parameters, tenant-bound object IDs, allowed actions, and explicit confirmation for sensitive workflows. Reject custom-scheme fallback, duplicate parameters, open redirects, and cross-tenant references.

LAB_MODULE=solution pytest -q
# integrate the same cases into Android instrumentation tests

Secure Implementation Patterns

The anti-pattern. A handler that reads parameters straight into an action:

val to = intent.data?.getQueryParameter("to")     // attacker-controlled
pay(to, intent.data?.getQueryParameter("amount")) // no origin proof, no tenant check, no confirm

The secure pattern — exact origin, single-valued params, tenant binding (mirrors solution.py):

def validate_link(url, *, tenant) -> dict:
    p = urlsplit(url)
    if p.scheme != "https" or p.hostname != "field.example":   # App Link origin, not a custom scheme
        raise ValueError("untrusted-origin")
    if p.path not in {"/work-order/open", "/inspection/view"}: # allowlisted paths only
        raise ValueError("unsupported-path")
    params = parse_qs(p.query, keep_blank_values=True)
    if any(len(v) != 1 for v in params.values()):              # reject DUPLICATE params (smuggling)
        raise ValueError("duplicate-parameter")
    flat = {k: v[0] for k, v in params.items()}
    if flat.get("tenant") != tenant:                            # bind to the authenticated tenant
        raise ValueError("tenant-mismatch")
    oid = flat.get("id", "")
    if not oid.startswith(tenant + "_") or len(oid) > 80:       # object id scoped to tenant + bounded
        raise ValueError("invalid-object")
    if "redirect" in flat:                                      # no open-redirect parameter
        raise ValueError("redirect-not-supported")
    return {"path": p.path, "id": oid, "confirmation": "required"}  # NEVER auto-acts

Note the return: confirmation: required. A deep link may navigate and pre-fill; it must never initiate a sensitive action without in-app confirmation and server-side authorization.

Production practices to carry forward:

  • Use verified Android App Links (https:// + assetlinks.json + autoVerify) for origin proof; custom schemes are claimable by any app.
  • Treat every parameter as untrusted input — same validation as an HTTP request body (single-valued, typed, length-bounded, tenant-scoped).
  • The real authorization is server-side, tied to the authenticated user — the client check is defense in depth, not the boundary.
  • No redirect=/open-redirect parameters; reject them outright.

Validation — What You Should Be Able to Do Now

  • Review a payment/sensitive deep link and name what's wrong (untrusted params, hijackable scheme, no confirmation, no server-side authZ) and the fixes.
  • Explain why App Links (verified https:// + assetlinks.json) beat custom schemes for ownership.
  • Treat deep-link parameters as untrusted input subject to the same validation as an HTTP request.

The Broader Perspective

This lab reinforces a theme you have now seen at the web layer (Phase 02 IDOR/SSRF) and will see again at the agent layer (Phase 11): any path that lets the outside world trigger an action with parameters is an authorization surface, and the real decision belongs server-side, tied to an authenticated identity. Client-side handling is convenience, not a boundary. Recognizing deep links, intents, webhooks, and tool calls as instances of the same "externally-triggered action" pattern is what lets you secure them consistently instead of treating each as a novel special case.

Interview Angle

  • "Review myapp://pay?to=X&amount=Y." — Exported entry point with attacker-controlled params and a hijackable scheme. Fix: App Links for origin proof, validate every param, never initiate payment without in-app confirm + server-side authZ tied to the authenticated user/tenant.

Extension (Stretch)

Wire the validated cases into the vulnerable/hardened app variants as instrumentation tests, and implement the assetlinks.json verification path.

References

  • Phase 04 WARMUP Chapter 7; Android App Links / Digital Asset Links documentation.
  • OWASP MASVS (platform interaction, deep linking).