Skip to content
Draft
57 changes: 57 additions & 0 deletions best-practices/MASTG-BEST-0029.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
title: Preventing Overlay Attacks
alias: preventing-overlay-attacks
id: MASTG-BEST-0029
platform: android
knowledge: [MASTG-KNOW-0022]
---

Apps should protect sensitive user interactions from overlay attacks by implementing appropriate defensive mechanisms. Overlay attacks (including tapjacking) occur when malicious apps place deceptive UI elements over legitimate app interfaces to trick users into unintended actions.

## Recommendation

Implement touch filtering to prevent touch events when the app's UI is obscured by another app. Use one or more of the following mechanisms:

1. **Set the layout attribute `android:filterTouchesWhenObscured="true"`** for sensitive views such as login buttons, payment confirmations, or permission requests. This filters touch events when the view is obscured.

2. **Call `setFilterTouchesWhenObscured(true)`** programmatically on sensitive views to enable touch filtering at runtime.

3. **Override `onFilterTouchEventForSecurity`** for more granular control and to implement custom security policies based on your app's specific requirements.

4. **Check motion event flags** such as `FLAG_WINDOW_IS_OBSCURED` (API level 9+) or `FLAG_WINDOW_IS_PARTIALLY_OBSCURED` (API level 29+) in touch event handlers to detect obscured windows and respond appropriately.

Apply these protections selectively to security-sensitive UI elements where user confirmation is critical, such as:

- Login and authentication screens
- Permission request dialogs
- Payment confirmation buttons
- Sensitive data entry fields
- Security settings changes

## Rationale

Without overlay protection, malicious apps can:

- Capture user credentials by overlaying fake login screens
- Trick users into granting dangerous permissions
- Intercept sensitive data entry
- Perform unauthorized actions by obscuring the true nature of UI elements

Touch filtering mechanisms help ensure that user interactions occur with the intended UI elements and not with overlays placed by malicious apps.

## Caveats and Considerations

- Touch filtering is not a complete solution on older Android versions that have system-level vulnerabilities. Apps should target modern API levels when possible.
- Some attacks, particularly those exploiting system-level vulnerabilities (for example, Toast Overlay on Android versions before 8.0), cannot be fully mitigated at the app level.
- Applying touch filtering too broadly may impact legitimate use cases where overlays are expected (for example, system dialogs, accessibility features).
- Users can still be tricked through social engineering even with touch filtering enabled. Apps should combine these protections with user education and clear UI indicators.
- For maximum protection, apps targeting older API levels should consider upgrading their `targetSdkVersion` to benefit from platform-level protections introduced in newer Android versions.

## References

- Android Developer Documentation: [Tapjacking](https://developer.android.com/privacy-and-security/risks/tapjacking)
- Android Developer Documentation: [View Security](https://developer.android.com/reference/android/view/View#security)
- Android Developer Documentation: [setFilterTouchesWhenObscured](https://developer.android.com/reference/android/view/View#setFilterTouchesWhenObscured(boolean))
- Android Developer Documentation: [onFilterTouchEventForSecurity](https://developer.android.com/reference/android/view/View#onFilterTouchEventForSecurity(android.view.MotionEvent))
- Android Developer Documentation: [FLAG_WINDOW_IS_OBSCURED](https://developer.android.com/reference/android/view/MotionEvent#FLAG_WINDOW_IS_OBSCURED)
- Android Developer Documentation: [FLAG_WINDOW_IS_PARTIALLY_OBSCURED](https://developer.android.com/reference/android/view/MotionEvent#FLAG_WINDOW_IS_PARTIALLY_OBSCURED)
52 changes: 52 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0083/MASTG-DEMO-0083.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
platform: android
title: Overlay Attack Protection Implementation
id: MASTG-DEMO-0083
code: [kotlin, java]
test: MASTG-TEST-0035
tools: [semgrep]
---

### Sample

This sample demonstrates different approaches to protecting against overlay attacks in Android apps. It includes both a vulnerable implementation (without protection) and secure implementations using various overlay protection mechanisms.

{{ MastgTest.kt # MastgTest_reversed.java }}

The code shows three buttons:

1. **Vulnerable button** - A Compose button without any overlay protection, making it susceptible to tapjacking attacks
2. **Protected button** - A traditional Android View Button with `filterTouchesWhenObscured = true` to block touches when the window is obscured
3. **Custom protected button** - A button with a custom implementation that overrides `onFilterTouchEventForSecurity` to manually check for the `FLAG_WINDOW_IS_OBSCURED` flag

### Steps

Let's run our semgrep rule against the decompiled code to detect overlay protection mechanisms.

{{ ../../../../rules/mastg-android-overlay-protection.yml }}

{{ run.sh }}

### Observation

The semgrep rule detected three instances of overlay protection mechanisms in the code:

{{ output.txt }}

The output shows:

1. Line 59: `setFilterTouchesWhenObscured(true)` - enabling touch filtering on the protected button
2. Lines 73-79: `onFilterTouchEventForSecurity` - custom override implementation
3. Line 74: Check for `FLAG_WINDOW_IS_OBSCURED` flag - detecting when the window is obscured

### Evaluation

The test partially passes and partially fails:

**FAIL:** The first button (lines 38-48 in the Kotlin code, not shown in the output) does not implement any overlay protection. This button performs a sensitive action (payment confirmation) and should be protected against overlay attacks.

**PASS:** The second button (line 59 in the decompiled output) properly implements overlay protection using `setFilterTouchesWhenObscured(true)`, which will filter touch events when the view is obscured by another window.

**PASS:** The third button (lines 73-79 in the decompiled output) implements custom overlay protection by overriding `onFilterTouchEventForSecurity` and manually checking the `FLAG_WINDOW_IS_OBSCURED` flag. This provides fine-grained control over how the app responds to overlay attempts.

In a real-world assessment, the vulnerable button should be flagged as a finding. Sensitive UI elements such as payment confirmations, permission grants, or authentication buttons should implement overlay protection using one of the demonstrated mechanisms.
94 changes: 94 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0083/MastgTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.owasp.mastestapp

import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView

// SUMMARY: This sample demonstrates different approaches to handling overlay attacks in Android apps,
// showing both vulnerable patterns and proper protections using filterTouchesWhenObscured.

const val MASTG_TEXT_TAG = "mastgTestText"

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MainScreen()
}
}
}

@Composable
fun MainScreen() {
Column(modifier = Modifier.padding(16.dp)) {
// FAIL: [MASTG-TEST-0035] Sensitive button without overlay protection
Button(
onClick = {
// Sensitive action: confirming a payment
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Text("Vulnerable: Confirm Payment")
}

// PASS: [MASTG-TEST-0035] Button with overlay protection using filterTouchesWhenObscured
AndroidView(
factory = { context ->
Button(context).apply {
text = "Protected: Confirm Payment"
filterTouchesWhenObscured = true
setOnClickListener {
// Sensitive action protected from overlay attacks
Toast.makeText(context, "Payment confirmed", Toast.LENGTH_SHORT).show()
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)

// PASS: [MASTG-TEST-0035] Custom view with manual obscured check
AndroidView(
factory = { context ->
object : Button(context) {
override fun onFilterTouchEventForSecurity(event: MotionEvent): Boolean {
if ((event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, filter the touch event
Toast.makeText(context, "Touch blocked - window obscured", Toast.LENGTH_SHORT).show()
return false
}
return super.onFilterTouchEventForSecurity(event)
}
}.apply {
text = "Custom Protection: Grant Permission"
setOnClickListener {
// Sensitive permission grant protected by custom implementation
Toast.makeText(context, "Permission granted", Toast.LENGTH_SHORT).show()
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.owasp.mastestapp;

import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.Button;
import android.widget.Toast;
import androidx.activity.ComponentActivity;
import androidx.compose.foundation.layout.ColumnKt;
import androidx.compose.foundation.layout.PaddingKt;
import androidx.compose.material3.ButtonKt;
import androidx.compose.material3.TextKt;
import androidx.compose.runtime.Composer;
import androidx.compose.ui.Modifier;
import androidx.compose.ui.unit.Dp;
import androidx.compose.ui.viewinterop.AndroidViewKt;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;
import kotlin.jvm.internal.Lambda;

public final class MainActivity extends ComponentActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.enableEdgeToEdge();
this.setContent(new Lambda(0) {
public final void invoke(Composer $composer, int $changed) {
MainScreenKt.MainScreen($composer, 0);
}
});
}
}

public final class MainScreenKt {
public static final void MainScreen(Composer $composer, int $changed) {
Composer $composer2 = $composer.startRestartGroup(0);
if ($changed == 0) {
if (!$composer2.getSkipping()) {
ColumnKt.m586Column(PaddingKt.m565padding(Modifier.Companion, Dp.m5307constructorimpl(16)), null, null, new Lambda(3) {
public final void invoke(ColumnScope $this$Column, Composer $composer, int $changed) {
Composer $composer2 = $composer;
ColumnScope columnScope = $this$Column;

// FAIL: [MASTG-TEST-0035] Vulnerable button without overlay protection
ButtonKt.m1334Button(new Lambda(0) {
public final void invoke() {
// Sensitive action: confirming a payment
}
}, PaddingKt.m565padding(Modifier.Companion.fillMaxWidth(), Dp.m5307constructorimpl(8)), false, null, null, null, null, null, new Lambda(3) {
public final void invoke(RowScope $this$Button, Composer $composer, int $changed) {
TextKt.m3574Text("Vulnerable: Confirm Payment", null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer, 0, 0, 131070);
}
}, $composer2, 805306368, 508);

// PASS: [MASTG-TEST-0035] Button with overlay protection
AndroidViewKt.m4555AndroidView(new Lambda(1) {
public final Button invoke(Context context) {
Button button = new Button(context);
button.setText("Protected: Confirm Payment");
button.setFilterTouchesWhenObscured(true);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Toast.makeText(context, "Payment confirmed", 0).show();
}
});
return button;
}
}, PaddingKt.m565padding(Modifier.Companion.fillMaxWidth(), Dp.m5307constructorimpl(8)), null, null, $composer2, 3080, 12);

// PASS: [MASTG-TEST-0035] Custom view with manual obscured check
AndroidViewKt.m4555AndroidView(new Lambda(1) {
public final Button invoke(Context context) {
return new Button(context) {
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
if ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
Toast.makeText(this.getContext(), "Touch blocked - window obscured", 0).show();
return false;
}
return super.onFilterTouchEventForSecurity(event);
}

{
this.setText("Custom Protection: Grant Permission");
this.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Toast.makeText(Button.this.getContext(), "Permission granted", 0).show();
}
});
}
};
}
}, PaddingKt.m565padding(Modifier.Companion.fillMaxWidth(), Dp.m5307constructorimpl(8)), null, null, $composer2, 3080, 12);
}
}, $composer2, 438, 6);
}
}
}
}
29 changes: 29 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0083/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@


┌─────────────────┐
│ 4 Code Findings │
└─────────────────┘

MastgTest_reversed.java
❱ rules.mastg-android-overlay-protection-setfiltertoucheswhenobscured
[MASVS-PLATFORM-3] setFilterTouchesWhenObscured is used to protect against overlay attacks

59┆ button.setFilterTouchesWhenObscured(true);

❱ rules.mastg-android-overlay-protection-onfiltertoucheventforsecurity
[MASVS-PLATFORM-3] onFilterTouchEventForSecurity is overridden for custom overlay protection

73┆ public boolean onFilterTouchEventForSecurity(MotionEvent event) {
74┆ if ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
75┆ Toast.makeText(this.getContext(), "Touch blocked - window obscured", 0).show();
76┆ return false;
77┆ }
78┆ return super.onFilterTouchEventForSecurity(event);
79┆ }

❱ rules.mastg-android-overlay-protection-flag-window-is-obscured
[MASVS-PLATFORM-3] FLAG_WINDOW_IS_OBSCURED is checked to detect overlay attacks

74┆ if ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
⋮┆----------------------------------------
74┆ if ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
4 changes: 4 additions & 0 deletions demos/android/MASVS-PLATFORM/MASTG-DEMO-0083/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

# Run semgrep to detect overlay protection mechanisms
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-overlay-protection.yml ./MastgTest_reversed.java --text -o output.txt
Loading
Loading