July 14, 2026
Your Android app probably doesn’t check whether something is drawn on top of it.
I scanned 10 open-source Android apps for a specific vulnerability: tapjacking — where a malicious app draws a transparent overlay on top…

By Yehor Mamaiev
6 min read
I scanned 10 open-source Android apps for a specific vulnerability: tapjacking — where a malicious app draws a transparent overlay on top of a legitimate app to intercept user taps. Out of 10 apps, 4 had zero protection on their most sensitive screens. One of those is a cryptocurrency wallet where a single hijacked tap can send funds to an attacker's address.
The fix is one XML attribute. But almost nobody uses it.
What Is Tapjacking?
Android allows apps to draw on top of other apps — that's how chat bubbles, floating video players, and screen dimmers work. The permission is called SYSTEM_ALERT_WINDOW, and users grant it routinely.
Tapjacking exploits this: a malicious app draws a transparent or misleading layer on top of a legitimate app. The user thinks they're tapping one thing — but the touch event goes to the legitimate app underneath, triggering a completely different action.
The defense is simple: Android provides filterTouchesWhenObscured="true" — an XML attribute that tells a view to reject touch events when another window is drawn on top of it. One attribute, applied to the root layout, protects the entire screen.
Here's what I found when I looked for it.
Pattern 1: The Cryptocurrency Wallet — Confirm Transaction Without Protection
App type: Bitcoin wallet (open source, financial app)
The main activity uses launchMode="singleTask" (StrandHogg 2.0 attack surface) and is exported with NFC intent filters. The send coins screen — where users confirm cryptocurrency transfers — has zero overlay protection:
<! — send_coins_fragment.xml — NO filterTouchesWhenObscured →
<LinearLayout android:orientation=”vertical”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”>
<! — Receiving address field →
<! — Amount in BTC →
<! — Amount in local currency →
<! — Private key password →
<! — SEND button →
</LinearLayout>
<! — send_coins_fragment.xml — NO filterTouchesWhenObscured →
<LinearLayout android:orientation=”vertical”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”>
<! — Receiving address field →
<! — Amount in BTC →
<! — Amount in local currency →
<! — Private key password →
<! — SEND button →
</LinearLayout>
No filterTouchesWhenObscured. Not on the root layout, not on the send button, not on the amount field.
The attack chain:
- Malicious app detects the wallet is in the foreground
- Overlay draws a transparent layer over the transaction screen
- The overlay shows a different recipient address over the real address field
- User thinks they're sending to their intended recipient
- They tap "Send" — transaction confirmed to the attacker's address
- Bitcoin transaction is irreversible — funds are permanently gone
What makes this worse:
- singleTask launch mode with default taskAffinity — a malicious app can inject an activity into the wallet's task (StrandHogg 2.0)
- Exported via NFC— an attacker with physical proximity can trigger the wallet with a pre-filled payment intent, then overlay the address display
- No code obfuscation— all 704 smali files use readable names (SendCoinsActivit, WalletActivity), making it trivial to reverse-engineer the UI layout and precisely position overlays
For a financial app where transactions are irreversible, this is the highest-impact tapjacking scenario possible.
Pattern 2: The Certificate Trust Screen — One Tap to Enable MITM
App type: CalDAV/CardDAV sync client (open source)
This app syncs calendars, contacts, and tasks with self-hosted servers. When it encounters an untrusted TLS certificate, it shows a trust dialog asking the user to accept or reject the certificate.
That dialog is:
- Exported (
android:exported="true") - singleInstance launch mode (runs in its own task)
- No filterTouchesWhenObscured anywhere in the app
- No permission guard on the activity
<activity
android:excludeFromRecents=”true”
android:exported=”true”
android:launchMode=”singleInstance”
android:name=”[…].TrustCertificateActivity”/>
<activity
android:excludeFromRecents=”true”
android:exported=”true”
android:launchMode=”singleInstance”
android:name=”[…].TrustCertificateActivity”/>The attack chain:
- Attacker performs MITM on the sync connection (e.g., rogue WiFi)
- App encounters untrusted certificate → shows trust dialog
- Attacker's overlay app (already on device) draws over the dialog
- Overlay shows "Connection Error — Tap to Retry" covering the "Accept" button
- User taps "Retry" → actually accepts the attacker's certificate
- Certificate is permanently trusted — all future syncs go through attacker's proxy
- Calendars, contacts, and tasks are now fully interceptable
Why 'singleInstance' + exported amplifies this:
The activity runs in its own task, creating a predictable visual transition when it appears. Since the attacker controls the MITM (triggering the cert error) and the overlay, they have precise timing control. excludeFromRecents="true" means the user can't easily review what they just accepted.
One tap. Permanent MITM. All personal data exposed.
Pattern 3: The Email Client — Authorize Account Access
App type: Email client / Thunderbird for Android (open source)
This app has 10 exported components and handles OAuth authorization for email accounts (Gmail, Outlook, etc.). No filterTouchesWhenObscured on any screen.
The OAuth flow is the critical target:
<! - OAuth setup screen - no protection →
<activity android:name="[…].OAuthFlowActivity"/>
<! - OAuth management - singleTask →
<activity android:exported="false"
android:launchMode="singleTask"
android:name="net.openid.appauth.AuthorizationManagementActivity"/>
<! - OAuth redirect - EXPORTED →
<activity android:exported="true"
android:name="net.openid.appauth.RedirectUriReceiverActivity">The OAuth flow is the critical target:
<! - OAuth setup screen - no protection →
<activity android:name="[…].OAuthFlowActivity"/>
<! - OAuth management - singleTask →
<activity android:exported="false"
android:launchMode="singleTask"
android:name="net.openid.appauth.AuthorizationManagementActivity"/>
<! - OAuth redirect - EXPORTED →
<activity android:exported="true"
android:name="net.openid.appauth.RedirectUriReceiverActivity">The attack chain:
- User initiates account setup → OAuth consent screen opens
- Overlay covers the consent screen during the browser→app transition
- The
singleTaskOAuth management activity creates a predictable task switch - Overlay shows fake "Deny" button positioned over the real "Allow" button
- User thinks they're denying a suspicious permission → actually grants full email access
- OR: After OAuth redirect, overlay shows "Setup Complete!" covering actual authorization
Additional risk:
The app also exports MessageCompose — the email composition activity. An overlay could pre-fill the "To" field with an attacker's address while showing a different address visually, causing the user to send sensitive content to the wrong recipient.
Pattern 4: The Privacy App That Uses Overlays But Doesn't Protect Against Them
App type: Privacy-focused video frontend (open source)
This app exists specifically for privacy — users choose it to avoid tracking by major video platforms. Ironically, it:
- Requests SYSTEM_ALERT_WINDOW itself (for its floating popup video player)
- Uses
singleTaskon its main activity - Has zero
filterTouchesWhenObscuredanywhere - Has 9 exported components including a URL handler with empty
taskAffinity
The developers clearly understand overlay mechanics — they built a feature that uses them. But they don't protect their own app from the same technique.
The exported RouterActivity:
<activity android:excludeFromRecents=”true”
android:exported=”true”
android:taskAffinity=””
android:name=”[…].RouterActivity”>
<! — Handles YouTube URLs, shares, NFC →
</activity>
<activity android:excludeFromRecents=”true”
android:exported=”true”
android:taskAffinity=””
android:name=”[…].RouterActivity”>
<! — Handles YouTube URLs, shares, NFC →
</activity>This activity handles URL intents and shows download dialogs. It's the entry point when a user shares a video link to the app. Without overlay protection:
- An attacker can manipulate the apparent download destination
- Search queries can be captured by overlays on the search bar
- Subscription export can be redirected to attacker-accessible storage
The privacy contradiction:
Users chose this app to protect their viewing habits from tracking. Tapjacking enables exactly the surveillance they were trying to avoid — capture search terms, viewing patterns, and subscription data through overlays on the unprotected UI.
The Positive Example: A Password Manager Gets It Right
Among the 10 apps I scanned, exactly one had proper tapjacking protection: a password manager with filterTouchesWhenObscured="true" on 35 layouts — applied at the root level of every activity, covering all interactive elements.
<! — How it’s done correctly →
<FrameLayout
android:filterTouchesWhenObscured=”true”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”>
<! — All child views are protected →
</FrameLayout>
<! — How it’s done correctly →
<FrameLayout
android:filterTouchesWhenObscured=”true”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”>
<! — All child views are protected →
</FrameLayout>One attribute. Root layout. Every screen. That's all it takes.
The password manager protects: master password entry, database operations, entry creation, credential viewing. Every sensitive interaction is covered by a single design decision.
How to Check Your App
1. Search for the protection attribute
grep -rn “filterTouchesWhenObscured” res/layout/
grep -rn “filterTouchesWhenObscured” res/layout/If this returns nothing — you're vulnerable.
2. Check for programmatic protection
grep -rn “FLAG_WINDOW_IS_OBSCURED\|onFilterTouchEventForSecurity” . — include=”*.java” — include=”*.kt”
grep -rn “FLAG_WINDOW_IS_OBSCURED\|onFilterTouchEventForSecurity” . — include=”*.java” — include=”*.kt”3. Identify high-risk screens
Any screen where the user:
- Confirms a financial transaction
- Accepts a certificate or permission
- Authorizes OAuth access
- Enters credentials
- Exports sensitive data
- Makes a security-relevant decision
These MUST have overlay protection.
4. Check for compounding risks
# StrandHogg 2.0: singleTask without explicit taskAffinity
grep “singleTask” AndroidManifest.xml
grep “taskAffinity” AndroidManifest.xml
# StrandHogg 2.0: singleTask without explicit taskAffinity
grep “singleTask” AndroidManifest.xml
grep “taskAffinity” AndroidManifest.xmlExported activities that handle sensitive operations
grep -B2 -A5 ‘exported=”true”’ AndroidManifest.xml | grep activity
grep -B2 -A5 ‘exported=”true”’ AndroidManifest.xml | grep activityThe Fix
One-line fix for any layout:
<FrameLayout
android:filterTouchesWhenObscured=”true”
…>
<FrameLayout
android:filterTouchesWhenObscured=”true”
…>Apply to the ROOT element of every activity layout. Child views inherit the protection.
Programmatic fix (for dynamic views or Compose):
override fun onFilterTouchEventForSecurity(event: MotionEvent): Boolean {
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0) {
return false // Reject touch events when obscured
}
return super.onFilterTouchEventForSecurity(event)
}
override fun onFilterTouchEventForSecurity(event: MotionEvent): Boolean {
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0) {
return false // Reject touch events when obscured
}
return super.onFilterTouchEventForSecurity(event)
}For highest-value screens:
// Prevent screenshots and some overlay techniques
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
// Prevent screenshots and some overlay techniques
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)StrandHogg 2.0 mitigation:
<! — Prevent task injection →
<activity
android:taskAffinity=””
android:launchMode=”singleTask”
…>
<! — Prevent task injection →
<activity
android:taskAffinity=””
android:launchMode=”singleTask”
…>Why This Keeps Happening
-
It's not in the default template. Android Studio's activity templates don't include
filterTouchesWhenObscured. Developers have to know to add it. -
It's invisible during development. Unlike a crash or a broken layout, tapjacking vulnerability has no visible symptom during normal testing.
-
Security reviews skip XML. Penetration tests focus on network traffic and API calls. Code reviews focus on Java/Kotlin logic. The layout XML that controls touch event behavior is almost never audited.
-
StrandHogg awareness is low. Most developers don't know that
singleTaskwithout taskAffinity="" creates a task injection vulnerability that compounds tapjacking. -
One app gets it right. Out of 10 apps, only the password manager — the app with the most obvious need for security — implements the protection. The other 9 (including a cryptocurrency wallet) don't.
Key Takeaways
-
4 out of 10 apps I scanned had tapjacking vulnerabilities on their most sensitive screens — transaction confirmation, certificate trust, OAuth authorization, and download decisions.
-
The fix is literally one XML attribute. filterTouchesWhenObscured="true" on the root layout. One line, applied once per activity.
-
Financial apps are the highest risk. A cryptocurrency wallet with no overlay protection on the send screen means a single hijacked tap = irreversible fund loss.
-
singleTask + default taskAffinity = StrandHogg 2.0. This is a separate but compounding vulnerability that makes tapjacking easier to execute.
-
Privacy apps are ironic targets. An app that exists to protect user privacy but doesn't protect its own UI from overlays undermines its entire value proposition.
I'm a mobile security researcher specializing in Android application security. If your app handles sensitive user actions and you want to verify your tapjacking protection — reach out: yehor.mamaiev@gmail.com