Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Permissions
DescriptionIncorrect authorization in Permissions
ComponentPermissions
Bug ClassLogic Error
Tracker517432155
Fix commitd6b18fd5db5b (chromium/src) +62/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
extensions/browser/guest_view/web_view/web_view_guest.cc
modified

Files Changed

  • chrome/browser/apps/guest_view/web_view_browsertest.cc
  • extensions/browser/guest_view/web_view/README.md
  • extensions/browser/guest_view/web_view/web_view_guest.cc
From d6b18fd5db5b042f56b73b13a1b3eaf170e68d06 Mon Sep 17 00:00:00 2001
From: Giovanni Pezzino <giovax@google.com>
Date: Thu, 06 Aug 2026 03:39:54 -0700
Subject: [PATCH] [webview] Route guest geolocation via embedder for all webviews

WebViewGuest::OverridePermissionResult() only returned ASK for
geolocation when the guest was owned by a Controlled Frame embedder. For
other <webview> embedders the permission lookup fell through to the
profile-scoped HostContentSettingsMap keyed on the guest's own origin,
so a prior grant to that origin in a regular tab (or an enterprise
policy) was applied without ever reaching DecidePermission() and the
embedder's permissionrequest handler.

Return ASK for geolocation from every WebViewGuest so that
GeolocationPermissionContextExtensions::DecidePermission() and
WebViewPermissionHelper::RequestGeolocationPermission() are always
consulted. Controlled Frame behaviour is unchanged; the remaining
Controlled-Frame-only overrides stay gated. Update the webview README
accordingly and add browser tests that seed a profile-level grant for
the guest origin and assert the embedder is still consulted.

TAG=agy

Bug: 517432155
Change-Id: I60101e39107f8650ce700529f69b1c267b5b8a89
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8193560
Commit-Queue: Giovanni Pezzino <giovax@google.com>
Auto-Submit: Giovanni Pezzino <giovax@google.com>
Reviewed-by: Simon Hangl <simonha@google.com>
Cr-Commit-Position: refs/heads/main@{#1674848}
---

diff --git a/chrome/browser/apps/guest_view/web_view_browsertest.cc b/chrome/browser/apps/guest_view/web_view_browsertest.cc
index d4aa613..bd79d6d 100644
--- a/chrome/browser/apps/guest_view/web_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_browsertest.cc
@@ -46,6 +46,7 @@
 #include "chrome/browser/autocomplete/aim_eligibility_service_factory.h"
 #include "chrome/browser/bluetooth/web_bluetooth_test_utils.h"
 #include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/devtools/devtools_window_testing.h"
 #include "chrome/browser/glic/host/glic_ui.h"
 #include "chrome/browser/glic/test_support/glic_browser_test.h"
@@ -86,6 +87,9 @@
 #include "chrome/test/base/chrome_test_utils.h"
 #include "chrome/test/base/tracing.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/content_settings/core/common/content_settings_types.h"
 #include "components/contextual_tasks/public/features.h"
 #include "components/download/public/common/download_task_runner.h"
 #include "components/find_in_page/find_tab_helper.h"
@@ -780,6 +784,37 @@
     ASSERT_TRUE(done_listener.WaitUntilSatisfied());
   }
 
+  void TestHelperWithProfileGrant(const std::string& test_name,
+                                  const std::string& app_location,
+                                  ContentSettingsType permission_type,
+                                  ContentSetting setting) {
+    ASSERT_TRUE(InitializeEmbeddedTestServer());
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::RedirectResponseHandler, kRedirectResponsePath,
+        embedded_test_server()->GetURL(kRedirectResponseFullPath)));
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::EmptyResponseHandler, kEmptyResponsePath));
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::UserAgentResponseHandler,
+        kUserAgentRedirectResponsePath,
+        embedded_test_server()->GetURL(kRedirectResponseFullPath)));
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::CacheControlResponseHandler, kCacheResponsePath));
+
+    EmbeddedTestServerAcceptConnections();
+
+    GURL guest_origin = embedded_test_server()->GetURL("localhost", "/");
+    HostContentSettingsMapFactory::GetForProfile(profile())
+        ->SetContentSettingDefaultScope(guest_origin, guest_origin,
+                                        permission_type, setting);
+
+    TestHelper(test_name, app_location, NO_TEST_SERVER);
+  }
+
   // Runs media_access/allow tests.
   void MediaAccessAPIAllowTestHelper(const std::string& test_name);
 
@@ -3565,6 +3600,20 @@
              NEEDS_TEST_SERVER);
 }
 
+IN_PROC_BROWSER_TEST_P(WebViewTest,
+                       GeolocationAPIEmbedderHasNoAccessWithProfileGrant) {
+  TestHelperWithProfileGrant(
+      "testDenyDenies", "web_view/geolocation/embedder_has_no_permission",
+      ContentSettingsType::GEOLOCATION, CONTENT_SETTING_ALLOW);
+}
+
+IN_PROC_BROWSER_TEST_P(WebViewTest,
+                       GeolocationAPIEmbedderHasAccessDenyWithProfileGrant) {
+  TestHelperWithProfileGrant(
+      "testDeny", "web_view/geolocation/embedder_has_permission",
+      ContentSettingsType::GEOLOCATION, CONTENT_SETTING_ALLOW);
+}
+
 // In following GeolocationAPIEmbedderHasAccess* tests, embedder (i.e. the
 // platform app) has geolocation permission
 //
diff --git a/extensions/browser/guest_view/web_view/README.md b/extensions/browser/guest_view/web_view/README.md
index e6d8e81..65ba8a2 100644
--- a/extensions/browser/guest_view/web_view/README.md
+++ b/extensions/browser/guest_view/web_view/README.md
@@ -65,7 +65,7 @@
         1.  Pointerlock is intercepted in [WebContentsImpl::RequestToLockPointer()](https://source.chromium.org/chromium/chromium/src/+/main:content/browser/web_contents/web_contents_impl.cc;l=4503;drc=cc5be7150eef183a1b9a6716d42a396ab7c59733;bpv=0;bpt=1).
         1.  Media is intercepted in [WebContentsImpl::RequestMediaAccessPermission()](https://source.chromium.org/chromium/chromium/src/+/main:content/browser/web_contents/web_contents_impl.cc;l=5165;drc=cc5be7150eef183a1b9a6716d42a396ab7c59733).
     1.  Geolocation - [GeolocationPermissionContextExtensions](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/geolocation/geolocation_permission_context_extensions.cc;l=51;drc=1149fe5f7bedbe8187bed8d6287a1ef19eac9b5a) which inherits [GeolocationPermissionContextDelegate](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/geolocation/geolocation_permission_context_delegate.h;l=20?q=GeolocationPermissionContextDelegate&sq=&ss=chromium%2Fchromium%2Fsrc). Intercepted in [GeolocationPermissionContextExtensions::DecidePermission()](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/geolocation/geolocation_permission_context_extensions.cc;l=71-73;drc=cc5be7150eef183a1b9a6716d42a396ab7c59733;bpv=0;bpt=1).
-        1. Geolocation also has an override where it calls [GuestViewBase::OverridePermissionResult()](https://source.chromium.org/chromium/chromium/src/+/main:components/permissions/permission_context_base.cc;l=326;drc=2046c842c9a8e7abe63a74f26e05896c15daa258) to ensure that for whatever origin, the permission look up result will always be ASK which leads into GeolocationPermissionContext::DecidePermission(). This is Controlled Frame-specific.
+        1. Geolocation also has an override where it calls [GuestViewBase::OverridePermissionResult()](https://source.chromium.org/chromium/chromium/src/+/main:components/permissions/permission_context_base.cc;l=326;drc=2046c842c9a8e7abe63a74f26e05896c15daa258) to ensure that for whatever origin, the permission look up result will always be ASK which leads into GeolocationPermissionContext::DecidePermission(), ensuring the embedder is always consulted.
     1.  HID checks whether a [RenderFrameHost](https://source.chromium.org/chromium/chromium/src/+/main:content/public/browser/render_frame_host.h;l=138?q=RenderFrameHost%20file:.h$&ss=chromium%2Fchromium%2Fsrc) is in a WebView in [ChromeHidDelegate](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/hid/chrome_hid_delegate.cc;l=192;drc=1149fe5f7bedbe8187bed8d6287a1ef19eac9b5a)
     1.  New Window is called from [WebViewGuest::CreateNewGuestWebViewWindow](https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/guest_view/web_view/web_view_guest.cc;l=685;drc=1149fe5f7bedbe8187bed8d6287a1ef19eac9b5a). Note this "newwindow" event is not from a permission request, but this is where we generate a permissionrequest event to capture it and send it to the embedder.
     1.  File System is called from [ChromeContentBrowserClient::AllowWorkerFileSystem](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/chrome_content_browser_client.cc;l=3114;drc=1149fe5f7bedbe8187bed8d6287a1ef19eac9b5a) and [ContentSettingsManagerDelegate::AllowStorageAccess](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/content_settings/content_settings_manager_delegate.cc;l=82;drc=1149fe5f7bedbe8187bed8d6287a1ef19eac9b5a)
diff --git a/extensions/browser/guest_view/web_view/web_view_guest.cc b/extensions/browser/guest_view/web_view/web_view_guest.cc
index 245fa84a..11abf755 100644
--- a/extensions/browser/guest_view/web_view/web_view_guest.cc
+++ b/extensions/browser/guest_view/web_view/web_view_guest.cc
@@ -1649,14 +1649,22 @@
     return result;
   }
 
+  blink::PermissionType permission_type;
+  if (!permissions::PermissionUtil::GetPermissionType(type, &permission_type)) {
+    return std::nullopt;
+  }
+
+  if (permission_type == blink::PermissionType::GEOLOCATION) {
+    return content::PermissionResult(
+        content::PermissionStatus::ASK,
+        content::PermissionStatusSource::UNSPECIFIED);
+  }
+
   if (IsOwnedByControlledFrameEmbedder()) {
     // Permission of content within a Controlled Frame is isolated.
     // Therefore, Controlled Frame decides what the immediate permission result
     // is.
-    const blink::PermissionType permission_type =
-        permissions::PermissionUtil::ContentSettingsTypeToPermissionType(type);
-    if (permission_type == blink::PermissionType::GEOLOCATION ||
-        permission_type == blink::PermissionType::AUDIO_CAPTURE ||
+    if (permission_type == blink::PermissionType::AUDIO_CAPTURE ||
         permission_type == blink::PermissionType::VIDEO_CAPTURE ||
         permission_type == blink::PermissionType::CLIPBOARD_READ_WRITE ||
         permission_type == blink::PermissionType::CLIPBOARD_SANITIZED_WRITE) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/apps/guest_view/web_view_browsertest.cc b/chrome/browser/apps/guest_view/web_view_browsertest.cc
index d4aa613..bd79d6d 100644
--- a/chrome/browser/apps/guest_view/web_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_browsertest.cc
@@ -46,6 +46,7 @@
 #include "chrome/browser/autocomplete/aim_eligibility_service_factory.h"
 #include "chrome/browser/bluetooth/web_bluetooth_test_utils.h"
 #include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/devtools/devtools_window_testing.h"
 #include "chrome/browser/glic/host/glic_ui.h"
 #include "chrome/browser/glic/test_support/glic_browser_test.h"
@@ -86,6 +87,9 @@
 #include "chrome/test/base/chrome_test_utils.h"
 #include "chrome/test/base/tracing.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "components/content_settings/core/common/content_settings.h"
+#include "components/content_settings/core/common/content_settings_types.h"
 #include "components/contextual_tasks/public/features.h"
 #include "components/download/public/common/download_task_runner.h"
 #include "components/find_in_page/find_tab_helper.h"
@@ -780,6 +784,37 @@
     ASSERT_TRUE(done_listener.WaitUntilSatisfied());
   }
 
+  void TestHelperWithProfileGrant(const std::string& test_name,
+                                  const std::string& app_location,
+                                  ContentSettingsType permission_type,
+                                  ContentSetting setting) {
+    ASSERT_TRUE(InitializeEmbeddedTestServer());
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::RedirectResponseHandler, kRedirectResponsePath,
+        embedded_test_server()->GetURL(kRedirectResponseFullPath)));
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::EmptyResponseHandler, kEmptyResponsePath));
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::UserAgentResponseHandler,
+        kUserAgentRedirectResponsePath,
+        embedded_test_server()->GetURL(kRedirectResponseFullPath)));
+
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &WebViewTestBase::CacheControlResponseHandler, kCacheResponsePath));
+
+    EmbeddedTestServerAcceptConnections();
+
+    GURL guest_origin = embedded_test_server()->GetURL("localhost", "/");
+    HostContentSettingsMapFactory::GetForProfile(profile())
+        ->SetContentSettingDefaultScope(guest_origin, guest_origin,
+                                        permission_type, setting);
+
+    TestHelper(test_name, app_location, NO_TEST_SERVER);
+  }
+
   // Runs media_access/allow tests.
   void MediaAccessAPIAllowTestHelper(const std::string& test_name);
 
@@ -3565,6 +3600,20 @@
              NEEDS_TEST_SERVER);
 }
 
+IN_PROC_BROWSER_TEST_P(WebViewTest,
+                       GeolocationAPIEmbedderHasNoAccessWithProfileGrant) {
+  TestHelperWithProfileGrant(
+      "testDenyDenies", "web_view/geolocation/embedder_has_no_permission",
+      ContentSettingsType::GEOLOCATION, CONTENT_SETTING_ALLOW);
+}
+
+IN_PROC_BROWSER_TEST_P(WebViewTest,
+                       GeolocationAPIEmbedderHasAccessDenyWithProfileGrant) {
+  TestHelperWithProfileGrant(
+      "testDeny", "web_view/geolocation/embedder_has_permission",
+      ContentSettingsType::GEOLOCATION, CONTENT_SETTING_ALLOW);
+}
+
 // In following GeolocationAPIEmbedderHasAccess* tests, embedder (i.e. the
 // platform app) has geolocation permission
 //
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Geolocation manifest permission bypass in standard <webview>

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential logic flaw in permission routing allows a non-ControlledFrame <webview> to bypass the host App’s manifest permission validation and obtain precise geolocation. When a guest page requests geolocation, PermissionContextBase checks the profile-scoped HostContentSettingsMap and can immediately grant permission if the origin has a profile-wide grant, short-circuiting before manifest verification or the webview permissionrequest event.

Affected files:

  • components/permissions/permission_context_base.cc
  • extensions/browser/guest_view/web_view/web_view_guest.cc
  • chrome/browser/guest_view/web_view/chrome_web_view_permission_helper_delegate.cc
  • chrome/browser/geolocation/geolocation_permission_context_extensions.cc

Estimated timestamp from git blame: 2024-08-27

Root Cause Analysis

In PermissionContextBase::RequestPermission (components/permissions/permission_context_base.cc), the profile-scoped permission status is retrieved first via GetPermissionStatus. If this status is GRANTED, execution short-circuits and immediately grants the permission, bypassing the DecidePermission path entirely:

content::PermissionResult result = GetPermissionStatus(*request_data, rfh);
bool status_ignorable = PermissionUtil::CanPermissionRequestIgnoreStatus(...);
if (!status_ignorable && (result.status == PermissionStatus::GRANTED || ... )) {
  ...
  NotifyPermissionSet(*request_data, std::move(callback), persist, &result, ...);
  return; // Short-circuit, DecidePermission is never reached
}

Normally, for GuestViews, OverridePermissionResult forces ASK to prevent this bypass and route the decision to the embedder, but it only does so if the embedder is owned by a ControlledFrame. For standard Chrome Apps or WebUI <webview>s, WebViewGuest::OverridePermissionResult returns std::nullopt:

std::optional<content::PermissionResult> WebViewGuest::OverridePermissionResult(
    ContentSettingsType type) const {
  auto result = web_view_permission_helper_->OverridePermissionResult(type);
  if (result) { return result; }
  if (IsOwnedByControlledFrameEmbedder()) {
    ...
    if (permission_type == blink::PermissionType::GEOLOCATION) {
      return content::PermissionResult(content::PermissionStatus::ASK, ...);
    }
  }
  return std::nullopt; // Non-ControlledFrame falls through
}

Furthermore, because IsPermissionRequestable(GEOLOCATION) returns true unconditionally, the deny gate does not trigger, and a profile-scoped lookup is performed against the HostContentSettingsMap of the profile. If the guest origin already has a profile-wide grant (e.g., from a user grant in a regular tab), GetPermissionStatus returns GRANTED and short-circuits. As a result, execution never reaches DecidePermission where the embedder’s routing is implemented. This potentially allows the guest to obtain geolocation without firing the embedder’s <webview> permissionrequest event and without verifying if the embedder possesses the geolocation manifest permission.

Potential Exploitation Scenario

An attacker could potentially perform the following steps to exploit this behavior:

  1. Package a platform app/Chrome App whose manifest includes the webview permission but explicitly lacks the geolocation manifest permission.
  2. Lure a victim user who already has a profile-wide geolocation grant for a specific trusted origin (e.g., https://example.com).
  3. Inside the app, instantiate a <webview> and navigate it to https://example.com.
  4. Execute a script inside the webview using webview.executeScript or receive a postMessage from the guest page that invokes navigator.geolocation.getCurrentPosition.
  5. The coordinates are successfully retrieved by the guest and exfiltrated to the app, bypassing the app’s lack of manifest permissions.

Note: These steps represent potential exploitation paths identified via static code analysis; our tooling does not currently have the capability to run code to produce a live proof-of-concept.

Impact

This is a confused-deputy permission escalation issue (CWE-863). An installed app lacking geolocation (or potentially audioCapture/videoCapture) manifest permissions can obtain sensitive user location or media data by loading an origin in its <webview> that already has a profile-scoped grant.

Suggested Fix

Modify WebViewGuest::OverridePermissionResult to return PermissionStatus::ASK for standard <webview>s as well for delegation-sensitive permission types like GEOLOCATION, AUDIO_CAPTURE, and VIDEO_CAPTURE. This ensures that they always route through the DecidePermission path where manifest checks and delegation logic are enforced.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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