Skip to content

Commit b1da9af

Browse files
authored
Merge pull request #154 from hotwired/mb/start_location_vulnerability_fix
Navigation: deeplink start location vulnerability fix
2 parents 7c72e6e + 52c2cf7 commit b1da9af

File tree

2 files changed

+89
-0
lines changed

2 files changed

+89
-0
lines changed

navigation-fragments/src/main/java/dev/hotwire/navigation/navigator/NavigatorHost.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package dev.hotwire.navigation.navigator
22

33
import android.os.Bundle
44
import android.view.View
5+
import androidx.annotation.VisibleForTesting
6+
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
7+
import androidx.core.net.toUri
58
import androidx.fragment.app.Fragment
69
import androidx.fragment.app.FragmentManager
710
import androidx.fragment.app.FragmentOnAttachListener
@@ -11,6 +14,9 @@ import dev.hotwire.core.config.Hotwire
1114
import dev.hotwire.navigation.activities.HotwireActivity
1215
import dev.hotwire.navigation.config.HotwireNavigation
1316

17+
internal const val DEEPLINK_EXTRAS_KEY = "android-support-nav:controller:deepLinkExtras"
18+
internal const val LOCATION_KEY = "location"
19+
1420
open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener {
1521
internal lateinit var activity: HotwireActivity
1622
lateinit var navigator: Navigator
@@ -51,6 +57,8 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener {
5157
}
5258

5359
internal fun initControllerGraph() {
60+
ensureDeeplinkStartLocationValid()
61+
5462
navController.apply {
5563
graph = NavigatorGraphBuilder(
5664
navigatorName = configuration.name,
@@ -63,6 +71,26 @@ open class NavigatorHost : NavHostFragment(), FragmentOnAttachListener {
6371
}
6472
}
6573

74+
/**
75+
* Google's Navigation library automatically navigates to deep links provided in the
76+
* Activity's Intent. This exposes a vulnerability for malicious Intents to open an arbitrary
77+
* webpage outside of the app's domain, allowing javascript injection on the page. Ensure
78+
* that deep link intents always match the app's domain.
79+
*/
80+
@VisibleForTesting(otherwise = PROTECTED)
81+
fun ensureDeeplinkStartLocationValid() {
82+
val extrasBundle = activity.intent.extras?.getBundle(DEEPLINK_EXTRAS_KEY) ?: return
83+
val startLocation = extrasBundle.getString(LOCATION_KEY) ?: return
84+
85+
val deepLinkStartUri = startLocation.toUri()
86+
val configStartUri = configuration.startLocation.toUri()
87+
88+
if (deepLinkStartUri.host != configStartUri.host) {
89+
extrasBundle.putString(LOCATION_KEY, configuration.startLocation)
90+
activity.intent.putExtra(DEEPLINK_EXTRAS_KEY, extrasBundle)
91+
}
92+
}
93+
6694
private val configuration get() = activity.navigatorConfigurations().firstOrNull {
6795
id == it.navigatorHostId
6896
} ?: throw IllegalStateException("No configuration found for NavigatorHost")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package dev.hotwire.navigation.navigator
2+
3+
import android.R.attr.host
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.core.os.bundleOf
7+
import dev.hotwire.navigation.activities.HotwireActivity
8+
import org.assertj.core.api.Assertions.assertThat
9+
import org.junit.Before
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.robolectric.Robolectric
13+
import org.robolectric.RobolectricTestRunner
14+
15+
@RunWith(RobolectricTestRunner::class)
16+
class NavigatorHostTest {
17+
18+
private lateinit var activity: TestActivity
19+
private lateinit var host: NavigatorHost
20+
21+
@Before
22+
fun setUp() {
23+
host = NavigatorHost()
24+
}
25+
26+
@Test
27+
fun `reverts to config start location when deep link host differs`() {
28+
val extras = bundleOf(LOCATION_KEY to "https://other.com/path")
29+
val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, extras) }
30+
activity = Robolectric.buildActivity(TestActivity::class.java, intent).get()
31+
32+
host.activity = activity
33+
host.ensureDeeplinkStartLocationValid()
34+
35+
val resultBundle = activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)
36+
assertThat(resultBundle?.getString(LOCATION_KEY)).isEqualTo("https://example.com/start")
37+
}
38+
39+
@Test
40+
fun `does not change start location when deep link host matches config`() {
41+
val extras = bundleOf(LOCATION_KEY to "https://example.com/path")
42+
val intent = Intent().apply { putExtra(DEEPLINK_EXTRAS_KEY, extras) }
43+
activity = Robolectric.buildActivity(TestActivity::class.java, intent).get()
44+
45+
host.activity = activity
46+
host.ensureDeeplinkStartLocationValid()
47+
48+
val resultBundle = activity.intent.getBundleExtra(DEEPLINK_EXTRAS_KEY)
49+
assertThat(resultBundle?.getString(LOCATION_KEY)).isEqualTo("https://example.com/path")
50+
}
51+
52+
private class TestActivity : HotwireActivity() {
53+
private val navConfig = NavigatorConfiguration(
54+
name = "test",
55+
startLocation = "https://example.com/start",
56+
navigatorHostId = 0
57+
)
58+
59+
override fun navigatorConfigurations(): List<NavigatorConfiguration> = listOf(navConfig)
60+
}
61+
}

0 commit comments

Comments
 (0)