Android Content Providers
What a Content Provider is (security perspective)
A Content Provider is Androidâs mechanism for exposing structured application data to other apps (or the OS)
via content:// URIs. Itâs not UI (Activity) and not background execution (Service). It is a
local data API, commonly backed by SQLite or internal storage.
From a security standpoint, Content Providers are high-impact because they can bypass UI flows and backend APIs. If exported or weakly protected, other apps on the device can directly read or modify internal data without user interaction and without server-side authorization.
How Content Providers are accessed
Providers are accessed using a URI format:
content://<authority>/<path>
Example:
content://com.bank.app.provider/accounts
Providers typically implement CRUD-style operations:
query()â readinsert()â createupdate()â modifydelete()â remove
Web application comparison (why itâs easy to miss)
| Web | Android |
|---|---|
| Backend API endpoint | Content Provider |
/api/users | content://com.app.provider/users |
| GET | query() |
| POST | insert() |
| PUT / PATCH | update() |
| DELETE | delete() |
| Auth middleware | Permissions + caller validation |
| IDOR / BOLA | URI path manipulation |
| SQL injection | SQLite SQL injection inside provider |
The key difference: Content Providers are local. They do not hit your backend. Backend authorization cannot âsave youâ if your provider is exported and unprotected.
Why Content Providers become vulnerable
- Providers exported unnecessarily (or exported by mistake)
- Missing custom permissions (or using weak/common permissions)
- Trusting IDs in the URI path (IDOR)
- Unsafe SQL construction (selection concatenation)
- Over-exposing write/delete operations
- Over-broad URI grants (
grantUriPermissions)
Vulnerabilities you see in real apps
1) Unprotected exported provider (direct data exposure)
If a provider is exported and not protected by a strong permission model, any app can query it. This is direct disclosure of app-private data (profiles, cached responses, tokens, messages, transactions, etc.).
2) IDOR / broken object access
If a provider exposes object identifiers via paths (e.g., /accounts/1) and does not validate ownership,
an attacker can change the ID and access other objects. This mirrors BOLA in web APIs, but it happens locally.
3) SQL injection inside the provider
Providers often sit directly on SQLite. If selection clauses are built by string concatenation, you can get on-device SQL injection: unexpected rows returned, filters bypassed, or crashes due to malformed SQL.
4) Unauthorized insert/update/delete
If insert/update/delete are exposed without checking who the caller is, other apps can tamper with internal state or delete data. In real apps this can break accounts, corrupt caches, or persist malicious state.
5) Weak permission model / over-broad URI grants
Providers guarded by common/system permissions are often reachable by many unrelated apps. Separately, careless URI grants can give broader access than intended.
How to test Content Providers (practical workflow)
Step 1: Enumerate providers (static)
apktool d app.apk -o out
grep -R "<provider" -n out/AndroidManifest.xml
Step 2: Enumerate providers (runtime)
adb shell dumpsys package com.target.app | sed -n '/Providers:/,/Receivers:/p'
Step 3: Identify exported + permission model
android:exported="true"
android:permission="..."
android:readPermission="..."
android:writePermission="..."
Step 4: Locate provider paths
grep -R "content://" -n out/
grep -R "CONTENT_URI" -n out/
Step 5: Test read access
adb shell content query \
--uri content://com.target.app.provider/accounts
Step 6: Test IDOR (path manipulation)
adb shell content query \
--uri content://com.target.app.provider/accounts/1
adb shell content query \
--uri content://com.target.app.provider/accounts/2
Step 7: Test SQLi (selection probing)
adb shell content query \
--uri content://com.target.app.provider/accounts \
--where "1=1--"
Step 8: Test write exposure
adb shell content insert \
--uri content://com.target.app.provider/accounts \
--bind name:s:test
adb shell content update \
--uri content://com.target.app.provider/accounts/1 \
--bind status:s:inactive
adb shell content delete \
--uri content://com.target.app.provider/accounts/1
Secure code (vulnerable â fixed) for each issue
1) Unprotected exported provider
Fix: donât export unless required.
<provider
android:name=".data.AccountProvider"
android:authorities="com.example.app.provider"
android:exported="false" />
If you must share: use signature-level custom permissions.
<permission
android:name="com.example.app.permission.READ_INTERNAL"
android:protectionLevel="signature" />
<permission
android:name="com.example.app.permission.WRITE_INTERNAL"
android:protectionLevel="signature" />
<provider
android:name=".data.AccountProvider"
android:authorities="com.example.app.provider"
android:exported="true"
android:readPermission="com.example.app.permission.READ_INTERNAL"
android:writePermission="com.example.app.permission.WRITE_INTERNAL" />
2) IDOR / broken object access
Vulnerable pattern: returns rows solely based on URI ID.
// Vulnerable (conceptual)
override fun query(uri: Uri, ...): Cursor? {
val id = ContentUris.parseId(uri)
return db.query("accounts", null, "id=?", arrayOf(id.toString()), null, null, null)
}
Fix: enforce caller trust + object ownership/authorization before returning data.
// Safer pattern (conceptual)
override fun query(uri: Uri, ...): Cursor? {
if (!enforceTrustedCaller()) throw SecurityException("Caller not allowed")
val id = ContentUris.parseId(uri)
if (!isAccountOwnedByCaller(id)) throw SecurityException("Not authorized")
return db.query("accounts", null, "id=?", arrayOf(id.toString()), null, null, null)
}
3) SQL injection inside provider
Vulnerable: concatenating untrusted input into SQL selection.
// Vulnerable
val where = "name = '" + userInput + "'"
return db.query("users", null, where, null, null, null, null)
Fix: parameterize selection arguments.
// Safe
val where = "name = ?"
val args = arrayOf(userInput)
return db.query("users", null, where, args, null, null, null)
Also avoid forwarding arbitrary selection/args from external callers. If you need filtering, implement allowlisted filters yourself.
4) Unauthorized insert/update/delete
Vulnerable: write operations without access control.
// Vulnerable
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val id = db.insert("accounts", null, values)
return ContentUris.withAppendedId(uri, id)
}
Fix: restrict writes to trusted callers and validate input.
// Safer (conceptual)
override fun insert(uri: Uri, values: ContentValues?): Uri? {
if (!enforceTrustedCaller()) throw SecurityException("Write not allowed")
val safe = sanitizeAndValidate(values)
val id = db.insert("accounts", null, safe)
return ContentUris.withAppendedId(uri, id)
}
Design fix: if external sharing is only for reads, make the provider read-only.
override fun insert(uri: Uri, values: ContentValues?) =
throw UnsupportedOperationException("Insert not supported")
override fun update(uri: Uri, values: ContentValues?, where: String?, args: Array<String>?) =
throw UnsupportedOperationException("Update not supported")
override fun delete(uri: Uri, where: String?, args: Array<String>?) =
throw UnsupportedOperationException("Delete not supported")
5) Weak permissions / URI grants
Fix: use signature-level permissions for private provider sharing and keep URI grants scoped to the minimum required URI.
<permission
android:name="com.example.app.permission.READ_INTERNAL"
android:protectionLevel="signature" />
// If you grant URI access, grant only the specific URI you intend to share
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
Interview questions & answers (Easy â Hard â Senior)
Easy
- What is a Content Provider?
Answer: A component that exposes structured app data viacontent://URIs to other apps or the OS. - How is it different from an Activity?
Answer: Activities expose UI screens; Content Providers expose data directly without UI. - What does a content URI look like?
Answer:content://<authority>/<path>
Medium
- Why are Content Providers high risk?
Answer: If exported or weakly protected, any app can directly read/modify internal data without backend/UI checks. - How do you test a Content Provider?
Answer: Enumerate providers (manifest/dumpsys), identify exported/permissions, then useadb shell contentto test query/insert/update/delete and prove impact. - What is IDOR in Content Providers?
Answer: When changing an object ID in the content URI gives access to other usersâ/objectsâ records without authorization.
Hard
- Can SQL injection occur inside a Content Provider?
Answer: Yes. Providers commonly use SQLite; unsafe selection handling can cause on-device SQL injection. - Backend auth is strongâwhy is an exported provider still a problem?
Answer: Because providers bypass the backend entirely; the backend is not in the loop. - Is âexported providerâ always a vulnerability?
Answer: No. Itâs a vulnerability when sensitive data or operations are reachable without strong permission and access control.
Senior / Architect
- How would you design a secure Content Provider?
Answer: Donât export unless required, enforce signature-level custom permissions, validate callers where appropriate, parameterize SQL, and minimize exposed data and write operations. - What is a realistic attack scenario?
Answer: A malicious app enumerates exported providers and silently reads or manipulates sensitive data via ContentResolver. - How do you explain provider risks to a backend team?
Answer: Itâs like a local unauthenticated API that exposes database access and bypasses backend authorization.