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 rule flags the files-path element in filepaths.xml:

  • path="." is an overly broad scope that grants URI-grant access to every file under filesDir, not just the intended reports/ subdirectory. Any app that receives a URI grant from the victim's ShareReportActivity can request any filename — including sensitive files such as session_token.txt.

Additionally, ShareReportActivity is declared with android:exported="true" in the AndroidManifest, meaning any external app can send it a crafted intent with an arbitrary file_name extra and receive back a valid content:// URI.

This attack cannot be reproduced with adb alone. adb shell am start can launch the exported activity, but it never receives the returned result intent or the temporary URI permission grant, so it can't read the file — and reading it via su -c 'content read …' only works because root can read any app's storage directly, which bypasses the provider rather than exploiting it. Demonstrating the real vulnerability therefore requires a separate attacker app that calls the activity with startActivityForResult() and reads the granted content:// URI.