CVE-2022-39349: Exposure of Resource to Wrong Sphere (CWE-668)
value: The Tasks.org Android app is an open-source app for to-do lists and reminders. The Tasks.org app uses the activity `ShareLinkActivity.kt` to handle "share" intents coming from other components in the same device and convert them to tasks. Those intents may contain arbitrary file paths as attachments, in which case the files pointed by those paths are copied in the app's external storage directory. Prior to versions 12.7.1 and 13.0.1, those paths were not validated, allowing a malicious or compromised application in the same device to force Tasks.org to copy files from its internal storage to its external storage directory, where they became accessible to any component with permission to read the external storage. This vulnerability can lead to sensitive information disclosure. All information in the user's notes and the app's preferences, including the encrypted credentials of CalDav integrations if enabled, could be accessed by third party applications installed on the same device. This issue was fixed in versions 12.7.1 and 13.0.1. There are no known workarounds.
CWE description: The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
CVSS v3 base score: 5.5
Published: 2022-10-25T17:15Z
Repository: https://github.com/tasks/tasks
Language: Kotlin
Files changed: CHANGELOG.md, app/build.gradle.kts, app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt, app/src/main/java/org/tasks/files/FileHelper.kt, fastlane/metadata/android/en-US/changelogs/120701.txt
Commit date: 2022-10-15 09:53:42 -0500
Commit message: Only accept content URIs in share activity

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index a249a05ec..ec27fc6ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
+### 12.7.1 (2022-10-04)
+
+* Bug fix
+
 ### 12.7 (2022-06-18)
 
 * Android 13 themed icon - Thanks @hanthor!
 * Fix self-signed SSL certificates on Android 12+
 * Don't hide empty tags and places in pickers
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fcb062424..ce5cc6fdc 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -54,12 +54,12 @@ android {
     compileSdk = Versions.compileSdk
 
     defaultConfig {
         testApplicationId = "org.tasks.test"
         applicationId = "org.tasks"
-        versionCode = 120700
-        versionName = "12.7"
+        versionCode = 120701
+        versionName = "12.7.1"
         targetSdk = Versions.targetSdk
         minSdk = Versions.minSdk
         testInstrumentationRunner = "org.tasks.TestRunner"
 
         kapt {
diff --git a/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt
index e21ac3fca..9fff75711 100644
--- a/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt
+++ b/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt
@@ -1,26 +1,25 @@
 package com.todoroo.astrid.activity
 
+import android.content.ContentResolver
 import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
 import androidx.lifecycle.lifecycleScope
-import com.google.common.io.Files
 import com.todoroo.astrid.data.Task
 import com.todoroo.astrid.service.TaskCreator
+import com.todoroo.astrid.utility.Constants
 import dagger.hilt.android.AndroidEntryPoint
 import kotlinx.coroutines.launch
-import org.tasks.Strings.isNullOrEmpty
 import org.tasks.analytics.Firebase
 import org.tasks.data.TaskAttachment
 import org.tasks.files.FileHelper
 import org.tasks.injection.InjectingAppCompatActivity
 import org.tasks.intents.TaskIntents
 import org.tasks.preferences.Preferences
 import timber.log.Timber
 import javax.inject.Inject
-import kotlin.math.min
 
 /**
  * @author joshuagross
  *
  * Create a new task based on incoming links from the "share" menu
@@ -86,33 +85,28 @@ class ShareLinkActivity : InjectingAppCompatActivity() {
         val intent = TaskIntents.getEditTaskIntent(this, null, task)
         intent.putExtra(MainActivity.FINISH_AFFINITY, true)
         startActivity(intent)
     }
 
-    private fun copyAttachment(intent: Intent): ArrayList<Uri> {
-        val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) ?: return ArrayList()
-        var filename = FileHelper.getFilename(this, uri)
-        if (isNullOrEmpty(filename)) {
-            filename = intent.getStringExtra(Intent.EXTRA_SUBJECT)
-                    ?.takeIf { it.isNotBlank() }
-                    ?.let { it.substring(0, min(it.length, FileHelper.MAX_FILENAME_LENGTH)) }
-                    ?: uri.lastPathSegment
-        }
-        val basename = Files.getNameWithoutExtension(filename!!)
-        return arrayListOf(FileHelper.copyToUri(this, preferences.attachmentsDirectory!!, uri, basename))
-    }
+    private fun copyAttachment(intent: Intent): ArrayList<Uri> =
+        intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
+            ?.let { copyAttachments(listOf(it)) }
+            ?: arrayListOf()
+
+    private fun copyMultipleAttachments(intent: Intent): ArrayList<Uri> =
+        intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
+            ?.let { copyAttachments(it) }
+            ?: arrayListOf()
 
-    private fun copyMultipleAttachments(intent: Intent): ArrayList<Uri> {
-        val result = ArrayList<Uri>()
-        val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
-        if (uris != null) {
-            for (uri in uris) {
-                result.add(FileHelper.copyToUri(this, preferences.attachmentsDirectory!!, uri))
+    private fun copyAttachments(uris: List<Uri>) =
+        uris
+            .filter {
+                it.scheme == ContentResolver.SCHEME_CONTENT
+                        && it.authority != Constants.FILE_PROVIDER_AUTHORITY
             }
-        }
-        return result
-    }
+            .map { FileHelper.copyToUri(this, preferences.attachmentsDirectory!!, it) }
+            .let { ArrayList(it) }
 
     private fun hasAttachments(intent: Intent) =
         intent.type?.let { type -> ATTACHMENT_TYPES.any { type.startsWith(it) } } ?: false
 
     companion object {
diff --git a/app/src/main/java/org/tasks/files/FileHelper.kt b/app/src/main/java/org/tasks/files/FileHelper.kt
index c15c38a45..5e9257d48 100644
--- a/app/src/main/java/org/tasks/files/FileHelper.kt
+++ b/app/src/main/java/org/tasks/files/FileHelper.kt
@@ -24,11 +24,10 @@ import java.io.File
 import java.io.FileNotFoundException
 import java.io.IOException
 import java.util.*
 
 object FileHelper {
-    const val MAX_FILENAME_LENGTH = 40
     fun newFilePickerIntent(activity: Activity?, initial: Uri?, vararg mimeTypes: String?): Intent =
             Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                 putExtra("android.content.extra.SHOW_ADVANCED", true)
                 putExtra("android.content.extra.FANCY", true)
                 putExtra("android.content.extra.SHOW_FILESIZE", true)
@@ -176,24 +175,24 @@ object FileHelper {
         }
     }
 
     fun copyToUri(context: Context, destination: Uri, input: Uri): Uri {
         val filename = getFilename(context, input)
-        return copyToUri(context, destination, input, Files.getNameWithoutExtension(filename!!))
-    }
-
-    fun copyToUri(context: Context, destination: Uri, input: Uri, basename: String): Uri = try {
-        val output = newFile(
+        val basename = Files.getNameWithoutExtension(filename!!)
+        try {
+            val output = newFile(
                 context,
                 destination,
                 getMimeType(context, input),
                 basename,
-                getExtension(context, input))
-        copyStream(context, input, output)
-        output
-    } catch (e: IOException) {
-        throw IllegalStateException(e)
+                getExtension(context, input)
+            )
+            copyStream(context, input, output)
+            return output
+        } catch (e: IOException) {
+            throw IllegalStateException(e)
+        }
     }
 
     fun copyStream(context: Context, input: Uri?, output: Uri?) {
         val contentResolver = context.contentResolver
         try {
diff --git a/fastlane/metadata/android/en-US/changelogs/120701.txt b/fastlane/metadata/android/en-US/changelogs/120701.txt
new file mode 100644
index 000000000..911511a20
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/120701.txt
@@ -0,0 +1 @@
+* Bug fix

Vulnerable code:         versionCode = 120700
        versionName = "12.7"
import com.google.common.io.Files
import org.tasks.Strings.isNullOrEmpty
import kotlin.math.min
    private fun copyAttachment(intent: Intent): ArrayList<Uri> {
        val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) ?: return ArrayList()
        var filename = FileHelper.getFilename(this, uri)
        if (isNullOrEmpty(filename)) {
            filename = intent.getStringExtra(Intent.EXTRA_SUBJECT)
                    ?.takeIf { it.isNotBlank() }
                    ?.let { it.substring(0, min(it.length, FileHelper.MAX_FILENAME_LENGTH)) }
                    ?: uri.lastPathSegment
        }
        val basename = Files.getNameWithoutExtension(filename!!)
        return arrayListOf(FileHelper.copyToUri(this, preferences.attachmentsDirectory!!, uri, basename))
    }
    private fun copyMultipleAttachments(intent: Intent): ArrayList<Uri> {
        val result = ArrayList<Uri>()
        val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
        if (uris != null) {
            for (uri in uris) {
                result.add(FileHelper.copyToUri(this, preferences.attachmentsDirectory!!, uri))
        }
        return result
    }
    const val MAX_FILENAME_LENGTH = 40
        return copyToUri(context, destination, input, Files.getNameWithoutExtension(filename!!))
    }

    fun copyToUri(context: Context, destination: Uri, input: Uri, basename: String): Uri = try {
        val output = newFile(
                getExtension(context, input))
        copyStream(context, input, output)
        output
    } catch (e: IOException) {
        throw IllegalStateException(e)
Fixed code: ### 12.7.1 (2022-10-04)

* Bug fix

        versionCode = 120701
        versionName = "12.7.1"
import android.content.ContentResolver
import com.todoroo.astrid.utility.Constants
    private fun copyAttachment(intent: Intent): ArrayList<Uri> =
        intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
            ?.let { copyAttachments(listOf(it)) }
            ?: arrayListOf()

    private fun copyMultipleAttachments(intent: Intent): ArrayList<Uri> =
        intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
            ?.let { copyAttachments(it) }
            ?: arrayListOf()
    private fun copyAttachments(uris: List<Uri>) =
        uris
            .filter {
                it.scheme == ContentResolver.SCHEME_CONTENT
                        && it.authority != Constants.FILE_PROVIDER_AUTHORITY
            .map { FileHelper.copyToUri(this, preferences.attachmentsDirectory!!, it) }
            .let { ArrayList(it) }
        val basename = Files.getNameWithoutExtension(filename!!)
        try {
            val output = newFile(
                getExtension(context, input)
            )
            copyStream(context, input, output)
            return output
        } catch (e: IOException) {
            throw IllegalStateException(e)
        }
* Bug fix