CVE-2025-48598 AI-generated analysis
platform/packages/apps/Settings, biometrics (face enrollment: BiometricEnrollBase, FaceEnroll trampoline, FaceEnrollIntroduction)
Settings biometric enrollment now only honors Intent.EXTRA_USER_ID when the activity was launched by the Settings package itself, and the FaceEnroll trampoline strips EXTRA_USER_ID before forwarding extras from external callers, so a third-party app can no longer steer face enrollment to another user id.
Root cause
BiometricEnrollBase.onCreate read mUserId directly from getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId()) with no check on who supplied the intent. The FaceEnroll trampoline (FaceEnroll.kt; the commit message refers to the trampoline added in change Ie469c47005afb941f5646a2f790736362c23c697) copies all extras of its incoming intent into nextIntent via putExtras(intent) and starts the next enrollment activity, so the next activity is started by Settings while carrying extras chosen by an external caller. FaceEnrollIntroduction.launchFaceSettingsActivity also re-read EXTRA_USER_ID from the raw intent and forwarded it to Settings.FaceSettingsInternalActivity. Together these let a caller outside Settings control the user id used in face enrollment flows (bulletin: High, EoP).
How the fix works
- src/com/android/settings/biometrics/BiometricEnrollBase.java:onCreate: calls getLaunchedFromPackage(); if it is null or not equal to getPackageName(), mUserId is forced to UserHandle.myUserId(). Only when the launcher is the Settings package itself is Intent.EXTRA_USER_ID read from the intent (defaulting to myUserId). This applies to every BiometricEnrollBase subclass.
- src/com/android/settings/biometrics/face/FaceEnroll.kt:onCreate: after nextIntent.putExtras(intent), if launchedFromProvider() (default: launchedFromPackage) is not packageName, nextIntent.removeExtra(Intent.EXTRA_USER_ID) is called before startActivityForResult, so the trampoline no longer passes an external EXTRA_USER_ID into a Settings-originated intent. launchedFromProvider is a @VisibleForTesting lambda that tests can replace.
- src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java:launchFaceSettingsActivity: replaces the re-read of EXTRA_USER_ID from getIntent() with the already validated mUserId field when building the intent for Settings.FaceSettingsInternalActivity.
- Tests: FaceEnrollTest.kt adds testFinishAndLaunchDefaultActivity_fromExternal_dropUser (external launcher 'com.foo.bar': EXTRA_USER_ID absent, only 1 extra) and asserts that the same-package case keeps EXTRA_USER_ID. FaceEnrollIntroductionTest and MultiBiometricEnrollHelperTest (via a new TestFaceEnrollIntroduction subclass) override getLaunchedFromPackage() to return getPackageName() so the existing user-id assertions still pass.
Completeness
- Covers: the base-class check in BiometricEnrollBase.onCreate gates EXTRA_USER_ID for all enrollment activities deriving from BiometricEnrollBase, not just the face ones changed here. The FaceEnroll trampoline drop and the FaceEnrollIntroduction field use add defense in depth on the face path.
- The trampoline only strips EXTRA_USER_ID. All other extras from an external caller (e.g. EXTRA_KEY_CHALLENGE_TOKEN, EXTRA_KEY_CHALLENGE, EXTRA_KEY_SENSOR_ID, EXTRA_KEY_GK_PW_HANDLE, multi-biometric extras) are still forwarded unchanged via putExtras(intent). This change does not validate them.
- FaceEnrollIntroduction.launchFaceSettingsActivity still copies EXTRA_KEY_CHALLENGE_TOKEN, EXTRA_KEY_CHALLENGE and EXTRA_KEY_SENSOR_ID straight from getIntent() into the intent for FaceSettingsInternalActivity. Only the user id was switched to the validated field.
- BiometricEnrollBase.onCreate reads mChallenge, mSensorId, mToken and mFromSettingsSummary from the intent without a caller check. This fix only guards mUserId.
- The caller check relies on getLaunchedFromPackage() returning the real launcher. A null result is treated as untrusted (falls back to myUserId), which fails safe.
Behavior changes
- Enrollment activities launched by any package other than Settings (or with an unknown launcher) now ignore Intent.EXTRA_USER_ID and operate on UserHandle.myUserId().
- FaceEnroll, when launched from outside Settings, forwards the incoming extras minus EXTRA_USER_ID to the next enrollment activity.
- FaceEnrollIntroduction passes the validated mUserId rather than the raw intent value to FaceSettingsInternalActivity.
- Robolectric tests of BiometricEnrollBase subclasses must now override getLaunchedFromPackage() to return the Settings package name to exercise EXTRA_USER_ID handling.
Per major
- Android 14: not affected per the bulletin (not listed); at android-security-14.0.0_r29, 7/8 lines of the patched region are present. Which line is missing and whether the fix is effectively present is not shown.
- Android 15: not affected per the bulletin (not listed); at android-security-15.0.0_r17, 8/8 lines of the patched region are present
- Android 16: same fix: commit 83447688f8e3 in android-security-16.0.0_r3 (cherry-pick of internal c07d03e5/e0476f87, Change-Id I49f4a980a0c9ed80cc2e370a42bd60425d018a4c); 8/8 lines present at android-security-16.0.0_r8
- Android 17: not affected per the bulletin (not listed); at android-security-17.0.0_r1, 8/8 lines of the patched region are present
Unknowns
- Whether FaceEnroll or the other enrollment activities are exported and which intent filters expose them to third-party apps.
- Which other enrollment activities (e.g. fingerprint) derive from BiometricEnrollBase and so get the base-class check.
- The concrete privileged action possible with a controlled user id (the commit only says privilege escalation via the trampoline).
- Why Android 14/15/17 are not listed while the patched region is present there (fix merged earlier or code differs), and which line of the patched region is missing in 14.