October 24, 2026
$10,200 for a Link That Opened the Wrong App’s Trust: Mobile Deep Link Hijacking to Account…
Mobile apps have their own version of the open-redirect and OAuth-callback problems I’d chased on the web, wearing a different costume…

By T4nv1
5 min read
Mobile apps have their own version of the open-redirect and OAuth-callback problems I'd chased on the web, wearing a different costume: custom URL schemes and deep links, which let a tap on a web link or a notification open directly into a specific screen of a native app. The trust model is different enough from a browser's that old instincts only partially transfer, and this bug lived entirely in that gap.
The target, which I'll call Beacon Ridge here per their program's terms, is a social fitness app supporting password-reset and email-verification flows that complete via deep link — tap the link in your email, the app opens directly to a screen that finalizes the reset using a token embedded in the link.
Reading the App Instead of Just the API
Testing this meant decompiling the Android APK to understand how deep links were registered and handled — work that's gotten meaningfully faster with AI assistance, and I want to describe that honestly rather than skip over it. After decompiling, I had an LLM summarize the app's manifest file and every activity registered to handle custom URL schemes or App Links, specifically flagging any intent filter that looked like it handled authentication-related actions. That's a structured extraction task over a file format the model has seen a great deal of — Android manifests follow a well-documented, consistent XML structure — and it correctly flagged the relevant activity within seconds, which I then read in full myself to confirm the actual handling logic, since the summary alone wasn't nearly enough to understand the real vulnerability.
The relevant intent filter, confirmed by reading the manifest directly, registered the app to handle a custom scheme:
<activity android:name=".PasswordResetActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="beaconridge" android:host="reset" />
</intent-filter>
</activity><activity android:name=".PasswordResetActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="beaconridge" android:host="reset" />
</intent-filter>
</activity>A link shaped like beaconridge://reset?token=... would open directly into PasswordResetActivity, with the token extracted from the URL and used to complete a reset.
Why Custom Schemes Are Weaker Than They Look
The core problem with custom URL schemes, as opposed to the more modern, verified App Links mechanism Android also supports, is that scheme ownership isn't exclusive or verified by anything beyond whichever app happened to register it. Multiple apps installed on the same device can register the identical custom scheme, and the operating system's behavior when more than one app claims the same scheme varies — sometimes prompting the user to choose, sometimes silently favoring whichever app registered it most recently. This means a malicious app, installed separately by a victim for an entirely unrelated reason, could register the exact same beaconridge:// scheme and potentially intercept a password-reset link intended for the real app.
Confirming the Interception Worked
I built a minimal test app registering the identical scheme and intent filter, installed it alongside the real Beacon Ridge app on a test device, and then simulated clicking a reset link by directly invoking the matching intent from the device's shell:
am start -a android.intent.action.VIEW -d "beaconridge://reset?token=TEST_TOKEN_VALUE"am start -a android.intent.action.VIEW -d "beaconridge://reset?token=TEST_TOKEN_VALUE"On this test device, the system presented a chooser dialog asking which app should handle the link — not a silent hijack by default on this Android version, which meant the realistic attack required an additional step: convincing a victim to select the malicious app from that chooser, a meaningfully weaker position than a fully silent interception, but still a real and demonstrable risk, especially since a malicious app could name itself convincingly or be the only recently-installed app matching the scheme, making it the more likely default selection in some Android versions' chooser-ranking behavior.
The More Serious Finding Underneath
Reading further into PasswordResetActivity's actual handling logic — again, after the AI-assisted manifest summary pointed me at the right file, with everything from here on read and verified entirely by hand — surfaced something more consequential than the scheme-collision risk alone: the activity didn't just read the token from the deep link. It also implicitly trusted that whatever Intent delivered it was legitimate, with no verification that the intent genuinely originated from a link tap at all, as opposed to another, malicious app on the same device directly constructing and firing that exact intent itself, entirely bypassing any URL or chooser dialog altogether.
Intent fakeIntent = new Intent(Intent.ACTION_VIEW);
fakeIntent.setClassName("com.beaconridge.app", "com.beaconridge.app.PasswordResetActivity");
fakeIntent.putExtra("token", "VICTIM_TOKEN_VALUE");
startActivity(fakeIntent);Intent fakeIntent = new Intent(Intent.ACTION_VIEW);
fakeIntent.setClassName("com.beaconridge.app", "com.beaconridge.app.PasswordResetActivity");
fakeIntent.putExtra("token", "VICTIM_TOKEN_VALUE");
startActivity(fakeIntent);A malicious app already installed on the same device, with no special permissions beyond the ability to start another app's exported activity, could launch PasswordResetActivity directly and supply any token value it wanted — no user interaction, no chooser dialog, no deep link click required at all, simply because the activity was exported and didn't verify its caller.
Confirming Against My Own Account Only
I tested this directly against my own Beacon Ridge test account, using a token I'd generated through a real password-reset flow for that same account, confirming the directly-constructed intent successfully completed the reset exactly as a real deep-link tap would have. I didn't attempt this against any other account or any token I hadn't generated myself.
The Report
Title: Exported PasswordResetActivity accepts reset tokens via directly-constructed intents, allowing any co-installed app to complete password resets without user interaction
Root cause: PasswordResetActivity is exported and registered to handle a custom URL scheme shared with no verification of uniqueness, and critically, the activity processes any Intent carrying a token extra regardless of whether that intent originated from a genuine deep-link tap or was constructed directly by another application installed on the same device.
Reproduction: The manifest configuration, the scheme-collision chooser-dialog behavior, and the directly-constructed intent demonstrating that any co-installed app can complete a password reset using a known token value, with no user interaction and no deep link required at all — tested entirely against a self-controlled account and self-generated token.
Impact: Any malicious application installed on the same device as Beacon Ridge can complete a password reset for the account if it obtains or guesses a valid reset token through any means, without needing the user to tap any link at all, independent of the weaker scheme-collision risk.
Fix recommendations:
- Set
android:exported="false"onPasswordResetActivitywhere possible, or, since deep linking requires the activity remain exported, add explicit verification that incoming intents originate from a genuine App Links-verified source rather than accepting any intent carrying the expected extras - Migrate from custom URL schemes to Android App Links, which are cryptographically verified against domain ownership and cannot be silently claimed by another installed application
- Treat reset tokens as single-use and short-lived regardless of delivery mechanism, limiting the window during which a token obtained by any means remains valid
What Happened After
Beacon Ridge migrated to verified App Links and added caller verification to the reset activity within two weeks. The report closed at $10,200, rated high severity.
Why Mobile Deep Link Auditing Rewards Reading the Manifest Closely, With AI as a Starting Point
Custom scheme and exported-activity issues are consistently underexplored compared to web vulnerabilities, partly because decompiling and manually reading through an entire Android manifest and its associated activities is genuinely tedious work. Having an LLM triage a manifest for authentication-relevant intent filters turns an hour of manual file-reading into a two-minute first pass — but the actual vulnerability, an exported activity trusting any intent regardless of origin, only became clear once I read the real Java logic myself and understood exactly what the activity did and didn't verify. The tooling gets you to the right file faster. It doesn't replace sitting with that file and actually understanding it.