From 6e6b04c57e490b1ba3b187b5f5175ff4eeb09aff Mon Sep 17 00:00:00 2001 From: Yoan Pintas Date: Thu, 3 Mar 2022 15:05:29 +0100 Subject: [PATCH] Merge pull request #4498 from vector-im/yostyle/fix_strandhogg Override task affinity to prevent unknown activities running in our app tasks. --- changelog.d/4498.misc | 1 + vector/build.gradle | 3 + vector/src/main/AndroidManifest.xml | 3 +- .../im/vector/app/core/extensions/Activity.kt | 20 ++- .../vector/app/features/home/HomeActivity.kt | 3 +- .../VectorActivityLifecycleCallbacks.kt | 117 ++++++++++++++++-- .../app/features/login/LoginActivity.kt | 5 + .../features/onboarding/OnboardingActivity.kt | 5 + 8 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 changelog.d/4498.misc diff --git a/changelog.d/4498.misc b/changelog.d/4498.misc new file mode 100644 index 0000000000..78493b5d77 --- /dev/null +++ b/changelog.d/4498.misc @@ -0,0 +1 @@ +Override task affinity to prevent unknown activities running in our app tasks. \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index 676b84839f..c4c9436e13 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -131,6 +131,9 @@ android { // Required for sonar analysis versionName "${versionMajor}.${versionMinor}.${versionPatch}-sonar" + // Generate a random app task affinity + manifestPlaceholders = [appTaskAffinitySuffix:"H_${gitRevision()}"] + buildConfigField "String", "GIT_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_REVISION_DATE", "\"${gitRevisionDate()}\"" buildConfigField "String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"" diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 2e5412870f..58b1bc177c 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -84,6 +84,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Vector.Light" + android:taskAffinity="${applicationId}.${appTaskAffinitySuffix}" tools:replace="android:allowBackup"> @@ -294,7 +295,7 @@ android:excludeFromRecents="true" android:launchMode="singleTask" android:supportsPictureInPicture="true" - android:taskAffinity=".features.call.VectorCallActivity" /> + android:taskAffinity=".features.call.VectorCallActivity.${appTaskAffinitySuffix}" /> Unit): ActivityResultLauncher { return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult) @@ -114,9 +116,25 @@ fun AppCompatActivity.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() { - startActivity(intent) finish() + startActivity(intent) } fun Activity.keepScreenOn() { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 47f1a9208b..964fb6f365 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -42,6 +42,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult 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.pushers.PushersManager import im.vector.app.databinding.ActivityHomeBinding @@ -515,7 +516,7 @@ class HomeActivity : if (views.drawerLayout.isDrawerOpen(GravityCompat.START)) { views.drawerLayout.closeDrawer(GravityCompat.START) } else { - super.onBackPressed() + validateBackPressed { super.onBackPressed() } } } diff --git a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt index 084dd6349a..386e60359d 100644 --- a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt +++ b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt @@ -16,31 +16,128 @@ package im.vector.app.features.lifecycle +import android.annotation.SuppressLint import android.app.Activity +import android.app.ActivityManager 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 androidx.core.content.getSystemService +import im.vector.app.features.MainActivity +import im.vector.app.features.MainActivityArgs 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 { - override fun onActivityPaused(activity: Activity) { - } + /** + * The activities information collected from the app manifest. + */ + private var activitiesInfo: Array = emptyArray() + + private val coroutineScope = CoroutineScope(SupervisorJob()) + + override fun onActivityPaused(activity: Activity) {} override fun onActivityResumed(activity: 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?) { + // 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() ?: 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 } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index bf596fc6aa..a40f26acec 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -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.addFragmentToBackstack 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.databinding.ActivityLoginBinding import im.vector.app.features.analytics.plan.MobileScreen @@ -279,6 +280,10 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } } + override fun onBackPressed() { + validateBackPressed { super.onBackPressed() } + } + private fun onRegistrationStageNotSupported() { MaterialAlertDialogBuilder(this) .setTitle(R.string.app_name) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt index 4165d4cb65..5a19732341 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.net.Uri import dagger.hilt.android.AndroidEntryPoint 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.lifecycleAwareLazy import im.vector.app.databinding.ActivityLoginBinding @@ -46,6 +47,10 @@ class OnboardingActivity : VectorBaseActivity(), UnlockedA onboardingVariant.onNewIntent(intent) } + override fun onBackPressed() { + validateBackPressed { super.onBackPressed() } + } + override fun initUiAndData() { onboardingVariant.initUiAndData(isFirstCreation()) }