
Why switching to a modern UI toolkit doesn’t automatically secure your app, and how to protect your users.
Many developers mistakenly believe that adopting a modern stack (Kotlin + Jetpack Compose) automatically makes an application secure. However, vulnerabilities like Task Hijacking or Intent Injection do not live in the UI layer — they reside in the architecture of how Android components interact. Furthermore, Compose introduces its own nuances regarding input event handling and the Window lifecycle.
In this article, we will break down 3 critical attacks that could threaten your application and show you how to defend against them using a declarative approach.
1. Task Hijacking (StrandHogg) & taskAffinity
This vulnerability (often known as “StrandHogg”) allows a malicious application to hijack your app’s task stack. A user clicks on your legitimate app icon (e.g., Banking), but instead, a malicious Activity opens up, displaying a phishing window that looks exactly like your login screen.
How it works
The malware manipulates the android:taskAffinity attribute in its manifest, setting it to match your application’s package name. If your app does not strictly define how it handles tasks, the system might launch the malware’s Activity inside your app’s task stack.
How to protect yourself
Jetpack Compose cannot help here, as this is a configuration issue in the Manifest. However, since most Compose apps follow the SingleActivity architecture, protection is straightforward.
The Solution in AndroidManifest.xml:
You need to isolate your application’s task.
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
>
Note: For critical applications (wallets, banking), it is recommended to add a runtime check at startup to ensure the Activity was not launched inside a foreign task history.
2. Tapjacking (Overlay Attacks / Pixnapping)
Tapjacking occurs when a malicious app draws a transparent layer (overlay) on top of your interface. The user thinks they are clicking on a harmless image in a game or utility app, but the touch event passes through (“pixnapping”) and hits a button in your app (like “Confirm Transfer”) hidden underneath.
In the XML world, we used the attribute android:filterTouchesWhenObscured. In Jetpack Compose, we don’t have direct XML attributes for every Composable, but we do have access to the Window or the root View.
Protection in Jetpack Compose
There are two approaches: Global and Local.
Method A: Global Protection (Recommended) Apply the filter to the root View hosting your Compose content.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.ui.platform.LocalView
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Get the current Android View
val view = LocalView.current
// Side Effect to configure the View
LaunchedEffect(Unit) {
// If the window is obscured by another window, ignore touches
view.filterTouchesWhenObscured = true
}
MyAppTheme {
MainScreen()
}
}
}
}
Method B: Component-level Protection If you don’t want to block the entire screen, you can use pointerInteropFilter (experimental) or configure WindowManager parameters for Dialogs.
Pitfall: Starting with Android 12 (API 31), the system automatically blocks touches from “untrusted” overlays. However, for full backward compatibility (minSdk < 31), manually setting this flag is mandatory.
3. Intent Injection & Deep Links in Navigation Compose
Modern Compose apps rely heavily on Deep Links for navigation. If you accept parameters via URL (e.g., myapp://reset_password?token=…) and do not validate them, an attacker can inject malicious data.
Intent Injection can force your app to perform an action the user has permissions for but didn’t initiate, or redirect data flow to a server controlled by a hacker.
The Vulnerability in Compose Navigation
// VULNERABLE CODE
composable(
route = "webview?url={url}",
arguments = listOf(navArgument("url") { type = NavType.StringType })
) { backStackEntry ->
val url = backStackEntry.arguments?.getString("url")
// DANGER: Opening any URL passed in the deeplink
MyWebView(url)
}
An attacker could create a link myapp://webview?url=file:///data/data/com.myapp/databases/secret.db. If the WebView is misconfigured, they could access local files.
How to protect yourself
- Input Validation: Never trust input data in NavHost.
- Verify App Links: Use Android App Links (with domain verification) so only your app can open your links.
Secure Implementation:
composable(
route = "payment?account={account}",
arguments = listOf(navArgument("account") { type = NavType.StringType })
) { entry ->
val account = entry.arguments?.getString("account") ?: ""
// Validate before rendering
if (isValidAccountNumber(account)) {
PaymentScreen(account)
} else {
// Log attack attempt and redirect to error
ErrorScreen("Invalid parameters")
}
}
fun isValidAccountNumber(acc: String): Boolean {
// Regex check: digits only, fixed length
return acc.matches(Regex("^\d{16,20}$"))
}
Bonus: Protecting Data in Recents (Screenshots)
A common data leak occurs via the “Recent Apps” screen, where screenshots of your app are visible — potentially revealing card balances or private chats.
In Compose, this is solved elegantly using DisposableEffect. You can hide content only on sensitive screens rather than the entire app.
@Composable
fun SecureScreen() {
val context = LocalContext.current
val window = (context as? Activity)?.window
DisposableEffect(Unit) {
// Add flag when entering the screen
window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
onDispose {
// Remove flag when leaving
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
// Your UI (Balance, PIN code, etc.)
Box(modifier = Modifier.fillMaxSize()) {
Text("Sensitive Data Here")
}
}
Limitations and Pitfalls
- Accessibility Services: This is the “Achilles’ Heel” of Android security. Malware often requests “Accessibility” permissions under the guise of “cleaning memory.” If granted, the malware can read screen content (Compose’s Text is accessible by default) and click buttons on behalf of the user.
- Mitigation: You can check for active Accessibility services, but Google generally discourages blocking them entirely as it discriminates against users with disabilities.
- Clipboard: Compose provides a convenient ClipboardManager. Remember that any text copied to the clipboard (OTP codes, passwords) is accessible to any app (pre-Android 10) or the current active app (Android 10+).
- Logs: Ensure your data classes used in Compose State do not print PII (Personally Identifiable Information) to Logcat during debugging or crashes via the auto-generated toString().
4. Security Checklist
Before pushing your app to production, run through this list. It will save you from 90% of common attacks.
🤖 Android Manifest & Config
- [ ] Exported Components: Ensure all <activity>, <service>, <receiver> tags have android:exported=”false” unless explicitly required.
- [ ] Task Affinity: Secondary Activities (if any) should have android:taskAffinity=”” to prevent StrandHogg.
- [ ] App Links: Use android:autoVerify=”true” for Deep Links to claim domain ownership.
- [ ] Backup: Set android:allowBackup=”false” (or configure fullBackupContent) if sensitive tokens are stored in SharedPreferences without encryption.
🎨 Compose UI & Interaction
- [ ] Overlays: On critical screens (password entry, payment confirmation), enable filterTouchesWhenObscured = true.
- [ ] Screenshots: Apply FLAG_SECURE on screens displaying PII.
- [ ] WebView: If using AndroidView with WebView, disable file:// access (setAllowFileAccess(false)) and JavaScript if not needed.
- [ ] TextFields: Password fields use VisualTransformation.Password and KeyboardType.Password (disables auto-correct and dictionary learning).
💾 Data & Navigation
- [ ] Navigation Arguments: All arguments from Deep Links are validated (Regex/Format check, not just null check).
- [ ] Logs: HTTP request body logging is disabled in release builds (OkHttp Level NONE) and println() with sensitive data is removed.
- [ ] Local Storage: Use EncryptedSharedPreferences (Jetpack Security) instead of plain SP or DataStore.
5. Code Snippets
Here are a few utilities to simplify adding security to your Compose project.
A. Custom Modifier for Tapjacking Protection
Instead of writing logic in the Activity, create a reusable Modifier.
import android.view.View
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.LocalView
/**
* Blocks clicks on the component if it is obscured by
* another (potentially malicious) window or overlay.
*/
fun Modifier.preventOverlayClicks(): Modifier = composed {
val view = LocalView.current
// Side-effect to configure View.
// Note: This affects the host View, not just the component.
androidx.compose.runtime.DisposableEffect(Unit) {
val originalState = view.filterTouchesWhenObscured
view.filterTouchesWhenObscured = true
onDispose {
view.filterTouchesWhenObscured = originalState
}
}
this
}
// Usage:
Button(
onClick = { doPayment() },
modifier = Modifier.preventOverlayClicks()
) {
Text("Pay Now")
}
B. Safe Data Logging (No PII leaks)
Prevents data class auto-generated toString() from leaking passwords.
/**
* Wrapper for sensitive data.
* toString() returns a mask, preventing leaks in Logcat.
*/
data class Sensitive<T>(val value: T) {
override fun toString(): String = "***REDACTED***"
// Explicit unmasking when needed (e.g., for API calls)
fun unmask(): T = value
}
// Usage in State
data class LoginState(
val username: String,
val password: Sensitive<String> // Log.d(state) will not leak the password
)
C. Secure WebView Setup
WebView is a frequent attack vector. Here is a secure initialization pattern.
@Composable
fun SecureWebView(url: String) {
AndroidView(factory = { context ->
android.webkit.WebView(context).apply {
settings.apply {
javaScriptEnabled = false // Enable only if necessary!
allowFileAccess = false // Block local FS access
allowContentAccess = false // Block Content Providers
// Prevent caching of sensitive pages
cacheMode = android.webkit.WebSettings.LOAD_NO_CACHE
saveFormData = false
}
}
}, update = { view ->
view.loadUrl(url)
})
}
Conclusion
Jetpack Compose simplifies UI creation, but it does not nullify Android security principles.
- Use filterTouchesWhenObscured to prevent clickjacking.
- Configure taskAffinity and launchMode in your Manifest.
- Validate all arguments in Navigation.
- Hide sensitive screens using FLAG_SECURE.
Security is not a feature; it is a process. Embed these checks into your base Composable components, and you won’t have to think about them every time.
Android Security in the Age of Jetpack Compose: From Task Hijacking to Tapjacking was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.




This Post Has 0 Comments