Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in WebAppInstalls
DescriptionIncorrect authorization in WebAppInstalls
ComponentWebAppInstalls
Bug ClassLogic Error
Tracker513119757
Fix commitab9ee6d092bd (chromium/src) +133/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

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

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappBridge.java
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManager.java
  • chrome/android/junit/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappPermissionManagerTest.java
From ab9ee6d092bd527ba910c995376f3fc1f64063a7 Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
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 <dibyapal@chromium.org>
Reviewed-by: Peter Conn <peconn@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
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[] {
Loading diff…

Original Bug Report

reported by vm...@google.com

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