Medium watch CVSS 4.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in WebAppInstalls
DescriptionIncorrect authorization in WebAppInstalls
ComponentWebAppInstalls
Sub-componentTWA
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513119757
Fix commitab9ee6d092bd (chromium/src) +133/-10
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-07-20 (public git)
Disclosed2026-08-25
Patch gap36 days — fix public in source before the advisory

Background

Trusted Web Activity (TWA)
an Android app that hosts web content for an associated origin; the web content’s permissions can be delegated from the wrapper app’s OS-level Android permissions.
`InstalledWebappPermissionManager` / `InstalledWebappBridge`
the Java side that reads the wrapper app’s Android permissions and hands Permission records over JNI to native (installed_webapp_bridge.cc) so Chrome can compute the web ContentSetting for the TWA origin.
`GeolocationSetting`
a native structure with two tiers, an approximate (coarse) permission option and a precise (fine) permission option, mirroring ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION.
`hasAndroidPermissions` (any-of) vs. `hasAndroidFineLocationPermission`
the former returns true if the app holds any of the listed permissions; the latter (new) checks specifically for ACCESS_FINE_LOCATION.

Root Cause Analysis

When resolving geolocation for a TWA, InstalledWebappBridge::GetInstalledWebappPermissions (in installed_webapp_bridge.cc) constructed a GeolocationSetting by mapping a single incoming setting value into both the approximate and precise fields via ToPermissionOption(setting). That single setting was derived from an any-of ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION grant, so a wrapper app that held only ACCESS_COARSE_LOCATION still produced precise = Allowed, granting the web content precise location it was never authorized for.

The fix threads a separate preciseSetting through the InstalledWebappBridge.Permission POD: InstalledWebappPermissionManager.getPermissions now, for GEOLOCATION_WITH_OPTIONS with an ALLOW approximate setting, calls the new hasAndroidFineLocationPermission(packageName) and sets preciseSetting to ALLOW only when fine is actually granted, otherwise BLOCK. Native then reads that value via the new Java_InstalledWebappBridge_getPreciseSettingFromPermission and uses it for the precise member of GeolocationSetting, while the approximate member keeps the existing any-of semantics. This works because the precise tier is now derived from the fine-specific OS grant rather than being a duplicate of the coarse-or-fine grant.

Key insight
The precise geolocation tier was copied from the coarse-or-fine any-of grant instead of being derived from the actual ACCESS_FINE_LOCATION grant; the fix computes preciseSetting from a dedicated fine-location check so coarse-only wrapper apps no longer report precise=Allowed.

Attack Path

  1. Wrapper app holds only coarse location A TWA’s Android wrapper app requests/holds ACCESS_COARSE_LOCATION (but not ACCESS_FINE_LOCATION) and the user allows location for the TWA origin (ContentSetting.ALLOW).
  2. Chrome reads the delegated permission getPermissions(GEOLOCATION_WITH_OPTIONS) reads the any-of grant and, pre-patch, packages a single setting into the Permission record.
  3. Precise tier is inflated Native GetInstalledWebappPermissions maps that single value into both GeolocationSetting fields, so the precise tier is reported as Allowed despite no fine grant.
  4. Web content obtains precise location The TWA’s web content requesting high-accuracy geolocation is served precise location it was not authorized for, an over-broad grant / incorrect authorization (privacy exposure).

Impact Assessment

A TWA whose wrapper app was granted only coarse location could have its embedded web content receive precise (fine) geolocation, defeating the OS-level coarse-only choice; this is an incorrect-authorization/privacy issue on Android in the browser’s permission-delegation logic. The precondition is a delegated GEOLOCATION_WITH_OPTIONS grant where the wrapper app holds coarse but not fine location and the origin’s content setting is ALLOW. The advisory classifies it as incorrect authorization in WebAppInstalls; no exploited-in-the-wild status is stated in the provided material.

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java
modified
if
chrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java
modified
if
chrome/browser/webapps/installable/installed_webapp_bridge.cc
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java
  • chrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java
  • chrome/browser/webapps/installable/installed_webapp_bridge.cc

Audit Directions

  • Any-of permission checks feeding tiered settings
    Audit places where a coarse “any of these permissions” query result is reused for a finer-grained capability; each tier should be derived from its own specific grant.
  • Values duplicated across structure fields
    Search for constructions like Setting{ToOption(x), ToOption(x)} where the same input populates two distinct fields (approximate/precise, read/write, etc.); this pattern often masks a missing distinct source.
  • Permission delegation to embedded web content
    Review other delegated permissions in TWA/installed-webapp bridges (camera, microphone, etc.) to confirm the web-exposed setting is not broader than the wrapper app’s actual OS grant.
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[] {
+                            PackageInfo.REQUESTED_PERMISSION_GRANTED,
+                            PackageInfo.REQUESTED_PERMISSION_GRANTED
+                        };
+                mShadowPackageManager.installPackage(packageInfo);
+
+                setStoredLocationPermission(ContentSetting.ALLOW);
+
+                InstalledWebappBridge.Permission[] permissions =
+                        InstalledWebappPermissionManager.getPermissions(mType);
+                assertEquals(1, permissions.length);
+                assertEquals(ContentSetting.ALLOW, permissions[0].setting); // approximate
+                assertEquals(ContentSetting.ALLOW, permissions[0].preciseSetting); // precise
+            }
+
+            // Scenario 3: Both requested, but only coarse granted.
+            {
+                PackageInfo packageInfo = new PackageInfo();
+                packageInfo.packageName = PACKAGE_NAME;
+                packageInfo.requestedPermissions =
+                        new String[] {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION};
+                packageInfo.requestedPermissionsFlags =
+                        new int[] {PackageInfo.REQUESTED_PERMISSION_GRANTED, 0};
+                mShadowPackageManager.installPackage(packageInfo);
+
+                setStoredLocationPermission(ContentSetting.ALLOW);
+
+                InstalledWebappBridge.Permission[] permissions =
+                        InstalledWebappPermissionManager.getPermissions(mType);
+                assertEquals(1, permissions.length);
+                assertEquals(ContentSetting.ALLOW, permissions[0].setting); // approximate
+                assertEquals(ContentSetting.BLOCK, permissions[0].preciseSetting); // precise
+            }
+        } finally {
+            // Clean up activity.
+            ApplicationStatus.onStateChangeForTesting(activity, ActivityState.DESTROYED);
+        }
+    }
 }
diff --git a/chrome/browser/webapps/installable/installed_webapp_bridge.cc b/chrome/browser/webapps/installable/installed_webapp_bridge.cc
index 0ae847a6..42d9990c 100644
--- a/chrome/browser/webapps/installable/installed_webapp_bridge.cc
+++ b/chrome/browser/webapps/installable/installed_webapp_bridge.cc
@@ -92,9 +92,12 @@
         Java_InstalledWebappBridge_getSettingFromPermission(env, j_permission));
     PermissionSetting permission_setting = setting;
     if (type == ContentSettingsType::GEOLOCATION_WITH_OPTIONS) {
-      permission_setting =
-          GeolocationSetting{content_settings::ToPermissionOption(setting),
-                             content_settings::ToPermissionOption(setting)};
+      ContentSetting precise_setting = IntToContentSetting(
+          Java_InstalledWebappBridge_getPreciseSettingFromPermission(
+              env, j_permission));
+      permission_setting = GeolocationSetting{
+          content_settings::ToPermissionOption(setting),
+          content_settings::ToPermissionOption(precise_setting)};
     }
     rules.emplace_back(origin, permission_setting);
   }
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java
  • chrome/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:

  1. Java Layer Flattening: In InstalledWebappPermissionManager.java, the method hasAndroidLocationPermission checks for either ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION. If either is granted, it returns true (chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java:244). This boolean state is converted to a single ContentSetting.ALLOW value, losing the distinction between coarse and fine access.

  2. C++ Bridge Inflation: In installed_webapp_bridge.cc, the GetInstalledWebappPermissions method receives this setting. For GEOLOCATION_WITH_OPTIONS, it unconditionally sets both the approximate and precise fields of the GeolocationSetting to kAllowed if the incoming setting is ALLOW (chrome/browser/webapps/installable/installed_webapp_bridge.cc:94-98).

  3. Provider Leakage: The InstalledWebappProvider is registered with high precedence in HostContentSettingsMap. The delegation logic is active if isRunningTwa() is true, which currently relies on ApplicationStatus.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

  1. On an Android 12+ device, install a TWA wrapper app for origin https://example.com that requests location permissions.
  2. Launch the TWA. When the OS location prompt appears, select ‘Approximate’ (ensure ‘Precise’ is toggled off) and ‘While using the app’.
  3. Ensure Chrome itself has system-level ‘Precise’ location access.
  4. Open https://example.com in a regular Chrome tab.
  5. Put the TWA and the Chrome tab in split-screen mode. Focus the TWA pane so it becomes the last focused activity.
  6. In the Chrome tab, execute JavaScript to request high-accuracy location:
    navigator.geolocation.getCurrentPosition(
        (pos) => console.log(pos), 
        (err) => console.error(err), 
        {enableHighAccuracy: true}
    );
    
  7. 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

  1. Modify InstalledWebappPermissionManager.java to distinguish between coarse and fine location grants (e.g., by returning an integer or a pair of booleans instead of a single boolean).
  2. Update InstalledWebappBridge::GetInstalledWebappPermissions in C++ to correctly populate the approximate and precise fields of the GeolocationSetting struct based on the actual Android permission state.
  3. Tighten the isRunningTwa() check or the InstalledWebappProvider logic 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.

View on issue tracker