Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions MOBILE_CLIENT/ANDROID/_LOW/APK_MISSING_FLAG_SECURE/context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Imagine a healthcare or financial application that carefully protects its main screen with `FLAG_SECURE` so that screenshots and screen recordings of account balances, messages or a live video visit come out black. The same application also has a *second* `Activity` — a lock-screen incoming-call window that lights up the screen when a provider or caller rings, showing the caller's name and identifier right on the lock screen. Because `FLAG_SECURE` is a per-window flag and that second window never sets it, a co-resident application's `MediaProjection` session (started after the user taps "Start now" on the consent dialog), the device's stock screen recorder, or a screenshot taken while the incoming-call UI is visible captures the caller's name and identifier — sensitive, individually identifiable health or personal data — in plain text, on the very same production build that protects the main screen.

### Why it happens

* **`FLAG_SECURE` is per-Window, not per-app.** Setting it on one `Activity`'s window does not protect any other `Activity`'s window, even within the same process.
* **No shared base class.** When every `Activity` extends a framework class (such as `ComponentActivity`) directly, the flag cannot be inherited; each sensitive `Activity` must set it explicitly, and it is easy to forget a secondary window such as a lock-screen, notification-triggered or exported `Activity`.
* **Lock-screen windows are easy to miss.** Lock-screen `Activity` windows typically focus on `setShowWhenLocked(true)`, `setTurnScreenOn(true)` or `FLAG_KEEP_SCREEN_ON | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON` to display over the keyguard, and `FLAG_SECURE` is simply never added to that code path.
* **Build guards hide one variant.** Gating the flag on `if (!BuildConfig.DEBUG)` leaves at least the `debug` build variant unprotected and signals that screen capture is intentionally allowed on some variant — a risky stance for any window that renders sensitive data.

### Real-world impact

* **PHI / PII exposure.** A lock-screen incoming-call UI that renders a provider/caller name and identifier exposes individually identifiable health information (the identity of a care provider and the fact that a care encounter is occurring), which is HIPAA-class data in a healthcare context.
* **No remote vector, but no attach required either.** Exploitation is not network-reachable and requires an external capture mechanism (the user's own recorder/screenshot, or a co-resident app's `MediaProjection` session after user consent) triggered while the sensitive window is on screen. On the non-debuggable production release build, capture succeeds with no debugger attach, frida or rooted device required.
* **Transient but real.** The sensitive window (for example an incoming-call lock-screen UI) is transient, which limits the capture window, but the exposed data is sensitive and the gap is present on every build variant including the shipped production build.

### Business impact

For organizations operating in healthcare, financial or other regulated sectors, the consequences can include regulatory exposure (HIPAA / GDPR / CCPA), reputational damage from a privacy incident, and loss of user trust. Applying `FLAG_SECURE` unconditionally to every sensitive-data window is a low-cost, high-value hardening control.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
`FLAG_SECURE` (`WindowManager.LayoutParams.FLAG_SECURE`) is a **per-`Window`** flag. When it is set on an `Activity`'s `Window`, the Android platform (WindowManager / SurfaceFlinger) returns a black/empty bitmap for that window to `MediaProjection`, `screencap` (e.g. `adb shell screencap`, `screenrecord`) and any `SurfaceView`-mirroring / `VirtualDisplay` consumer, blocks or blackens screenshots taken with the hardware screenshot key combination or screenshot APIs, and hides the window's content in the recent-apps / overview (Recents) thumbnail.

Because the flag is **per-Window**, an `Activity` window that does **not** set it remains fully capturable even when another `Activity`'s window in the same application sets it. Coverage must therefore be evaluated independently for every `Activity` window that renders sensitive data — there is no inheritance: each `Activity` extends a framework base class (for example `androidx.activity.ComponentActivity`) directly, so a flag applied in one `Activity`'s `onCreate` does not propagate to any other `Activity`.

This vulnerability is raised when an `Activity` that displays sensitive data (for example a lock-screen / incoming-call / telehealth window that renders a caller or provider name and identifier, or any window presenting PHI, PII, credentials, financial or care-team data) never calls `window.setFlags(FLAG_SECURE, FLAG_SECURE)` (nor `window.addFlags(FLAG_SECURE)`) on any code path, including the production release build. Such a window is capturable on every build variant — including a non-debuggable, release-signed production build shipped via Google Play — by an external capture mechanism: the device's own stock screen recorder or screenshot key combination, `adb shell screencap` on a debuggable build, or a `MediaProjection` session started by a co-resident application after the user grants consent. No rooted device, debugger attach, or frida instrumentation is required.

The exposure is often introduced when an application protects its main `Activity` with `FLAG_SECURE` but forgets to protect a *separate* `Activity` window — for instance a lock-screen `Activity` declared in the manifest with `android:showOnLockScreen="true"` and `android:exported="true"` that draws sensitive content into its own `ComponentActivity` window via `setContent {}`. That separate window only receives unrelated flags such as `FLAG_KEEP_SCREEN_ON` and `FLAG_ALLOW_LOCK_WHILE_SCREEN_ON` (or uses `setShowWhenLocked(true)` / `setTurnScreenOn(true)` on Android O_MR1+) and never receives `FLAG_SECURE`.

A common, related manifestation is gating `FLAG_SECURE` behind a debug guard:

=== "Kotlin"
```kotlin
if (!BuildConfig.DEBUG) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
}
```

While AGP only sets `BuildConfig.DEBUG = true` for the literal `debug` build type (so this guard still protects custom build types such as `staging`, `prod` and `release`), the guard leaves the `debug` build variant unprotected and signals an intent to allow screen capture on at least one variant. Any `Activity` that renders sensitive data should set `FLAG_SECURE` unconditionally rather than gating it on the build variant.
40 changes: 40 additions & 0 deletions MOBILE_CLIENT/ANDROID/_LOW/APK_MISSING_FLAG_SECURE/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"risk_rating": "low",
"short_description": "An Activity window that displays sensitive data does not set the FLAG_SECURE window flag, allowing its content to be captured by screenshots, screen recording or MediaProjection.",
"references": {
"Android Developer FLAG_SECURE": "https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE",
"Android Developer Window setFlags": "https://developer.android.com/reference/android/view/Window#setFlags(int,%20int)",
"Android Developer setShowWhenLocked": "https://developer.android.com/reference/android/app/Activity#setShowWhenLocked(boolean)",
"Android Developer activity element (showOnLockScreen / exported)": "https://developer.android.com/guide/topics/manifest/activity-element",
"Android Developer MediaProjection": "https://developer.android.com/reference/android/media/projection/MediaProjection",
"CWE-200: Exposure of Sensitive Information to an Unauthorized Actor": "https://cwe.mitre.org/data/definitions/200.html",
"OWASP MASVS-STORAGE-2": "https://mas.owasp.org/MASVS/controls/MASVS-STORAGE-2/",
"OWASP MASTG-BEST-0016: Use SECURE_FLAG to Prevent Screenshots and Screen Recording": "https://mas.owasp.org/MASTG/best-practices/MASTG-BEST-0016/"
},
"title": "Application window missing FLAG_SECURE screen-capture protection",
"privacy_issue": true,
"security_issue": true,
"categories": {
"OWASP_MASVS_L2": [
"MSTG_STORAGE_9"
],
"OWASP_MASVS_v2_1": [
"MASVS_STORAGE_2"
],
"OWASP_MOBILE_TOP_10": [
"M6_2024"
],
"HIPAA_CONTROLS": [
"SECURITY212",
"SECURITY215"
],
"GDPR": [
"ART_5",
"ART_32"
],
"SOC2_CONTROLS": [
"CC_6_1",
"CC_7_1"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Add `FLAG_SECURE` to every `Activity` window that displays sensitive data, on every code path and build variant. Apply the flag in `onCreate`, before `setContent {}` draws any UI, so the protection is in place from the first rendered frame.

### Immediate mitigation

=== "Kotlin"
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
showOnLockScreen()
super.onCreate(savedInstanceState)
// Block screenshots, screen recording and MediaProjection of this window on every build variant.
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
// ... existing extras handling and setContent ...
}
```

Set the flag **unconditionally** — do not gate it on `BuildConfig.DEBUG`. Keep any existing keep-screen-on / allow-lock-while-screen-on logic; `FLAG_SECURE` is additive and compatible with `FLAG_KEEP_SCREEN_ON`, `FLAG_ALLOW_LOCK_WHILE_SCREEN_ON`, `setShowWhenLocked(true)` and `setTurnScreenOn(true)`.

### Permanent fix

Introduce a shared base `ComponentActivity` that sets `FLAG_SECURE` in `onCreate` on every build variant, and have every sensitive-data `Activity` extend it. This removes the per-`Activity` reliance on remembering to set the flag and prevents the same gap recurring for future Activities.

=== "Kotlin"
```kotlin
abstract class SecureComponentActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
}
}

class IncomingCallActivity : SecureComponentActivity() { /* ... */ }
class MainActivity : SecureComponentActivity() { /* ... */ }
```

### Prevent regressions

Add an instrumentation test or a static check (lint / detekt / unit test) asserting that every `Activity` in the sensitive-data set sets `FLAG_SECURE` on its window in `onCreate` (for example assert `window.attributes.flags and FLAG_SECURE != 0`), so a new sensitive `Activity` cannot ship without the flag. A repository-wide search for `FLAG_SECURE` should return a match in every sensitive `Activity` (or the shared base class).
Loading