Skip to content
Last updated: July 10, 2024

MASTG-DEMO-0003: App Writing to External Storage without Scoped Storage Restrictions

Content in BETA

This content is in beta and still under active development, so it is subject to change any time (e.g. structure, IDs, content, URLs, etc.).

Send Feedback

Sample

The snippet below shows sample code that creates a file in external storage without using scoped storage APIs. The getExternalStorageDirectory API returns a path to the root of the shared external storage (e.g. /storage/emulated/0).

This requires special app access called "All files access", so the MANAGE_EXTERNAL_STORAGE permission must be declared in the manifest file.

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

import android.content.Context
import android.os.Environment
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

class MastgTest (private val context: Context){

    fun mastgTest(): String {

        val externalStorageDir = Environment.getExternalStorageDirectory()

        val fileName = File(externalStorageDir, "secret.txt")
        val fileContent = "Secret not using scoped storage"

        try {
            FileOutputStream(fileName).use { output ->
                output.write(fileContent.toByteArray())
                Log.d("WriteExternalStorage", "File written to external storage successfully.")
            }
        } catch (e: IOException) {
            Log.e("WriteExternalStorage", "Error writing file to external storage", e)
            return "ERROR!!\n\nError writing file to external storage. Do you have the MANAGE_EXTERNAL_STORAGE permission in the manifest and it's granted in 'All files access'?"
        }

        return "SUCCESS!!\n\nFile $fileName with content $fileContent saved to $externalStorageDir"
    }
}
 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
package org.owasp.mastestapp;

import android.content.Context;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import kotlin.Metadata;
import kotlin.io.CloseableKt;
import kotlin.jvm.internal.Intrinsics;
import kotlin.text.Charsets;

/* compiled from: MastgTest.kt */
@Metadata(d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\u0006\u0010\u0005\u001a\u00020\u0006R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u0007"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "(Landroid/content/Context;)V", "mastgTest", "", "app_debug"}, k = 1, mv = {1, 9, 0}, xi = 48)
/* loaded from: classes4.dex */
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() {
        File externalStorageDir = Environment.getExternalStorageDirectory();
        File fileName = new File(externalStorageDir, "secret.txt");
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(fileName);
            try {
                FileOutputStream output = fileOutputStream;
                byte[] bytes = "Secret not using scoped storage".getBytes(Charsets.UTF_8);
                Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
                output.write(bytes);
                Log.d("WriteExternalStorage", "File written to external storage successfully.");
                CloseableKt.closeFinally(fileOutputStream, null);
                return "File " + fileName + " with content Secret not using scoped storage saved to " + externalStorageDir;
            } finally {
            }
        } catch (IOException e) {
            Log.e("WriteExternalStorage", "Error writing file to external storage", e);
            return "Error writing file to external storage. Do you have the MANAGE_EXTERNAL_STORAGE permission in the manifest and it's granted?";
        }
    }
}
 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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

    <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:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </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
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="34" android:compileSdkVersionCodename="14" package="org.owasp.mastestapp" platformBuildVersionCode="34" platformBuildVersionName="14">
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    <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:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:debuggable="true" android:extractNativeLibs="false" 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:testOnly="true" android:theme="@style/Theme.MASTestApp">
        <activity android:exported="true" android:name="org.owasp.mastestapp.MainActivity" 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:exported="true" android:name="androidx.compose.ui.tooling.PreviewActivity"/>
        <activity android:exported="true" android:name="androidx.activity.ComponentActivity"/>
        <provider android:authorities="org.owasp.mastestapp.androidx-startup" android:exported="false" android:name="androidx.startup.InitializationProvider">
            <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:directBootAware="false" android:enabled="true" android:exported="true" android:name="androidx.profileinstaller.ProfileInstallReceiver" android:permission="android.permission.DUMP">
            <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>

Steps

Let's run our semgrep rule against the reversed java code.

../../../../rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-apis.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
rules:
  - id: mastg-android-data-unencrypted-shared-storage-no-user-interaction-external-api-public
    severity: WARNING
    languages:
      - java
    metadata:
      summary: This rule looks for methods that returns locations to "external storage" which is shared with other apps
    message: "[MASVS-STORAGE] Make sure to encrypt files at these locations if necessary"
    pattern-either:
      - pattern: $X.getExternalStorageDirectory(...)
      - pattern: $X.getExternalStoragePublicDirectory(...)
      - pattern: $X.getDownloadCacheDirectory(...)
      - pattern: Intent.ACTION_CREATE_DOCUMENT
  - id: mastg-android-data-unencrypted-shared-storage-no-user-interaction-external-api-scoped
    severity: WARNING
    languages:
      - java
    metadata:
      summary: This rule looks for methods that returns locations to "scoped external storage"
    message: "[MASVS-STORAGE] These locations might be accessible to other apps on Android 10 and below given relevant permissions"
    pattern-either:
      - pattern: $X.getExternalFilesDir(...)
      - pattern: $X.getExternalFilesDirs(...)
      - pattern: $X.getExternalCacheDir(...)
      - pattern: $X.getExternalCacheDirs(...)
      - pattern: $X.getExternalMediaDirs(...)
  - id: mastg-android-data-unencrypted-shared-storage-no-user-interaction-mediastore
    severity: WARNING
    languages:
      - java
    metadata:
      summary: This rule scans for uses of MediaStore API that writes data to the external storage. This data can be accessed by other apps.
    message: "[MASVS-STORAGE] Make sure to want this data to be shared with other apps"
    pattern-either:
      - pattern: import android.provider.MediaStore
      - pattern: $X.MediaStore

And another one against the sample manifest file.

../../../../rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-manifest.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
rules:
  - id: mastg-android-data-unencrypted-shared-storage-no-user-interaction-manifest
    severity: WARNING
    languages:
      - generic
    metadata:
      summary: This rule scans for permissions that allows your app to write to external storage or shared storage
    message: "[MASVS-STORAGE] Make sure to encrypt files in external storage if necessary"
    pattern-either:
      - pattern: WRITE_EXTERNAL_STORAGE
      - pattern: MANAGE_EXTERNAL_STORAGE
      - pattern: ACCESS_ALL_EXTERNAL_STORAGE
      - pattern: requestLegacyExternalStorage="true"
      - pattern: preserveLegacyExternalStorage="true"
      - pattern: android:requestRawExternalStorageAccess="true"
run.sh
1
2
3
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-apis.yml ./MastgTest_reversed.java --text -o output.txt

NO_COLOR=true semgrep -c ../../../../rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-manifest.yml ./AndroidManifest_reversed.xml --text -o output2.txt

Observation

The rule has identified one location in the code file where an API, getExternalStorageDirectory, is used to write to external storage as well as the location in the manifest file where the MANAGE_EXTERNAL_STORAGE permission is declared.

1
2
3
4
5
6
7
8
9
┌────────────────┐
│ 1 Code Finding │
└────────────────┘

    MastgTest_reversed.java 
       rules.mastg-android-data-unencrypted-shared-storage-no-user-interaction-external-api-public
          [MASVS-STORAGE] Make sure to encrypt files at these locations if necessary              

           27┆ File externalStorageDir = Environment.getExternalStorageDirectory();
1
2
3
4
5
6
7
8
9
┌────────────────┐
│ 1 Code Finding │
└────────────────┘

    AndroidManifest_reversed.xml 
       rules.mastg-android-data-unencrypted-shared-storage-no-user-interaction-manifest
          [MASVS-STORAGE] Make sure to encrypt files in external storage if necessary  

            2┆ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

Evaluation

After reviewing the decompiled code at the location specified in the output (file and line number) we can conclude that the test fails because the file written by this instance contains sensitive data, specifically a password.