CVE-2026-79213
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java |
modified | |
ifchrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.javachrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.javachrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java
Patch
From ab9ee6d092bd527ba910c995376f3fc1f64063a7 Mon Sep 17 00:00:00 2001 From: Nate Chapin <[email protected]> Date: Mon, 20 Jul 2026 13:03:13 -0700 Subject: [PATCH] [TWA] Derive precise geolocation tier from FINE OS grant InstalledWebappBridge mapped a single any-of ACCESS_COARSE_LOCATION / ACCESS_FINE_LOCATION grant to both fields of GeolocationSetting, so a TWA whose wrapper app held only ACCESS_COARSE_LOCATION was reported as precise=Allowed. Thread a separate preciseSetting through the InstalledWebappBridge.Permission POD, populated from a new hasAndroidFineLocationPermission() check, and use it for the precise member of GeolocationSetting in GetInstalledWebappPermissions(). The approximate tier keeps the existing any-of semantics. TAG=agy CONV=b71eacbb-9620-4275-a780-1697b5d00cfd Fixed: 513119757 Change-Id: I6186412fe145d115309637db0961febe842d088e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8112120 Reviewed-by: Dibyajyoti Pal <[email protected]> Reviewed-by: Peter Conn <[email protected]> Commit-Queue: Nate Chapin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1664876} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java index 40b838d..4f9475e 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java @@ -31,19 +31,24 @@ * A POD class to store the combination of a permission setting and the origin the permission is * relevant for. * - * It would make more sense for this to be a subclass of - * {@link InstalledWebappPermissionManager} or a top level class. Unfortunately for the JNI - * tool to be able to handle passing a class over the JNI boundary the class either needs to be - * in this file or imported explicitly. Our presubmits don't like explicitly importing classes - * that we don't need to, so it's easier to just let the class live here. + * <p>It would make more sense for this to be a subclass of {@link + * InstalledWebappPermissionManager} or a top level class. Unfortunately for the JNI tool to be + * able to handle passing a class over the JNI boundary the class either needs to be in this + * file or imported explicitly. Our presubmits don't like explicitly importing classes that we + * don't need to, so it's easier to just let the class live here. */ static class Permission { public final Origin origin; + // The primary permission setting. For Geolocation, this represents the approximate setting. public final @ContentSetting int setting; + // The precise permission setting. Only used for Geolocation. + public final @ContentSetting int preciseSetting; - public Permission(Origin origin, @ContentSetting int setting) { + public Permission( + Origin origin, @ContentSetting int setting, @ContentSetting int preciseSetting) { this.origin = origin; this.setting = setting; + this.preciseSetting = preciseSetting; } } @@ -81,6 +86,11 @@ } @CalledByNative + private static int getPreciseSettingFromPermission(Permission permission) { + return permission.preciseSetting; + } + + @CalledByNative private static void decidePermission( @ContentSettingsType.EnumType int type, @JniType("std::string") String originUrl, diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java index 41d82165..7ff0084 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java @@ -77,7 +77,19 @@ @ContentSetting int setting = getPermission(type, origin); if (setting != ContentSetting.DEFAULT) { - permissions.add(new InstalledWebappBridge.Permission(origin, setting)); + @ContentSetting int preciseSetting = setting; + if (type == ContentSettingsType.GEOLOCATION_WITH_OPTIONS) { + String packageName = getDelegatePackageName(origin); + Boolean fineEnabled = hasAndroidFineLocationPermission(packageName); + if (setting == ContentSetting.ALLOW) { + preciseSetting = + (fineEnabled != null && fineEnabled) + ? ContentSetting.ALLOW + : ContentSetting.BLOCK; + } + } + permissions.add( + new InstalledWebappBridge.Permission(origin, setting, preciseSetting)); } } @@ -245,6 +257,14 @@ } /** + * Returns whether the delegate application for the origin has Android fine location permission, + * or {@code null} if it does not exist or did not request fine location permission. + */ + public static @Nullable Boolean hasAndroidFineLocationPermission(@Nullable String packageName) { + return hasAndroidPermissions(packageName, new String[] {ACCESS_FINE_LOCATION}); + } + + /** * Returns whether the delegate application for the origin has any of specific Android * permissions, or {@code null} if it does not exist or did not request those permissions. */ diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java index de210a1..b605d70 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java @@ -12,6 +12,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,9 +35,12 @@ import org.robolectric.shadows.ShadowPackageManager; import org.robolectric.util.ReflectionHelpers; +import org.chromium.base.ActivityState; +import org.chromium.base.ApplicationStatus; import org.chromium.base.FeatureOverrides; import org.chromium.base.test.BaseRobolectricTestRule; import org.chromium.base.test.util.Feature; +import org.chromium.chrome.browser.customtabs.CustomTabActivity; import org.chromium.chrome.browser.webapps.WebappRegistry; import org.chromium.components.content_settings.ContentSetting; import org.chromium.components.content_settings.ContentSettingsType; @@ -45,6 +49,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.Collections; /** Tests for {@link InstalledWebappPermissionManager}. */ @RunWith(ParameterizedRobolectricTestRunner.class) @@ -237,4 +242,89 @@ .setStateForOrigin( eq(mOrigin), eq(PACKAGE_NAME), anyString(), eq(mType), eq(settingValue)); } + + @Test + @Feature("TrustedWebActivities") + public void getPermissions_distinguishesPreciseFromApproximate() { + if (mType != ContentSettingsType.GEOLOCATION_WITH_OPTIONS) { + return; + } + + // Mock store to return our origin. + when(mStore.getStoredOrigins()).thenReturn(Collections.singleton(mOrigin.toString())); + + // Mock CustomTabActivity. + CustomTabActivity activity = mock(CustomTabActivity.class); + when(activity.isInTwaMode()).thenReturn(true); + + // Register activity with ApplicationStatus. + // We need to transition it to CREATED then RESUMED to mimic lifecycle. + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.CREATED); + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.RESUMED); + + try { + // Scenario 1: Coarse only granted. + { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = PACKAGE_NAME; + packageInfo.requestedPermissions = new String[] {ACCESS_COARSE_LOCATION}; + packageInfo.requestedPermissionsFlags = + new int[] {PackageInfo.REQUESTED_PERMISSION_GRANTED}; + mShadowPackageManager.installPackage(packageInfo); + + setStoredLocationPermission(ContentSetting.ALLOW); + + InstalledWebappBridge.Permission[] permissions = + InstalledWebappPermissionManager.getPermissions(mType); + assertEquals(1, permissions.length); + assertEquals(mOrigin, permissions[0].origin); + assertEquals(ContentSetting.ALLOW, permissions[0].setting); // approximate + assertEquals(ContentSetting.BLOCK, permissions[0].preciseSetting); // precise + } + + // Scenario 2: Both granted. + { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = PACKAGE_NAME; + packageInfo.requestedPermissions = + new String[] {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}; + packageInfo.requestedPermissionsFlags = + new int[] {
Original Bug Report
Potential privacy bypass in Android TWA location delegation flattens precision controls
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Chrome’s Trusted Web Activity (TWA) permission delegation flattens Android 12+ location precision controls, incorrectly promoting approximate grants to precise access. This potentially allows web origins to bypass permission prompts and access high-accuracy GPS data in regular tabs despite the user selecting approximate-only access for the corresponding TWA app.
Affected files:
chrome/browser/webapps/installable/installed_webapp_bridge.ccchrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.javachrome/browser/webapps/installable/installed_webapp_provider.cc
Estimated timestamp from git blame: 2025-09-09
Summary
A logic flaw in the Trusted Web Activity (TWA) permission delegation system on Android causes ‘Approximate’ location permissions to be incorrectly promoted to ‘Precise’ location permissions. When a user grants only approximate location access to a TWA wrapper app, the delegation bridge in Chrome treats this as a full grant for both approximate and precise location. This allows the web origin to obtain high-accuracy GPS data via Chrome’s internal geolocation service in regular tabs, bypassing the user’s explicit choice.
Root Cause Analysis
The issue exists in how location permissions are processed as they flow from the Android OS through the Java layer and into the C++ bridge:
-
Java Layer Flattening: In
InstalledWebappPermissionManager.java, the methodhasAndroidLocationPermissionchecks for eitherACCESS_COARSE_LOCATIONorACCESS_FINE_LOCATION. If either is granted, it returnstrue(chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java:244). This boolean state is converted to a singleContentSetting.ALLOWvalue, losing the distinction between coarse and fine access. -
C++ Bridge Inflation: In
installed_webapp_bridge.cc, theGetInstalledWebappPermissionsmethod receives this setting. ForGEOLOCATION_WITH_OPTIONS, it unconditionally sets both theapproximateandprecisefields of theGeolocationSettingtokAllowedif the incoming setting isALLOW(chrome/browser/webapps/installable/installed_webapp_bridge.cc:94-98). -
Provider Leakage: The
InstalledWebappProvideris registered with high precedence inHostContentSettingsMap. The delegation logic is active ifisRunningTwa()is true, which currently relies onApplicationStatus.getLastTrackedFocusedActivity()(chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java:57). In multi-window or split-screen scenarios, if a TWA was the last focused activity, a regular Chrome tab on the same origin can inherit these inflated permissions.
Since the permission check passes for ‘precise’ access, Chrome proceeds to use its own system-level privileged location access to fulfill the request, providing high-accuracy coordinates to the origin.
Potential Steps to Reproduce
- On an Android 12+ device, install a TWA wrapper app for origin
https://example.comthat requests location permissions. - Launch the TWA. When the OS location prompt appears, select ‘Approximate’ (ensure ‘Precise’ is toggled off) and ‘While using the app’.
- Ensure Chrome itself has system-level ‘Precise’ location access.
- Open
https://example.comin a regular Chrome tab. - Put the TWA and the Chrome tab in split-screen mode. Focus the TWA pane so it becomes the last focused activity.
- In the Chrome tab, execute JavaScript to request high-accuracy location:
navigator.geolocation.getCurrentPosition( (pos) => console.log(pos), (err) => console.error(err), {enableHighAccuracy: true} ); - Observe that the Chrome tab potentially receives high-accuracy coordinates without a permission prompt, and
navigator.permissions.query({name:'geolocation'})reports that precise access is allowed.
Suggested Fix
- Modify
InstalledWebappPermissionManager.javato distinguish between coarse and fine location grants (e.g., by returning an integer or a pair of booleans instead of a single boolean). - Update
InstalledWebappBridge::GetInstalledWebappPermissionsin C++ to correctly populate theapproximateandprecisefields of theGeolocationSettingstruct based on the actual Android permission state. - Tighten the
isRunningTwa()check or theInstalledWebappProviderlogic to ensure that delegated permissions do not inappropriately leak into standard Chrome tabs.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.