Sanitize donations webview intents.

Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
Alex Hart
2026-04-17 16:08:53 -03:00
committed by jeffrey-signal
parent 53023517b3
commit 91f73b473f
3 changed files with 159 additions and 5 deletions

View File

@@ -10,11 +10,13 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.signal.donations.StripeApi
import org.thoughtcrime.securesms.R
import java.net.URISyntaxException
/**
* Encapsulates the logic for navigating a user to a deeplink from within a webview or parsing out the fallback
@@ -30,18 +32,31 @@ object ExternalNavigationHelper {
return false
}
val intent = try {
Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME).sanitizeWebIntent()
} catch (e: URISyntaxException) {
Log.w(TAG, "Failed to parse web intent URI.", e)
return false
}
val targetLabel = resolveTargetLabel(context, intent)
val message = if (targetLabel != null) {
context.getString(R.string.ExternalNavigationHelper__once_payment_confirmed_in_app, targetLabel)
} else {
context.getString(R.string.ExternalNavigationHelper__once_this_payment_is_confirmed)
}
MaterialAlertDialogBuilder(context)
.setTitle(R.string.ExternalNavigationHelper__leave_signal_to_confirm_payment)
.setMessage(R.string.ExternalNavigationHelper__once_this_payment_is_confirmed)
.setPositiveButton(android.R.string.ok) { _, _ -> attemptIntentLaunch(context, url, launchIntent) }
.setMessage(message)
.setPositiveButton(android.R.string.ok) { _, _ -> attemptIntentLaunch(context, intent, launchIntent) }
.setNegativeButton(android.R.string.cancel, null)
.show()
return true
}
private fun attemptIntentLaunch(context: Context, url: Uri, launchIntent: (Intent) -> Unit) {
val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME)
private fun attemptIntentLaunch(context: Context, intent: Intent, launchIntent: (Intent) -> Unit) {
try {
launchIntent(intent)
} catch (e: ActivityNotFoundException) {
@@ -50,7 +65,7 @@ object ExternalNavigationHelper {
val fallback = intent.getStringExtra("browser_fallback_url")
if (fallback.isNotNullOrBlank()) {
try {
launchIntent(Intent.parseUri(fallback, Intent.URI_INTENT_SCHEME))
launchIntent(Intent.parseUri(fallback, Intent.URI_INTENT_SCHEME).sanitizeWebIntent())
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "Failed to launch fallback URL.", e)
toastOnActivityNotFound(context)
@@ -59,6 +74,30 @@ object ExternalNavigationHelper {
}
}
private fun resolveTargetLabel(context: Context, intent: Intent): CharSequence? {
val resolveInfo = context.packageManager.resolveActivity(intent, 0) ?: return null
return resolveInfo.loadLabel(context.packageManager).toString().takeIf { it.isNotBlank() }
}
/**
* Sanitize an intent parsed from a web-originated URI to prevent targeting
* non-exported or internal activities. This mirrors the sanitization that
* browsers apply to intent:// URIs before dispatching them.
*/
@VisibleForTesting
fun Intent.sanitizeWebIntent(): Intent {
component = null
selector = null
addCategory(Intent.CATEGORY_BROWSABLE)
flags = flags and (
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
).inv()
return this
}
private fun toastOnActivityNotFound(context: Context) {
Toast.makeText(context, R.string.CommunicationActions_no_browser_found, Toast.LENGTH_SHORT).show()
}