Skip to content

Commit

Permalink
Merge pull request #4498 from vector-im/yostyle/fix_strandhogg
Browse files Browse the repository at this point in the history
Override task affinity to prevent unknown activities running in our app tasks.
  • Loading branch information
yostyle authored Mar 3, 2022
1 parent 7a1322b commit 6e6b04c
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 13 deletions.
1 change: 1 addition & 0 deletions changelog.d/4498.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Override task affinity to prevent unknown activities running in our app tasks.
3 changes: 3 additions & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()}\""
Expand Down
3 changes: 2 additions & 1 deletion vector/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">

<!-- No limit for screen ratio: avoid black strips -->
Expand Down Expand Up @@ -294,7 +295,7 @@
android:excludeFromRecents="true"
android:launchMode="singleTask"
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 -->
<activity
android:name=".features.call.conference.VectorJitsiActivity"
Expand Down
20 changes: 19 additions & 1 deletion vector/src/main/java/im/vector/app/core/extensions/Activity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package im.vector.app.core.extensions

import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Parcelable
import android.view.ViewGroup
import android.view.WindowManager
Expand All @@ -29,6 +30,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import im.vector.app.R
import timber.log.Timber

fun ComponentActivity.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): ActivityResultLauncher<Intent> {
return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult)
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -515,7 +516,7 @@ class HomeActivity :
if (views.drawerLayout.isDrawerOpen(GravityCompat.START)) {
views.drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
validateBackPressed { super.onBackPressed() }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActivityInfo> = 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<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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -279,6 +280,10 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) }
}

override fun onBackPressed() {
validateBackPressed { super.onBackPressed() }
}

private fun onRegistrationStageNotSupported() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.app_name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,6 +47,10 @@ class OnboardingActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
onboardingVariant.onNewIntent(intent)
}

override fun onBackPressed() {
validateBackPressed { super.onBackPressed() }
}

override fun initUiAndData() {
onboardingVariant.initUiAndData(isFirstCreation())
}
Expand Down

0 comments on commit 6e6b04c

Please sign in to comment.