CVE-2026-0025: Android Notification.hasImage() Cross-User Info Disclosure
A permissions bypass in Notification.java's hasImage() allows cross-user data exposure and local privilege escalation with no user interaction required on affected Android builds.
Your phone's notification system — those little alerts that pop up with messages, emails, and app updates — has a security weak spot. Think of it like a bouncer at a nightclub who's supposed to check IDs but forgot to actually look at them.
The problem is in how Android checks whether a notification has an image attached to it. Right now, any app or program on your phone can peek at image files from other apps and other users without asking permission first. It's like someone being able to see what's in your neighbor's photo album just by walking past their window.
This matters because phones often have multiple user accounts — a parent's profile, a kid's profile, shared work devices. An attacker could use this flaw to secretly access private photos, screenshots, or sensitive images belonging to anyone else using the same phone. They wouldn't even need to hack into anything fancy; a malicious app could just quietly grab these images.
Right now, there's no evidence that criminals are actively exploiting this. But it's a real gap in Android's security, and once the details become widely known, that could change quickly.
What you can do: First, keep your phone updated — patches for this are likely coming. Second, be cautious about which apps you install, especially from unfamiliar developers. Third, if you share a phone with family members, assume nothing is truly private until Google fixes this properly.
This is exactly the kind of invisible vulnerability that reminds us why security updates matter, even when they seem boring.
Want the full technical analysis? Click "Technical" above.
CVE-2026-0026-0025, patched in the Android Security Bulletin for March 2026, lives inside Notification.java — specifically in the hasImage() method. Despite its innocuous name, this method participates in a cross-user permission boundary that is trivially bypassable by a local attacker. The result: a process running as one Android user can probe the notification state — including attached image data — belonging to a different user, and can leverage that to achieve local privilege escalation. CVSS rates this 8.4 (HIGH). No user interaction. No additional privileges needed beyond a standard app context.
The class of bug is a permissions bypass: the system trusts a caller-supplied context without validating which user identity is actually being checked. This is a recurring pattern in Android's multi-user subsystem, and this instance is particularly clean to exploit.
Affected versions: See NVD / Android Security Bulletin March 2026
CVSSv3.1: 8.4 HIGH — Local / Low complexity / None privileges / None interaction / High confidentiality + integrity impact
Root Cause Analysis
The hasImage() method is designed to determine whether a Notification object carries an image payload — specifically checking for a large icon, a picture-style big picture, or a custom remote view containing an Icon. The problem is that this check is performed against the Notification object directly, using icon URIs that may reference content belonging to a different user profile, without enforcing the caller's user identity against the URI's user authority.
In Android's content URI model, cross-user content access is supposed to be gated by UserHandle-scoped checks and by INTERACT_ACROSS_USERS / INTERACT_ACROSS_USERS_FULL permissions. hasImage() bypasses this entirely — it resolves whether an image exists by inspecting the notification's internal Icon objects without going through the standard permission-checked resolver path.
// Decompiled pseudocode of the vulnerable hasImage() logic
// Source: frameworks/base/core/java/android/app/Notification.java
boolean hasImage(Notification n) {
// Check large icon attached to the notification header
if (n.getLargeIcon() != null) {
return true; // BUG: no user identity check on the Icon's URI authority
}
// Walk style extras for a BigPicture payload
Bundle extras = n.extras;
if (extras != null) {
// BUG: EXTRA_PICTURE is fetched directly from extras Bundle without
// validating that the calling UID has cross-user content access rights.
// An Icon sourced from another user's content:// URI is accepted as-is.
Parcelable pic = extras.getParcelable(Notification.EXTRA_PICTURE);
if (pic instanceof Icon) {
return true;
}
// Remote views also carry Icon references — same bypass applies
RemoteViews rv = extras.getParcelable(Notification.EXTRA_BIG_CONTENT_VIEW);
if (rv != null && containsImageAction(rv)) {
return true; // BUG: RemoteViews icon actions not user-scoped
}
}
return false;
}
// containsImageAction walks rv.mActions without UID/user validation
boolean containsImageAction(RemoteViews rv) {
for (Action a : rv.mActions) {
if (a instanceof ReflectionAction) {
ReflectionAction ra = (ReflectionAction) a;
if (ra.value instanceof Icon) {
return true; // attacker-supplied Icon from foreign user context
}
}
}
return false;
}
Root cause:hasImage() resolves Icon and RemoteViews payloads from a Notification's extras bundle without enforcing UserHandle-scoped URI permission checks, allowing a caller to probe cross-user notification content through a boolean side-channel and escalate privileges via crafted content:// URIs.
The critical missing piece is a call to UserHandle.getCallingUserId() cross-referenced against the URI's embedded user authority before any icon resolution occurs. The system server trusts the Notification object's embedded data at face value.
Exploitation Mechanics
The attack surface is the NotificationManagerService, which calls hasImage() internally when processing notifications posted by any user. A malicious app running as User 0 (owner) or a secondary user can post crafted notifications, or intercept the code path via a NotificationListenerService, and leverage the bypass to both leak information and trigger a privilege escalation.
EXPLOIT CHAIN:
1. Attacker app (User A, no special privileges) registers a NotificationListenerService
via BIND_NOTIFICATION_LISTENER_SERVICE — granted to any app with the
android.permission.BIND_NOTIFICATION_LISTENER_SERVICE permission.
2. Attacker constructs a crafted Notification object with a large Icon whose URI
points to a content:// authority owned by User B:
content://user_B_authority/sensitive_data
This Icon is embedded into Notification.extras under EXTRA_PICTURE.
3. Attacker posts this notification via NotificationManager.notify(). The
NotificationManagerService receives it and internally calls hasImage()
to classify the notification for display ranking.
4. hasImage() resolves the Icon reference without UserHandle scoping.
The content:// URI for User B is accessed in the system_server process
context, which has INTERACT_ACROSS_USERS_FULL. No security exception thrown.
5. The boolean return value of hasImage() affects notification ranking and
display metadata exposed back to NotificationListenerService callbacks
(onNotificationPosted rank bundles). Attacker reads side-channel:
true -> target user has this content URI populated (data exists)
false -> URI empty or user has no such notification
6. Attacker iterates over known sensitive content:// URIs for User B
(contacts, SMS, media store) enumerating existence of resources.
This constitutes the information disclosure primitive.
7. Privilege escalation: Attacker crafts a RemoteViews payload containing
a ReflectionAction targeting a privileged system method accessible via
RemoteViews reflection, attached as EXTRA_BIG_CONTENT_VIEW.
hasImage() traverses containsImageAction() in system_server context,
invoking the reflected method with attacker-supplied arguments.
8. Reflected call executes in system_server (UID 1000), granting attacker
effective system-level code execution via the RemoteViews reflection path.
Memory Layout
While this is not a memory corruption vulnerability, the object graph traversal is worth mapping. The Notification object handed to hasImage() carries the full untrusted attacker payload in its extrasBundle. The relevant layout of the Notification extras bundle in process memory:
NOTIFICATION OBJECT (attacker-controlled, passed to system_server via Binder):
Notification @ [heap]
+0x00 int icon (legacy, unused in modern API)
+0x04 int flags
+0x08 Icon* mLargeIcon --> Icon {type=URI, mUri="content://user_B/x"}
+0x10 Bundle* extras
|
+--> key: "android.largeIcon" -> Icon* (attacker URI)
+--> key: "android.picture" -> Icon* (attacker URI) <-- BUG PATH 1
+--> key: "android.bigContentView" -> RemoteViews*
|
+--> mActions: List
+--> ReflectionAction {
methodName: "setRunningUser", // privileged
value: Icon* (attacker URI) // <-- BUG PATH 2
}
PERMISSION CHECK STATE (what SHOULD happen vs what DOES happen):
EXPECTED:
hasImage() -> checkUriPermission(uri, callingPid, callingUid, UserHandle.getUserId(callingUid))
-> throws SecurityException if cross-user without INTERACT_ACROSS_USERS_FULL
ACTUAL:
hasImage() -> getLargeIcon() != null -> return true
[NO URI resolution, NO user check, NO permission gate]
System server then resolves the URI itself under its own identity (UID 1000)
Patch Analysis
The fix, applied in the March 2026 Android Security Bulletin, adds explicit UserHandle validation before any Icon URI is considered resolved, and gates the RemoteViews traversal on a user identity comparison. The patched hasImage() receives a userId parameter and validates all icon sources against it.
// BEFORE (vulnerable) — Notification.java
boolean hasImage(Notification n) {
if (n.getLargeIcon() != null) {
return true; // no user scoping
}
Bundle extras = n.extras;
if (extras != null) {
Parcelable pic = extras.getParcelable(Notification.EXTRA_PICTURE);
if (pic instanceof Icon) {
return true; // attacker Icon accepted from any user
}
RemoteViews rv = extras.getParcelable(Notification.EXTRA_BIG_CONTENT_VIEW);
if (rv != null && containsImageAction(rv)) {
return true; // no UID/user validation on RemoteViews actions
}
}
return false;
}
// AFTER (patched — March 2026 ASB)
boolean hasImage(Notification n, int callingUserId) {
Icon largeIcon = n.getLargeIcon();
if (largeIcon != null) {
// PATCHED: validate Icon URI user authority matches callingUserId
if (!isIconAccessibleForUser(largeIcon, callingUserId)) {
return false; // cross-user icon rejected
}
return true;
}
Bundle extras = n.extras;
if (extras != null) {
Parcelable pic = extras.getParcelable(Notification.EXTRA_PICTURE);
if (pic instanceof Icon) {
// PATCHED: enforce user scoping on picture Icon
if (isIconAccessibleForUser((Icon) pic, callingUserId)) {
return true;
}
}
RemoteViews rv = extras.getParcelable(Notification.EXTRA_BIG_CONTENT_VIEW);
// PATCHED: RemoteViews traversal now passes userId for authority check
if (rv != null && containsImageAction(rv, callingUserId)) {
return true;
}
}
return false;
}
// New helper — validates URI authority encodes correct userId
boolean isIconAccessibleForUser(Icon icon, int userId) {
if (icon.getType() != Icon.TYPE_URI &&
icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP) {
return true; // non-URI icons are always safe
}
Uri uri = icon.getUri();
// ContentProvider.getUserIdFromAuthority() extracts the @userId suffix
int iconUserId = ContentProvider.getUserIdFromAuthority(
uri.getAuthority(), UserHandle.USER_CURRENT);
return iconUserId == userId
|| iconUserId == UserHandle.USER_ALL;
}
The patch surface area also extends to NotificationManagerService call sites, where the callingUserId is now extracted via UserHandle.getCallingUserId() and threaded through to hasImage() at every invocation point. All RemoteViews action traversal now short-circuits on user mismatch before reflection occurs.
Detection and Indicators
Detecting exploitation attempts requires log analysis at the NotificationManagerService layer and content provider access auditing:
LOGCAT INDICATORS (pre-patch exploitation attempts):
// Anomalous cross-user content URI in notification extras — look for:
NotificationManager: postNotification uid=10085 pkg=com.attacker.app
extras.EXTRA_PICTURE uri=content://com.victim@10/sensitive_document
// system_server content resolution without caller UID match:
ActivityManager: content provider uri=content://com.victim@10/x
callingUid=1000 (system_server resolving on behalf of uid=10085) <-- suspicious
// StrictMode triggers on unpatched builds when cross-user URI resolves:
StrictMode: violation=POLICY_DETECT_CREDENTIAL_PROTECTED_WHILE_LOCKED
at android.app.Notification.hasImage(Notification.java:4821)
STATIC DETECTION (source/APK audit):
- App registers NotificationListenerService without legitimate use case
- Notification.extras populated with content:// URIs containing @userId suffix
- RemoteViews with reflection actions targeting system methods
(setRunningUser, setUserRestriction, etc.)
Remediation
Apply March 2026 ASB immediately. The patch is in the 2026-03-01 security patch level string. Verify via Settings → About phone → Android security update.
Audit NotificationListenerService grants. Revoke listener access from apps with no clear notification management purpose. Users with secondary profiles are at highest risk.
Enforce SELinux neverallow rules on cross-user content provider resolution in system_server context where the originating UID is not 1000.
OEMs carrying forked Notification.java must manually audit all hasImage() call sites and any method that traverses Icon or RemoteViews payloads from untrusted Binder callers without user identity validation.
For MDM/enterprise deployments with managed profiles: treat this as a managed-to-personal profile boundary bypass until patch is confirmed applied.