Skip to content

Commit daceb0a

Browse files
authored
Merge pull request #355 from hotwired/mb/deeplink_args_vulnerability_fix
Validate deep link args when resolving the start location
2 parents 855b838 + 2cc12df commit daceb0a

2 files changed

Lines changed: 38 additions & 6 deletions

File tree

turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionNavHostFragment.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import dev.hotwire.turbo.views.TurboWebView
1717
import kotlin.reflect.KClass
1818

1919
internal const val DEEPLINK_EXTRAS_KEY = "android-support-nav:controller:deepLinkExtras"
20+
internal const val DEEPLINK_ARGS_KEY = "android-support-nav:controller:deepLinkArgs"
2021
internal const val LOCATION_KEY = "location"
2122

2223
abstract class TurboSessionNavHostFragment : NavHostFragment() {
@@ -114,22 +115,30 @@ abstract class TurboSessionNavHostFragment : NavHostFragment() {
114115
?: throw IllegalStateException("No current destination found in NavHostFragment")
115116

116117
/**
117-
* Google's Navigation library automatically navigates to deep links provided in the
118-
* Activity's Intent. This exposes a vulnerability for malicious Intents to open an arbitrary
119-
* webpage outside of the app's domain, allowing javascript injection on the page. Ensure
120-
* that deep link intents always match the app's domain.
118+
* Google's Navigation library automatically navigates to deep links provided in the launching
119+
* Intent, which lets a malicious Intent open an arbitrary page in the WebView. Sanitize the
120+
* Intent's attacker-controllable deep-link arguments so the start location stays within the
121+
* app's domain.
121122
*/
122123
@VisibleForTesting
123124
internal fun ensureDeeplinkStartLocationValid(activity: FragmentActivity) {
124-
val extrasBundle = activity.intent.extras?.getBundle(DEEPLINK_EXTRAS_KEY) ?: return
125+
val intent = activity.intent
126+
127+
// NavController merges deepLinkArgs over the validated deepLinkExtras (last write wins), so
128+
// empty each per-destination bundle to stop it overriding the validated start location.
129+
intent.extras?.getParcelableArrayList<Bundle>(DEEPLINK_ARGS_KEY)?.let { args ->
130+
intent.putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, ArrayList(args.map { Bundle() }))
131+
}
132+
133+
val extrasBundle = intent.extras?.getBundle(DEEPLINK_EXTRAS_KEY) ?: return
125134
val startLocationFromIntent = extrasBundle.getString(LOCATION_KEY) ?: return
126135

127136
val deepLinkStartUri = startLocationFromIntent.toUri()
128137
val configStartUri = startLocation.toUri()
129138

130139
if (deepLinkStartUri.host != configStartUri.host) {
131140
extrasBundle.putString(LOCATION_KEY, startLocation)
132-
activity.intent.putExtra(DEEPLINK_EXTRAS_KEY, extrasBundle)
141+
intent.putExtra(DEEPLINK_EXTRAS_KEY, extrasBundle)
133142
}
134143
}
135144

turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionNavHostFragmentTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dev.hotwire.turbo.session
22

33
import android.content.Intent
44
import android.os.Build
5+
import android.os.Bundle
56
import androidx.appcompat.app.AppCompatActivity
67
import androidx.core.os.bundleOf
78
import androidx.fragment.app.Fragment
@@ -54,6 +55,28 @@ class TurboSessionNavHostFragmentTest : BaseUnitTest() {
5455
assertThat(resultBundle?.getString(LOCATION_KEY)).isEqualTo("https://example.com/path")
5556
}
5657

58+
// NavController merges deepLinkArgs over deepLinkExtras (last write wins); the intent's args
59+
// must not survive to override the validated start location.
60+
@Test
61+
fun `empties deepLinkArgs so they cannot override the start location`() {
62+
val intent = Intent().apply {
63+
putExtra(DEEPLINK_EXTRAS_KEY, bundleOf(LOCATION_KEY to "https://example.com/ok"))
64+
putParcelableArrayListExtra(DEEPLINK_ARGS_KEY, arrayListOf(bundleOf(LOCATION_KEY to ATTACKER_URL)))
65+
}
66+
activity = Robolectric.buildActivity(TestActivity::class.java, intent).create().get()
67+
68+
host = TestNavHostFragment()
69+
host.ensureDeeplinkStartLocationValid(activity)
70+
71+
val survivingArgs = activity.intent.getParcelableArrayListExtra<Bundle>(DEEPLINK_ARGS_KEY)
72+
?.mapNotNull { it.getString(LOCATION_KEY) }.orEmpty()
73+
assertThat(survivingArgs).doesNotContain(ATTACKER_URL)
74+
}
75+
76+
companion object {
77+
private const val ATTACKER_URL = "https://attacker.example/steal"
78+
}
79+
5780
}
5881

5982
class TestActivity : AppCompatActivity()

0 commit comments

Comments
 (0)