Skip to content

MASTG-DEMO-0005: App Writing to External Storage via the MediaStore API

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 uses the MediaStore API to write a file to shared storage in a path like /storage/emulated/0/Download/ which does not require any permissions to access and is shared with other apps.

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

import android.content.ContentValues
import android.util.Log
import android.content.Context
import android.os.Environment
import android.provider.MediaStore
import java.io.OutputStream

class MastgTest (private val context: Context){

    fun mastgTest(): String {
        try {
            val resolver = context.contentResolver
            val contentValues = ContentValues().apply {
                put(MediaStore.MediaColumns.DISPLAY_NAME, "secretFile.txt")
                put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
                put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
            }
            val textUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)

            textUri?.let {
                val outputStream: OutputStream? = resolver.openOutputStream(it)
                outputStream?.use {
                    it.write("MAS_API_KEY=8767086b9f6f976g-a8df76\n".toByteArray())
                    it.flush()
                }
                Log.d("MediaStore", "File written to external storage successfully.")
                return  "SUCCESS!!\n\nMediaStore inserted to $textUri"
            } ?: run {
                Log.e("MediaStore", "Error inserting URI to MediaStore.")
                return  "FAILURE!!\n\nMediaStore couldn't insert data."
            }
        } catch (exception: Exception) {
            Log.e("MediaStore", "Error writing file to URI from MediaStore", exception)
            return "FAILURE!!\n\nMediaStore couldn't insert data."
        }
    }
}
 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
package org.owasp.mastestapp;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import java.io.OutputStream;
import kotlin.Metadata;
import kotlin.Unit;
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() {
        try {
            ContentResolver resolver = this.context.getContentResolver();
            ContentValues contentValues = new ContentValues();
            contentValues.put("_display_name", "secretFile.txt");
            contentValues.put("mime_type", "text/plain");
            contentValues.put("relative_path", Environment.DIRECTORY_DOCUMENTS);
            Uri textUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
            if (textUri != null) {
                OutputStream outputStream = resolver.openOutputStream(textUri);
                if (outputStream != null) {
                    OutputStream outputStream2 = outputStream;
                    OutputStream it = outputStream2;
                    byte[] bytes = "MAS_API_KEY=8767086b9f6f976g-a8df76\n".getBytes(Charsets.UTF_8);
                    Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
                    it.write(bytes);
                    it.flush();
                    Unit unit = Unit.INSTANCE;
                    CloseableKt.closeFinally(outputStream2, null);
                }
                Log.d("MediaStore", "File written to external storage successfully.");
                return "SUCCESS!!\n\nMediaStore inserted to " + textUri;
            }
            MastgTest mastgTest = this;
            Log.e("MediaStore", "Error inserting URI to MediaStore.");
            return "FAILURE!!\n\nMediaStore couldn't insert data.";
        } catch (Exception exception) {
            Log.e("MediaStore", "Error writing file to URI from MediaStore", exception);
            return "FAILURE!!\n\nMediaStore couldn't insert data.";
        }
    }
}

Steps

Let's run our semgrep rule against the sample 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
run.sh
1
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-apis.yml ./MastgTest_reversed.java --text -o output.txt

Observation

The rule has identified 2 locations that indicate a use of MediaStore API.

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌─────────────────┐
 2 Code Findings 
└─────────────────┘

    MastgTest_reversed.java 
       rules.mastg-android-data-unencrypted-shared-storage-no-user-interaction-mediastore
          [MASVS-STORAGE] Make sure to want this data to be shared with other apps       

            8 import android.provider.MediaStore;
            ⋮┆----------------------------------------
           35 Uri textUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

The first location is the import statement for the MediaStore API and the second location is where the MediaStore API is used to write to shared storage.

Evaluation

After reviewing the decompiled code at the locations 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 API key.