Safeguard
Application Security

A Deep Link Is an Unauthenticated Entry Point Into Your App

Anything can send one: a web page, a QR code, a message, another app on the device. With a custom scheme there is not even a guarantee the link reaches your app rather than someone else's.

Shadab Khan
Engineering
5 min read

Your mobile app registers a URL scheme so links can open it. myapp:// now routes into your application, carrying parameters that drive navigation and sometimes perform actions.

Anything can send those links: a web page, a QR code, a message, another app on the same device. There is no authentication at that boundary and, with a custom scheme, not even a guarantee that the link reaches your app rather than someone else's.

This post is what to check about your deep links. For whoever owns the mobile client.

A custom scheme is not an identity

Any application can declare myapp:// in its manifest. The operating system's behaviour when two apps claim the same scheme is not something you control, and it has varied across platforms and versions: last installed wins, first installed wins, or the user gets a picker.

So a custom scheme is a request rather than a claim. A malicious app that registers yours can receive links intended for you, including anything in them.

Verified links are the fix. App Links on Android and Universal Links on iOS tie your app to a domain you control through a file the platform fetches and verifies. Another app cannot claim your domain.

Two practical notes. If the verification file is unreachable or malformed, the link falls back to opening in the browser, which is a silent failure of your deep linking rather than of your security. And you should keep a custom scheme only as a fallback, never as the path that carries anything sensitive.

Treat the parameters as untrusted input

A deep link is an unauthenticated request into your app's navigation, and the parameters are attacker-controlled in exactly the same sense as a query string on your web API.

The failures follow the same pattern:

Identifiers that grant access. myapp://document/4812 should display document 4812 only if the logged-in user may see it. The check belongs in the code the link routes to, not in the interface the user would normally have come from.

Actions performed on arrival. A link that deletes, transfers, approves or changes a setting on open is a request forgery with no origin check available. State-changing deep links should land on a confirmation screen rather than acting.

Redirect targets in parameters. A link carrying a destination URL that your app then opens is an open redirect, and on mobile it can also be a route into a WebView with your session in it.

Injection into a WebView. Parameters interpolated into a locally rendered page are a cross-site scripting sink with access to whatever the WebView is configured to reach, which is often more than a browser tab would be.

Do not carry secrets in a link a scheme can receive

This is why the OAuth guidance for mobile exists. An authorisation code returned to a custom scheme can be received by whichever app the system decides owns that scheme, and a code alone is enough to complete the exchange.

The mitigation is to bind the exchange to the client that started it, which is what PKCE does, and to use a verified link rather than a custom scheme for the redirect. The same reasoning applies to anything else you might be tempted to deliver by link: magic-link tokens, password reset codes, invitation tokens.

If a secret must travel this way, make it single use, short lived, and bound to something the receiving app must also prove.

Check yours

The tests are quick and the first one is the one people have never tried:

# Android: send a deep link as any app would
adb shell am start -W -a android.intent.action.VIEW \
  -d "myapp://document/4812" com.yourcompany.app

# What schemes and hosts does the app declare?
aapt dump xmltree app.apk AndroidManifest.xml | grep -A3 -i "scheme\|host"

# Is the verification file present and valid?
curl -s https://yourdomain.example.com/.well-known/assetlinks.json | head -20
curl -s https://yourdomain.example.com/.well-known/apple-app-site-association | head -20

Then, by hand: send a link referencing an object belonging to another account and see what the app renders. Send a link that triggers a state change and see whether it confirms. Install a second app declaring the same custom scheme and see which one receives the link.

The server is where authorisation belongs

Everything above is client-side hygiene, and none of it is the control. The app is on a device you do not own, and anyone can craft requests to your API regardless of what the app does.

So the deep link handler should navigate, and the API should authorise. If the only thing preventing a user seeing another customer's document is that the app would not normally show them that screen, you do not have an access control, you have a user interface.

The concession

Verified links require a domain, a hosted file, and a build configuration that some cross-platform toolchains make awkward. Teams ship a custom scheme because it works in ten minutes, and for a link that opens a settings screen the risk is genuinely negligible.

Scale it to what the link carries. Navigation to non-sensitive screens: a custom scheme is fine. Anything carrying a token, identifying a specific record, or triggering an action: verified links, and the authorisation check on the server behind it.

The implication

Deep links are built by whoever was implementing the marketing campaign or the email template, and they open a path into your application that nothing authenticates.

Send yourself a link pointing at someone else's record. What your app displays is the whole answer.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.