From 2f24ddcc7ff473d6fb39850093b42e0458d70ff6 Mon Sep 17 00:00:00 2001 From: Milan Barta Date: Thu, 18 Jun 2026 17:14:50 +0200 Subject: [PATCH 1/3] Validate deep-link args when resolving the start location NavigatorHost.ensureDeeplinkStartLocationValid() validated only the `location` inside the deepLinkExtras intent extra. AndroidX NavController also reads a separate deepLinkArgs extra and merges it over deepLinkExtras when building the start destination's arguments (last write wins), so a value supplied via deepLinkArgs on an externally-launched intent could override the validated start location. Neutralize the externally-supplied deepLinkArgs by replacing each per-destination argument bundle with an empty one, in addition to the existing deepLinkExtras host validation. This is a rewrite (not a removal), so the deepLinkIds navigation path and the validated start location are unaffected. Co-Authored-By: Claude Opus 4.8 --- .../navigation/navigator/NavigatorHost.kt | 14 ++++- .../navigation/navigator/NavigatorHostTest.kt | 54 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt index 25291b7b..d868629f 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt @@ -15,6 +15,7 @@ import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.config.HotwireNavigation internal const val DEEPLINK_EXTRAS_KEY = "android-support-nav:controller:deepLinkExtras" +internal const val DEEPLINK_ARGS_KEY = "android-support-nav:controller:deepLinkArgs" internal const val LOCATION_KEY = "location" open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener { @@ -79,7 +80,16 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener { */ @VisibleForTesting(otherwise = PROTECTED) fun ensureDeeplinkStartLocationValid() { - val extrasBundle = activity.intent.extras?.getBundle(DEEPLINK_EXTRAS_KEY) ?: return + val intent = activity.intent + + // NavController merges deepLinkArgs over the validated deepLinkExtras (last write wins), so + // replace each per-destination bundle with an empty one to stop it overriding the validated + // start location below (or injecting other arguments). + intent.extras?.getParcelableArrayList(DEEPLINK_ARGS_KEY)?.let { args -> + intent.putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, ArrayList(args.map { Bundle() })) + } + + val extrasBundle = intent.extras?.getBundle(DEEPLINK_EXTRAS_KEY) ?: return val startLocation = extrasBundle.getString(LOCATION_KEY) ?: return val deepLinkStartUri = startLocation.toUri() @@ -87,7 +97,7 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener { if (deepLinkStartUri.host != configStartUri.host) { extrasBundle.putString(LOCATION_KEY, configuration.startLocation) - activity.intent.putExtra(DEEPLINK_EXTRAS_KEY, extrasBundle) + intent.putExtra(DEEPLINK_EXTRAS_KEY, extrasBundle) } } diff --git a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt index d6ddec32..2c22bd83 100644 --- a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt @@ -49,6 +49,60 @@ class NavigatorHostTest { assertThat(resultBundle?.getString(LOCATION_KEY)).isEqualTo("https://example.com/path") } + // NavController merges deepLinkArgs over the validated deepLinkExtras, so a location supplied + // via deepLinkArgs must not survive validation. + @Test + fun `neutralizes attacker location smuggled via deepLinkArgs`() { + val intent = Intent().apply { + // Benign owned-domain location passes the existing deepLinkExtras host check. + putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/ok")) + // Attacker override that AndroidX would apply last. + putParcelableArrayListExtra( + DEEPLINK_ARGS_KEY, + arrayListOf(bundleOf(LOCATION_KEY to ATTACKER_URL)) + ) + } + activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() + + host.activity = activity + host.ensureDeeplinkStartLocationValid() + + val survivingLocations = activity.intent + .getParcelableArrayListExtra(DEEPLINK_ARGS_KEY) + ?.mapNotNull { it.getString(LOCATION_KEY) } + .orEmpty() + assertThat(survivingLocations).doesNotContain(ATTACKER_URL) + } + + // deepLinkArgs can carry arbitrary fragment arguments, not just location — none should survive + // validation. (A location-only sanitizer would not satisfy this.) + @Test + fun `does not leak arbitrary attacker arguments smuggled via deepLinkArgs`() { + val intent = Intent().apply { + putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/ok")) + putParcelableArrayListExtra( + DEEPLINK_ARGS_KEY, + arrayListOf(bundleOf(ATTACKER_ARG_KEY to ATTACKER_URL)) + ) + } + activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() + + host.activity = activity + host.ensureDeeplinkStartLocationValid() + + val survivingArgs = activity.intent + .getParcelableArrayListExtra(DEEPLINK_ARGS_KEY) + ?.mapNotNull { it.getString(ATTACKER_ARG_KEY) } + .orEmpty() + assertThat(survivingArgs).doesNotContain(ATTACKER_URL) + } + + companion object { + private const val DEEPLINK_ARGS_KEY = "android-support-nav:controller:deepLinkArgs" + private const val ATTACKER_URL = "https://attacker.example/steal" + private const val ATTACKER_ARG_KEY = "key_docs_and_files_api_url" + } + private class TestActivity : HotwireActivity() { private val navConfig = NavigatorConfiguration( name = "test", From b9a6790e183d80387989350ce8132068c39b70b8 Mon Sep 17 00:00:00 2001 From: Milan Barta Date: Wed, 24 Jun 2026 12:39:50 +0200 Subject: [PATCH 2/3] Gate deep-link sanitization on the launching intent's origin Trust intents the app produced itself (verified via the calling package or referrer, rejecting the spoofable referrer extras) and leave their deep-link extras untouched. Any other intent keeps the existing sanitization: empty deepLinkArgs and revert an off-host start location to the configured one. Co-Authored-By: Claude Opus 4.8 --- .../navigation/navigator/NavigatorHost.kt | 23 +++-- .../navigation/navigator/NavigatorHostTest.kt | 86 ++++++++++--------- 2 files changed, 64 insertions(+), 45 deletions(-) diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt index d868629f..ad2f3291 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt @@ -1,5 +1,6 @@ package dev.hotwire.navigation.navigator +import android.content.Intent import android.os.Bundle import android.view.View import androidx.annotation.VisibleForTesting @@ -73,18 +74,18 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener { } /** - * Google's Navigation library automatically navigates to deep links provided in the - * Activity's Intent. This exposes a vulnerability for malicious Intents to open an arbitrary - * webpage outside of the app's domain, allowing javascript injection on the page. Ensure - * that deep link intents always match the app's domain. + * Google's Navigation library automatically navigates to deep links provided in the launching + * Intent, which lets a malicious Intent open an arbitrary page in the WebView. Intents the app + * produced itself are trusted; any other Intent has its attacker-controllable deep-link + * arguments sanitized so the start location stays within the app's domain. */ @VisibleForTesting(otherwise = PROTECTED) fun ensureDeeplinkStartLocationValid() { val intent = activity.intent + if (shouldTrustIntent(intent)) return // NavController merges deepLinkArgs over the validated deepLinkExtras (last write wins), so - // replace each per-destination bundle with an empty one to stop it overriding the validated - // start location below (or injecting other arguments). + // empty each per-destination bundle to stop it overriding the validated start location. intent.extras?.getParcelableArrayList(DEEPLINK_ARGS_KEY)?.let { args -> intent.putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, ArrayList(args.map { Bundle() })) } @@ -101,6 +102,16 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener { } } + private fun shouldTrustIntent(intent: Intent): Boolean { + // EXTRA_REFERRER / EXTRA_REFERRER_NAME back Activity.referrer and are attacker-settable, so a + // self-origin claim that relies on them can't be trusted. + if (intent.hasExtra(Intent.EXTRA_REFERRER) || intent.hasExtra(Intent.EXTRA_REFERRER_NAME)) { + return false + } + val caller = activity.callingPackage ?: activity.referrer?.authority + return caller == activity.packageName + } + private val configuration get() = activity.navigatorConfigurations().firstOrNull { id == it.navigatorHostId } ?: throw IllegalStateException("No configuration found for NavigatorHost") diff --git a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt index 2c22bd83..aca4afa3 100644 --- a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt @@ -1,7 +1,7 @@ package dev.hotwire.navigation.navigator -import android.R.attr.host import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.core.os.bundleOf import dev.hotwire.navigation.activities.HotwireActivity @@ -11,6 +11,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows @RunWith(RobolectricTestRunner::class) class NavigatorHostTest { @@ -24,83 +25,90 @@ class NavigatorHostTest { } @Test - fun `reverts to config start location when deep link host differs`() { - val extras = bundleOf(LOCATION_KEY to "https://other.com/path") - val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, extras) } + fun `does not modify a trusted self-originated intent`() { + val intent = Intent().apply { + putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://other.com/path")) + putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, arrayListOf(bundleOf(LOCATION_KEY to ATTACKER_URL))) + } activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() + Shadows.shadowOf(activity).setCallingPackage(activity.packageName) host.activity = activity host.ensureDeeplinkStartLocationValid() - val resultBundle = activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY) - assertThat(resultBundle?.getString(LOCATION_KEY)).isEqualTo("https://example.com/start") + // Trusted intents pass through untouched — neither the off-host location nor the args change. + assertThat(activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)?.getString(LOCATION_KEY)) + .isEqualTo("https://other.com/path") + val args = activity.intent.getParcelableArrayListExtra(DEEPLINK_ARGS_KEY) + ?.mapNotNull { it.getString(LOCATION_KEY) }.orEmpty() + assertThat(args).contains(ATTACKER_URL) } @Test - fun `does not change start location when deep link host matches config`() { - val extras = bundleOf(LOCATION_KEY to "https://example.com/path") - val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, extras) } + fun `reverts off-host start location for an untrusted intent`() { + val intent = Intent().apply { + putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://other.com/path")) + } activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() host.activity = activity host.ensureDeeplinkStartLocationValid() - val resultBundle = activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY) - assertThat(resultBundle?.getString(LOCATION_KEY)).isEqualTo("https://example.com/path") + assertThat(activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)?.getString(LOCATION_KEY)) + .isEqualTo("https://example.com/start") } - // NavController merges deepLinkArgs over the validated deepLinkExtras, so a location supplied - // via deepLinkArgs must not survive validation. @Test - fun `neutralizes attacker location smuggled via deepLinkArgs`() { + fun `keeps same-host start location for an untrusted intent`() { val intent = Intent().apply { - // Benign owned-domain location passes the existing deepLinkExtras host check. - putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/ok")) - // Attacker override that AndroidX would apply last. - putParcelableArrayListExtra( - DEEPLINK_ARGS_KEY, - arrayListOf(bundleOf(LOCATION_KEY to ATTACKER_URL)) - ) + putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/path")) } activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() host.activity = activity host.ensureDeeplinkStartLocationValid() - val survivingLocations = activity.intent - .getParcelableArrayListExtra(DEEPLINK_ARGS_KEY) - ?.mapNotNull { it.getString(LOCATION_KEY) } - .orEmpty() - assertThat(survivingLocations).doesNotContain(ATTACKER_URL) + assertThat(activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)?.getString(LOCATION_KEY)) + .isEqualTo("https://example.com/path") } - // deepLinkArgs can carry arbitrary fragment arguments, not just location — none should survive - // validation. (A location-only sanitizer would not satisfy this.) + // NavController merges deepLinkArgs over deepLinkExtras (last write wins); an untrusted intent's + // args must not survive to override the validated start location. @Test - fun `does not leak arbitrary attacker arguments smuggled via deepLinkArgs`() { + fun `empties deepLinkArgs for an untrusted intent`() { val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/ok")) - putParcelableArrayListExtra( - DEEPLINK_ARGS_KEY, - arrayListOf(bundleOf(ATTACKER_ARG_KEY to ATTACKER_URL)) - ) + putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, arrayListOf(bundleOf(LOCATION_KEY to ATTACKER_URL))) } activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() host.activity = activity host.ensureDeeplinkStartLocationValid() - val survivingArgs = activity.intent - .getParcelableArrayListExtra(DEEPLINK_ARGS_KEY) - ?.mapNotNull { it.getString(ATTACKER_ARG_KEY) } - .orEmpty() + val survivingArgs = activity.intent.getParcelableArrayListExtra(DEEPLINK_ARGS_KEY) + ?.mapNotNull { it.getString(LOCATION_KEY) }.orEmpty() assertThat(survivingArgs).doesNotContain(ATTACKER_URL) } + // EXTRA_REFERRER is attacker-settable, so an intent carrying one is not trusted even when it + // names our own package — it still gets sanitized. + @Test + fun `treats a spoofed-referrer intent as untrusted`() { + val intent = Intent().apply { + putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://other.com/path")) + } + activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() + activity.intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse("android-app://${activity.packageName}")) + + host.activity = activity + host.ensureDeeplinkStartLocationValid() + + assertThat(activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)?.getString(LOCATION_KEY)) + .isEqualTo("https://example.com/start") + } + companion object { - private const val DEEPLINK_ARGS_KEY = "android-support-nav:controller:deepLinkArgs" private const val ATTACKER_URL = "https://attacker.example/steal" - private const val ATTACKER_ARG_KEY = "key_docs_and_files_api_url" } private class TestActivity : HotwireActivity() { From 8b2faded13173860f62a4c6074b34c021949e9c2 Mon Sep 17 00:00:00 2001 From: Milan Barta Date: Thu, 2 Jul 2026 17:58:37 +0200 Subject: [PATCH 3/3] Remove the intent trust gate and sanitize unconditionally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Origin signals are too weak to gate on: callingPackage is only set for startActivityForResult launches, and Activity.referrer is backed by the attacker-settable EXTRA_REFERRER. Sanitizing every launching intent is simpler and costs nothing — same-host start locations still pass through, and app-produced deep links keep working. Co-Authored-By: Claude Fable 5 --- .../navigation/navigator/NavigatorHost.kt | 18 ++----- .../navigation/navigator/NavigatorHostTest.kt | 49 ++----------------- 2 files changed, 8 insertions(+), 59 deletions(-) diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt index ad2f3291..2fc433c6 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt @@ -1,6 +1,5 @@ package dev.hotwire.navigation.navigator -import android.content.Intent import android.os.Bundle import android.view.View import androidx.annotation.VisibleForTesting @@ -75,14 +74,13 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener { /** * Google's Navigation library automatically navigates to deep links provided in the launching - * Intent, which lets a malicious Intent open an arbitrary page in the WebView. Intents the app - * produced itself are trusted; any other Intent has its attacker-controllable deep-link - * arguments sanitized so the start location stays within the app's domain. + * Intent, which lets a malicious Intent open an arbitrary page in the WebView. Sanitize the + * Intent's attacker-controllable deep-link arguments so the start location stays within the + * app's domain. */ @VisibleForTesting(otherwise = PROTECTED) fun ensureDeeplinkStartLocationValid() { val intent = activity.intent - if (shouldTrustIntent(intent)) return // NavController merges deepLinkArgs over the validated deepLinkExtras (last write wins), so // empty each per-destination bundle to stop it overriding the validated start location. @@ -102,16 +100,6 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener { } } - private fun shouldTrustIntent(intent: Intent): Boolean { - // EXTRA_REFERRER / EXTRA_REFERRER_NAME back Activity.referrer and are attacker-settable, so a - // self-origin claim that relies on them can't be trusted. - if (intent.hasExtra(Intent.EXTRA_REFERRER) || intent.hasExtra(Intent.EXTRA_REFERRER_NAME)) { - return false - } - val caller = activity.callingPackage ?: activity.referrer?.authority - return caller == activity.packageName - } - private val configuration get() = activity.navigatorConfigurations().firstOrNull { id == it.navigatorHostId } ?: throw IllegalStateException("No configuration found for NavigatorHost") diff --git a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt index aca4afa3..6165ae6e 100644 --- a/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt +++ b/navigation-fragments/src/test/kotlin/dev/hotwire/navigation/navigator/NavigatorHostTest.kt @@ -1,7 +1,6 @@ package dev.hotwire.navigation.navigator import android.content.Intent -import android.net.Uri import android.os.Bundle import androidx.core.os.bundleOf import dev.hotwire.navigation.activities.HotwireActivity @@ -11,7 +10,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows @RunWith(RobolectricTestRunner::class) class NavigatorHostTest { @@ -25,27 +23,7 @@ class NavigatorHostTest { } @Test - fun `does not modify a trusted self-originated intent`() { - val intent = Intent().apply { - putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://other.com/path")) - putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, arrayListOf(bundleOf(LOCATION_KEY to ATTACKER_URL))) - } - activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() - Shadows.shadowOf(activity).setCallingPackage(activity.packageName) - - host.activity = activity - host.ensureDeeplinkStartLocationValid() - - // Trusted intents pass through untouched — neither the off-host location nor the args change. - assertThat(activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)?.getString(LOCATION_KEY)) - .isEqualTo("https://other.com/path") - val args = activity.intent.getParcelableArrayListExtra(DEEPLINK_ARGS_KEY) - ?.mapNotNull { it.getString(LOCATION_KEY) }.orEmpty() - assertThat(args).contains(ATTACKER_URL) - } - - @Test - fun `reverts off-host start location for an untrusted intent`() { + fun `reverts to config start location when deep link host differs`() { val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://other.com/path")) } @@ -59,7 +37,7 @@ class NavigatorHostTest { } @Test - fun `keeps same-host start location for an untrusted intent`() { + fun `does not change start location when deep link host matches config`() { val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/path")) } @@ -72,10 +50,10 @@ class NavigatorHostTest { .isEqualTo("https://example.com/path") } - // NavController merges deepLinkArgs over deepLinkExtras (last write wins); an untrusted intent's - // args must not survive to override the validated start location. + // NavController merges deepLinkArgs over deepLinkExtras (last write wins); the intent's args + // must not survive to override the validated start location. @Test - fun `empties deepLinkArgs for an untrusted intent`() { + fun `empties deepLinkArgs so they cannot override the start location`() { val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/ok")) putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, arrayListOf(bundleOf(LOCATION_KEY to ATTACKER_URL))) @@ -90,23 +68,6 @@ class NavigatorHostTest { assertThat(survivingArgs).doesNotContain(ATTACKER_URL) } - // EXTRA_REFERRER is attacker-settable, so an intent carrying one is not trusted even when it - // names our own package — it still gets sanitized. - @Test - fun `treats a spoofed-referrer intent as untrusted`() { - val intent = Intent().apply { - putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://other.com/path")) - } - activity = Robolectric.buildActivity(TestActivity::class.java, intent).get() - activity.intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse("android-app://${activity.packageName}")) - - host.activity = activity - host.ensureDeeplinkStartLocationValid() - - assertThat(activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)?.getString(LOCATION_KEY)) - .isEqualTo("https://example.com/start") - } - companion object { private const val ATTACKER_URL = "https://attacker.example/steal" }