Merge pull request #4498 from vector-im/yostyle/fix_strandhogg

Override task affinity to prevent unknown activities running in our app tasks.
This commit is contained in:
Yoan Pintas 2022-03-03 15:05:29 +01:00 committed by GitHub
parent 7a1322baf7
commit 6e6b04c57e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 13 deletions

1
changelog.d/4498.misc Normal file
View File

@ -0,0 +1 @@
Override task affinity to prevent unknown activities running in our app tasks.

View File

@ -131,6 +131,9 @@ android {
// Required for sonar analysis // Required for sonar analysis
versionName "${versionMajor}.${versionMinor}.${versionPatch}-sonar" versionName "${versionMajor}.${versionMinor}.${versionPatch}-sonar"
// Generate a random app task affinity
manifestPlaceholders = [appTaskAffinitySuffix:"H_${gitRevision()}"]
buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_REVISION_DATE", "\"${gitRevisionDate()}\"" buildConfigField "String", "GIT_REVISION_DATE", "\"${gitRevisionDate()}\""
buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"" buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\""

View File

@ -84,6 +84,7 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Vector.Light" android:theme="@style/Theme.Vector.Light"
android:taskAffinity="${applicationId}.${appTaskAffinitySuffix}"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<!-- No limit for screen ratio: avoid black strips --> <!-- No limit for screen ratio: avoid black strips -->
@ -294,7 +295,7 @@
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
android:taskAffinity=".features.call.VectorCallActivity" /> android:taskAffinity=".features.call.VectorCallActivity.${appTaskAffinitySuffix}" />
<!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture --> <!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture -->
<activity <activity
android:name=".features.call.conference.VectorJitsiActivity" android:name=".features.call.conference.VectorJitsiActivity"

View File

@ -18,6 +18,7 @@ package im.vector.app.core.extensions
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
@ -29,6 +30,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import im.vector.app.R import im.vector.app.R
import timber.log.Timber
fun ComponentActivity.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): ActivityResultLauncher<Intent> { fun ComponentActivity.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): ActivityResultLauncher<Intent> {
return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult) return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult)
@ -114,9 +116,25 @@ fun AppCompatActivity.hideKeyboard() {
currentFocus?.hideKeyboard() currentFocus?.hideKeyboard()
} }
/**
* The current activity must be the root of a task to call onBackPressed, otherwise finish activities with the same task affinity.
*/
fun AppCompatActivity.validateBackPressed(onBackPressed: () -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && supportFragmentManager.backStackEntryCount == 0) {
if (isTaskRoot) {
onBackPressed()
} else {
Timber.e("Application is potentially corrupted by an unknown activity")
finishAffinity()
}
} else {
onBackPressed()
}
}
fun Activity.restart() { fun Activity.restart() {
startActivity(intent)
finish() finish()
startActivity(intent)
} }
fun Activity.keepScreenOn() { fun Activity.keepScreenOn() {

View File

@ -42,6 +42,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.databinding.ActivityHomeBinding
@ -515,7 +516,7 @@ class HomeActivity :
if (views.drawerLayout.isDrawerOpen(GravityCompat.START)) { if (views.drawerLayout.isDrawerOpen(GravityCompat.START)) {
views.drawerLayout.closeDrawer(GravityCompat.START) views.drawerLayout.closeDrawer(GravityCompat.START)
} else { } else {
super.onBackPressed() validateBackPressed { super.onBackPressed() }
} }
} }

View File

@ -16,31 +16,128 @@
package im.vector.app.features.lifecycle package im.vector.app.features.lifecycle
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.content.ComponentName
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.core.content.getSystemService
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager: PopupAlertManager) : Application.ActivityLifecycleCallbacks { class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager: PopupAlertManager) : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) { /**
} * The activities information collected from the app manifest.
*/
private var activitiesInfo: Array<ActivityInfo> = emptyArray()
private val coroutineScope = CoroutineScope(SupervisorJob())
override fun onActivityPaused(activity: Activity) {}
override fun onActivityResumed(activity: Activity) { override fun onActivityResumed(activity: Activity) {
popupAlertManager.onNewActivityDisplayed(activity) popupAlertManager.onNewActivityDisplayed(activity)
} }
override fun onActivityStarted(activity: Activity) { override fun onActivityStarted(activity: Activity) {}
}
override fun onActivityDestroyed(activity: Activity) { override fun onActivityDestroyed(activity: Activity) {}
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
}
override fun onActivityStopped(activity: Activity) { override fun onActivityStopped(activity: Activity) {}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
// restart the app if the task contains an unknown activity
coroutineScope.launch {
val isTaskCorrupted = try {
isTaskCorrupted(activity)
} catch (failure: Throwable) {
when (failure) {
// The task was not found. We can ignore it.
is IllegalArgumentException -> {
Timber.e("The task was not found: ${failure.localizedMessage}")
false
}
is PackageManager.NameNotFoundException -> {
Timber.e("Package manager error: ${failure.localizedMessage}")
true
}
else -> throw failure
}
}
if (isTaskCorrupted) {
Timber.e("Application is potentially corrupted by an unknown activity")
MainActivity.restartApp(activity, MainActivityArgs())
return@launch
}
}
} }
/**
* Check if all activities running on the task with package name affinity are safe.
*
* @return true if an app task is corrupted by a potentially malicious activity
*/
@SuppressLint("NewApi")
@Suppress("DEPRECATION")
private suspend fun isTaskCorrupted(activity: Activity): Boolean = withContext(Dispatchers.Default) {
val context = activity.applicationContext
val packageManager: PackageManager = context.packageManager
// Get all activities from app manifest
if (activitiesInfo.isEmpty()) {
activitiesInfo = packageManager.getPackageInfo(context.packageName, PackageManager.GET_ACTIVITIES).activities
}
// Get all running activities on app task
// and compare to activities declared in manifest
val manager = context.getSystemService<ActivityManager>() ?: return@withContext false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android lint may return an error on topActivity field.
// This field was added in ActivityManager.RecentTaskInfo class since Android M (API level 23)
// and it is inherited from TaskInfo since Android Q (API level 29).
// API 23 changes : https://developer.android.com/sdk/api_diff/23/changes/android.app.ActivityManager.RecentTaskInfo
// API 29 changes : https://developer.android.com/sdk/api_diff/29/changes/android.app.ActivityManager.RecentTaskInfo
manager.appTasks.any { appTask ->
appTask.taskInfo.topActivity?.let { isPotentialMaliciousActivity(it) } ?: false
}
} else {
// Android lint may return an error on topActivity field.
// This was present in ActivityManager.RunningTaskInfo class since API level 1!
// and it is inherited from TaskInfo since Android Q (API level 29).
// API 29 changes : https://developer.android.com/sdk/api_diff/29/changes/android.app.ActivityManager.RunningTaskInfo
manager.getRunningTasks(10).any { runningTaskInfo ->
runningTaskInfo.topActivity?.let {
// Check whether the activity task affinity matches with app task affinity.
// The activity is considered safe when its task affinity doesn't correspond to app task affinity.
if (packageManager.getActivityInfo(it, 0).taskAffinity == context.applicationInfo.taskAffinity) {
isPotentialMaliciousActivity(it)
} else false
} ?: false
}
}
}
/**
* Detect potential malicious activity.
* Check if the activity running in app task is declared in app manifest.
*
* @param activity the activity of the task
* @return true if the activity is potentially malicious
*/
private fun isPotentialMaliciousActivity(activity: ComponentName): Boolean = activitiesInfo.none { it.name == activity.className }
} }

View File

@ -36,6 +36,7 @@ import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.databinding.ActivityLoginBinding
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
@ -279,6 +280,10 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) }
} }
override fun onBackPressed() {
validateBackPressed { super.onBackPressed() }
}
private fun onRegistrationStageNotSupported() { private fun onRegistrationStageNotSupported() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.app_name) .setTitle(R.string.app_name)

View File

@ -21,6 +21,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.lazyViewModel import im.vector.app.core.extensions.lazyViewModel
import im.vector.app.core.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.lifecycleAwareLazy
import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.databinding.ActivityLoginBinding
@ -46,6 +47,10 @@ class OnboardingActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
onboardingVariant.onNewIntent(intent) onboardingVariant.onNewIntent(intent)
} }
override fun onBackPressed() {
validateBackPressed { super.onBackPressed() }
}
override fun initUiAndData() { override fun initUiAndData() {
onboardingVariant.initUiAndData(isFirstCreation()) onboardingVariant.initUiAndData(isFirstCreation())
} }