adb shell content query --uri content://com.mhfh.providerlab.notes/notes --projection _id:title --limit 1
Executive Summary: A URI Can Cross the Sandbox
Android Content Provider exploitation is the controlled testing of URI-based interfaces that expose application data or files across process boundaries. A provider may be intentionally reachable for search, sharing, synchronization, or widgets. The security failure begins when an untrusted caller can read sensitive rows, change protected state, influence unsafe SQL construction, traverse beyond an intended file directory, or receive a broader URI grant than the workflow requires.
This tutorial builds a fictional package named com.mhfh.providerlab. Its laboratory provider contains non-sensitive note records and accepts reversible marker operations. You will enumerate authorities, confirm effective permissions, issue a bounded query, add and remove one test marker, review query construction, inspect FileProvider path mappings, and verify temporary grants. Every proof is scoped to an emulator or application you are authorized to assess.
The central rule is simple: prove the smallest meaningful behavior first. Provider resolution is not a vulnerability. A returned cursor is not automatically sensitive. Severity comes from the data, operation, caller privileges, and security boundary demonstrated by reproducible evidence.
How Android Content Providers Work
A ContentProvider presents structured data through URIs such as content://authority/path/row-id. The authority selects the provider, the path usually selects a collection, and an optional final segment identifies one record. Clients call the provider through ContentResolver, which dispatches query, insert, update, delete, type, batch, or file-open operations across Binder.
Android's Content Provider basics map the major query arguments to SQL concepts:
| Provider argument | Typical SQL role | Security question |
|---|---|---|
projection | Selected columns | Are identifiers allowlisted? |
selection | WHERE expression | Is syntax caller-controlled? |
selectionArgs | Bound values | Are values parameterized? |
sortOrder | ORDER BY expression | Are columns and direction constrained? |
| URI path | Table or row selection | Are segments matched and typed? |
Providers do not have to use SQLite. A provider can front files, preferences, network-backed records, generated reports, or another internal API. Testing must follow the implementation rather than assuming every URI maps directly to a database table.

The caller crosses a permission boundary through ContentResolver. URI matching, query validation, provider permissions, and temporary grants determine which database rows or files become reachable.
Build a Precise Provider Threat Model
Before sending commands, state who the provider is meant to serve. A search-suggestion provider may intentionally answer public, low-sensitivity queries. A same-publisher account provider may trust only packages signed with the same certificate. A FileProvider normally remains non-exported and relies on temporary grants for individual files. Those designs require different tests and different findings.
Record the authority held by the target process. It might access private SQLite rows, authenticated documents, cached exports, health data, account identifiers, or files under the application sandbox. A caller that lacks direct access may still ask an inadequately protected provider to act as a deputy. Name the exact capability crossed rather than describing the provider generically as dangerous.
Use four checkpoints for each URI:
- Resolvable: Android recognizes the authority and path.
- Permitted: the caller passes provider, path, and URI-grant checks.
- Controllable: caller input changes a query, record, filename, or operation.
- Security-relevant: the result violates confidentiality, integrity, authentication, or availability expectations.
This model prevents inflated reports. A public catalog returning public product names is expected. A private draft provider returning unpublished documents to an ordinary application crosses a real boundary.
Prepare the Controlled Provider Lab
Use an emulator snapshot and capture the environment before testing:
adb shell getprop ro.build.fingerprint
adb shell dumpsys package com.mhfh.providerlab > providerlab-package.txt
adb shell pm clear com.mhfh.providerlab
adb shell monkey -p com.mhfh.providerlab 1The lab manifest declares one deliberately exposed training provider and one correctly configured FileProvider:
<provider
android:name=".NotesProvider"
android:authorities="com.mhfh.providerlab.notes"
android:exported="true" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.mhfh.providerlab.files"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>ADB commands execute as the shell UID, which is not identical to an ordinary third-party application. Establish candidates with ADB, then reproduce important access from a minimal companion APK that requests no unrelated permission. Preserve the caller package, UID, target version, command output, and target logcat alongside every claim.
Enumerate Authorities and Manifest Controls
Start statically because the merged manifest defines the provider boundary:
apktool d provider-lab.apk -o provider-lab-decoded
jadx -d provider-lab-jadx provider-lab.apk
grep -n "<provider" provider-lab-decoded/AndroidManifest.xml
grep -RniE "content://|UriMatcher|ContentProvider|FileProvider" provider-lab-jadxFor each provider, record the class, authority, exported and enabled states, process, general permission, separate read and write permissions, grantUriPermissions, child path-permission rules, and provider metadata. Inspect custom permission definitions and their protection levels. A permission named INTERNAL is not protective if it uses the automatically granted normal level.
Then identify paths and operations. Search for UriMatcher.addURI, contract classes containing CONTENT_URI, and switches inside query, insert, update, delete, openFile, and call. The current OWASP provider enumeration technique recommends combining manifest inspection with installed-state and tool-assisted enumeration.
Confirm the effective package state:
adb shell dumpsys package com.mhfh.providerlab
adb shell dumpsys activity providers | grep -A 12 com.mhfh.providerlabManifest merging, application-level permissions, product flavors, and runtime enablement can change the result seen in source snippets. Always compare the decoded APK with the installed device.
Start with a Bounded Read-Only Query
Query only the columns and row count required to establish access:
adb shell content query \
--uri content://com.mhfh.providerlab.notes/notes \
--projection _id:title \
--limit 1An expected lab response resembles:
Row: 0 _id=1, title=training-noteThis proves that the shell caller can resolve the authority, reach the collection path, pass the read gate, and retrieve two projected fields. It does not prove access from an ordinary app, reveal whether the title is sensitive, or establish write access. Repeat from the companion APK before generalizing the caller model.
The OWASP runtime unauthorized database access test evaluates both accessibility and record sensitivity. Document why the returned fields matter: tokens, private messages, medical records, unpublished content, or identifiers carry different impact from public labels.
Resolve Permission Precedence Correctly
Provider access control can be layered, so reading one attribute is not enough. The application-level android:permission may supply a default inherited by components. The provider's android:permission can restrict both directions, while android:readPermission and android:writePermission specialize the operation. Child path-permission elements can apply different rules to particular URI paths. Finally, a temporary grant may authorize one URI for a caller that lacks the general permission.
Build a permission matrix rather than describing the provider as simply protected:
| Path | Operation | Declared gate | Caller holds it? | Temporary grant? | Result |
|---|---|---|---|---|---|
/catalog | query | none | N/A | none | allowed |
/drafts | query | READ_DRAFTS | no | none | denied |
/drafts/12 | query | READ_DRAFTS | no | read URI | allowed |
/drafts/12 | update | WRITE_DRAFTS | no | read URI | denied |
This table captures the distinction between provider-wide access and URI-scoped capabilities. Test the collection and one row separately because a prefix or path rule can change the outcome. Test read and write separately because a read grant must not silently become a write grant.
Inspect the custom permission declaration as carefully as the component. A normal permission is granted automatically to requesting applications and is not an effective boundary for sensitive data. A dangerous permission depends on runtime consent and may still be inappropriate for a private partner integration. A signature-level permission is generally suitable when all trusted clients are signed by the same publisher, but certificate rotation and multi-app release processes need deliberate testing.
Do not treat a permission-denied response as a dead end. Preserve the exception, caller UID, requested permissions, exact URI, and provider declaration. Negative evidence demonstrates that the platform gate worked for that identity and often closes the assessment path cleanly. Only investigate alternate paths when static review identifies a real reason, such as a weaker path permission, an unintended URI grant, or another method lacking the same check.
Reproduce Access from a Minimal Caller App
A small caller application turns an ADB candidate into realistic IPC evidence. Give it a distinct package name, declare no permissions initially, and place one test action behind a visible button. The action should use ContentResolver, request a narrow projection, close its Cursor deterministically, and display only the row count or prearranged marker. Log the caller UID with Process.myUid() so the evidence identifies the security principal.
val uri = Uri.parse("content://com.mhfh.providerlab.notes/notes")
val projection = arrayOf("_id", "title")
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
Log.i("ProviderCaller", "uid=${Process.myUid()} rows=${cursor.count}")
if (cursor.moveToFirst()) {
val title = cursor.getString(cursor.getColumnIndexOrThrow("title"))
resultView.text = "First title: $title"
}
}Install the caller after the target so package visibility and permission state resemble the real threat model. Capture both package records with dumpsys package. If the provider requires a custom permission, build one caller variant without it and a second variant declaring it. For a signature permission, sign the negative variant with an unrelated test key and the trusted variant with the lab publisher key. The difference demonstrates enforcement more convincingly than a single success or failure.
Use the caller app for temporary-grant testing as well. Have the target explicitly share one lab document with the caller package, verify that the granted URI opens, then try a sibling URI and the same URI after revocation. Record the Intent flags and ClipData because modern sharing flows may place grantable URIs in either location. A screenshot of a selected document is useful, but the decisive evidence is the allowed and denied operation pair under the same caller UID.
Keep the harness deliberately boring. It should not enumerate unrelated authorities, loop over unknown paths, request broad storage permissions, or retain copied data. One button, one known URI, one bounded operation, and one reset path make the result easier to audit and reproduce. When the test ends, uninstall the caller or clear its data so persisted grants and cached output cannot contaminate the next run.
Test Insert, Update, and Delete with One Marker
Write operations can change user data, so use a unique marker and take before-and-after counts. Never test destructive mutations against a production account.
adb shell content insert \
--uri content://com.mhfh.providerlab.notes/notes \
--bind title:s:MHFH_PROVIDER_TEST_20260721 \
--bind body:s:reversible-marker
adb shell content query \
--uri content://com.mhfh.providerlab.notes/notes \
--where "title='MHFH_PROVIDER_TEST_20260721'"If insertion succeeds, update only that marker:
adb shell content update \
--uri content://com.mhfh.providerlab.notes/notes \
--where "title='MHFH_PROVIDER_TEST_20260721'" \
--bind body:s:updated-reversible-markerClean up the same row and verify zero matches:
adb shell content delete \
--uri content://com.mhfh.providerlab.notes/notes \
--where "title='MHFH_PROVIDER_TEST_20260721'"
adb shell content query \
--uri content://com.mhfh.providerlab.notes/notes \
--where "title='MHFH_PROVIDER_TEST_20260721'"Separate read and write conclusions. A provider may intentionally expose public reads while protecting writes with android:writePermission. Test the permission actually required for each operation and report a SecurityException as evidence that enforcement worked.
Audit SQL Construction Without Damaging Data
Parameterized values are the main defense against provider SQL injection. A safe implementation passes a fixed selection expression and supplies caller values through selectionArgs:
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor {
val match = matcher.match(uri)
require(match == NOTES || match == NOTE_ID)
val safeProjection = projection?.also { columns ->
require(columns.all { it in ALLOWED_COLUMNS })
}
val safeSort = sortOrder?.takeIf { it in ALLOWED_SORTS }
return builder.query(db, safeProjection, selection, selectionArgs, null, null, safeSort)
}Values bound to ? placeholders are handled as data. However, placeholders cannot safely bind table names, column identifiers, operators, or sorting direction. Projection and sort inputs therefore need explicit allowlists.
Risky patterns include concatenating a URI path segment into appendWhere, building selection with string interpolation, accepting arbitrary projection expressions, or passing unvalidated sortOrder into a query builder. OWASP's current ContentProvider knowledge reference distinguishes bound selectionArgs from caller-controlled SQL syntax.
In the lab, compare one legitimate filter with a benign predicate variation and inspect row-count changes. Do not mutate records while validating injection. Preserve the exact URI, decoded provider code, generated query logs where available, baseline count, test count, and cleanup state. A syntax error alone proves input reached SQL parsing; it does not establish data exposure.
Test URI Paths, IDs, and MIME Types
Enumerate only paths supported by the contract or discovered UriMatcher rules. Common patterns include notes for a collection and notes/# for a numeric record. Confirm that unsupported paths fail closed and numeric routes reject non-numeric identifiers.
Check getType results because MIME behavior can reveal collections and influence downstream intent routing. Review call separately: it exposes method-style operations that may bypass normal CRUD assumptions. A provider that correctly restricts query can still expose a sensitive reset, export, or synchronization action through an inadequately checked call(method, arg, extras) branch.
Batch operations deserve method-level review. applyBatch may execute several mutations atomically and can expose back references between operations. Confirm that authorization and validation apply to every item, not only the first URI in a batch.
File-returning methods add resource and type boundaries. Review the mode passed to openFile, openAssetFile, or openTypedAssetFile; a caller requesting rw must not receive write access when the contract promises read-only sharing. Confirm that the provider returns the requested representation, closes descriptors on error, and does not expose a stable underlying path through diagnostic output. For generated or piped content, enforce cancellation and length limits so an external caller cannot leave unlimited background work attached to abandoned descriptors.
Canonicalization methods can also influence trust decisions. If the provider implements canonicalize or uncanonicalize, verify that a canonical URI does not erase an account, tenant, or access-control distinction embedded in the original path. The transformed URI must resolve to the same authorized object for the same caller, not merely to a globally stable identifier.
Audit FileProvider Path Configuration
AndroidX FileProvider converts app-owned files into grantable content:// URIs. Its API reference specifies that it should be non-exported and grant URI permissions. The path XML defines which directories it can share:
<paths>
<files-path name="reports" path="exports/reports/" />
<cache-path name="preview" path="share-preview/" />
</paths>Prefer narrow subdirectories created specifically for sharing. Android's FileProvider security guidance warns against <root-path>, broad . or / mappings, and sensitive data under external paths. Read and write grants should be limited to the operation genuinely required.
Review code that generates and consumes provider URIs. Canonicalize paths before enforcing directory containment, reject unexpected schemes and authorities, and avoid turning provider-controlled metadata directly into local paths. A consuming app must also treat OpenableColumns.DISPLAY_NAME as untrusted because a malicious provider can return traversal sequences in a filename.
Validate Temporary URI Grants
A private provider can deliberately share one URI through an Intent containing FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION. Android documents that these flags apply to the specific URI rather than granting blanket provider access.
Test grants as capabilities:
- Which exact URI or prefix is granted?
- Is access read-only, write-only, or both?
- Which receiving package obtains it?
- Does a chooser or forwarded Intent broaden the recipient set?
- Is the grant persistable, and does the app actually need persistence?
- Does revocation occur after logout, deletion, or workflow completion?
Use a companion app to open the granted URI and then try a sibling URI that was not shared. The expected design permits the selected document and rejects its neighbor. Verify revocation after the intended lifecycle. Do not infer grant behavior solely from manifest attributes because runtime flags and ClipData determine the effective capability.
Treat Imported Provider Data as Untrusted
Provider security is bidirectional. An app may securely protect its own provider yet trust a malicious provider selected through a document or share workflow. The returning app controls URI content, MIME claims, cursor columns, display name, declared size, and stream behavior.
OWASP's external-component sanitization guidance recommends accepting expected content:// schemes, sanitizing display names to a final path component, anchoring output under an app-controlled directory, and validating resolved containment. Also impose byte limits, timeouts, acceptable MIME types, and collision-safe filenames. A declared size can be absent or dishonest, so enforce limits while streaming.
Never concatenate a returned display name directly beneath a sensitive directory. Create a new local name or reduce metadata to a basename, resolve the destination canonically, and ensure it remains beneath the intended base directory before opening the output stream.

A defensible provider assessment is read-first and bounded: inventory, resolve, query minimally, verify permissions, use one reversible marker, validate file boundaries, preserve evidence, and restore the baseline.
Dynamic Observation with Frida
When obfuscation hides URI dispatch, observe calls without changing behavior. This lab hook logs query parameters entering the target provider:
Java.perform(function () {
const Provider = Java.use("com.mhfh.providerlab.NotesProvider");
const query = Provider.query.overload(
"android.net.Uri",
"[Ljava.lang.String;",
"java.lang.String",
"[Ljava.lang.String;",
"java.lang.String"
);
query.implementation = function (uri, projection, selection, args, sort) {
console.log("[provider] uri=" + uri);
console.log("[provider] selection=" + selection);
console.log("[provider] sort=" + sort);
return query.call(this, uri, projection, selection, args, sort);
};
});Trace insert, update, delete, openFile, or call only when the assessment path uses them. Pair runtime logs with manifest controls and observable output. A hook shows delivery and parameter flow; it does not independently prove that another application can pass the access gate.
Evidence and Severity Framework
Preserve the following for each tested authority:
- APK hash, package version, target SDK, Android build, and test time.
- Provider manifest declaration and custom permission protection levels.
- Installed package state and calling identity.
- Exact content URI, method, projection, selection, arguments, and sort order.
- Baseline and resulting row counts or file hashes.
- Lifecycle logs or observational traces.
- Data classification and business meaning.
- Marker cleanup result or emulator snapshot identifier.
Score confidentiality and integrity independently. Reading one public label is informational; reading authenticated records may be high impact. Creating a reversible draft marker differs from changing an account's authoritative state. File reads, file writes, URI-grant persistence, and SQL injection should be scored by the demonstrated scope rather than the vulnerability label.
Availability claims require measurement. Use a small bounded request count, observe latency, cursor or descriptor cleanup, and reset. Do not flood a provider merely to state that repeated calls consume resources.
Secure Android Provider Checklist
- Internal providers explicitly use
android:exported="false". - Required external providers document their caller and data model.
- Read and write permissions match each operation's sensitivity.
- Same-publisher access uses a signature-level permission where appropriate.
- Path permissions do not accidentally override stronger general rules.
- URI paths are matched narrowly and IDs are parsed to expected types.
- Values use selection arguments rather than SQL concatenation.
- Projection and sort identifiers use allowlists.
-
call,openFile, batch, and canonicalize methods receive equal review. - FileProvider is private with narrow path mappings.
- Grants contain the minimum URI, mode, recipient, and lifetime.
- Imported URIs, display names, MIME types, and stream lengths are untrusted.
- Tests run from a separate no-permission application identity.
- Mutation tests use unique markers and verify cleanup.
Frequently Asked Questions
What is Android Content Provider exploitation?
It is the abuse of a provider's URI interface to perform an operation beyond the caller's intended authority. Examples include unauthorized database reads, protected record changes, SQL injection through query construction, unintended file access, or misuse of URI grants. An exported declaration alone does not establish exploitation.
How are exported providers discovered?
Decode the merged manifest and inventory every provider authority, export rule, permission, path permission, grant setting, and metadata entry. Confirm the installed package with dumpsys. Then trace contract URIs, UriMatcher paths, and provider lifecycle methods in decompiled code before issuing a minimal query.
Can selectionArgs prevent every provider SQL injection?
No. Selection arguments safely bind values placed at ? placeholders, but they do not bind table names, column identifiers, operators, projection expressions, or sort direction. Keep the SQL structure fixed and allowlist any identifier that a caller can select.
Should FileProvider use android:exported=true?
No. Standard AndroidX FileProvider deployments should be non-exported. The owning app grants temporary access to individual content URIs with explicit read or write flags. Path XML should expose narrowly scoped sharing directories rather than the filesystem root, an entire internal directory, or sensitive external storage.
Why reproduce ADB findings from a companion app?
ADB's content command runs as the shell UID, which can receive different treatment from an ordinary installed application. A minimal companion app establishes the realistic caller identity, requested permissions, package visibility, grant state, and exact ContentResolver behavior needed for a defensible finding.
Conclusion: Test the Capability Behind the URI
Android Content Provider exploitation is not a search for one manifest boolean. It is a chain connecting an external identity, an effective permission decision, a controlled URI or query field, provider behavior, and a security-relevant result. Each link needs evidence.
The reliable workflow is to inventory first, query minimally, reproduce from an ordinary app, separate reads from writes, audit SQL structure, constrain file paths, verify temporary grants, and reset every marker. That approach identifies real provider flaws while producing evidence that engineers can reproduce and fix.
Continue with the exported activities, services, and receivers tutorial, the Android WebView bridge guide, the MobSF tool profile, or our mobile application penetration testing service.
