Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in GuestView
DescriptionInsufficient policy enforcement in GuestView
ComponentGuestView
Bug ClassLogic Error
Tracker517427352
Fix commit43f291054ac7 (chromium/src) +63/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
ControlledFramePermissionStatusLeakTest
chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
modified
ControlledFramePermissionRequestPEPCTest
chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
modified
if
extensions/browser/guest_view/web_view/web_view_guest.cc
modified

Files Changed

  • chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
  • extensions/browser/guest_view/web_view/web_view_guest.cc
From 43f291054ac7dadfec25f9a16bb0bb1d61838793 Mon Sep 17 00:00:00 2001
From: Giovanni Pezzino <giovax@google.com>
Date: Mon, 22 Jun 2026 07:35:01 -0700
Subject: [PATCH] Controlled Frame: Isolate camera, microphone, and clipboard permissions

This CL fixes a vulnerability where Controlled Frame guests could leak
profile-scoped camera, microphone, and clipboard permission states.

This CL extends WebViewGuest::OverridePermissionResult to override
AUDIO_CAPTURE, VIDEO_CAPTURE, CLIPBOARD_READ_WRITE, and
CLIPBOARD_SANITIZED_WRITE to ASK for Controlled Frames. This prevents
the permission status query from falling back to the profile-wide
HostContentSettingsMap, ensuring proper isolation.

It also adds a browser test to verify that these permissions do not leak
and correctly return "prompt" when queried inside the Controlled Frame,
even if they are allowed or blocked in the profile.

Bug: 517427352
Test: ControlledFramePermissionStatusLeakTest.PermissionsStatusDoNotLeak

TAG=agy

Change-Id: I11e231aeaee68df6f45c94af5a500444cc1feca9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7977999
Reviewed-by: Andrew Rayskiy <greengrape@google.com>
Commit-Queue: Giovanni Pezzino <giovax@google.com>
Cr-Commit-Position: refs/heads/main@{#1650303}
---

diff --git a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
index a83f86f..14d1f0cb 100644
--- a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
@@ -13,6 +13,7 @@
 #include "base/test/values_test_util.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/controlled_frame/controlled_frame_permission_request_test_base.h"
 #include "chrome/browser/hid/chrome_hid_delegate.h"
 #include "chrome/browser/hid/hid_chooser_context_factory.h"
@@ -20,6 +21,8 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/hid/hid_chooser_controller.h"
 #include "chrome/common/pref_names.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/download/public/common/download_item.h"
 #include "components/permissions/mock_chooser_controller_view.h"
@@ -508,6 +511,61 @@
             content::EvalJs(controlled_frame, kTestScript).ExtractString());
 }
 
+class ControlledFramePermissionStatusLeakTest : public ControlledFrameTestBase {
+ protected:
+  void SetPermission(const GURL& url,
+                     ContentSettingsType type,
+                     ContentSetting setting) {
+    HostContentSettingsMapFactory::GetForProfile(profile())
+        ->SetContentSettingDefaultScope(url, url, type, setting);
+  }
+
+  std::string QueryPermission(content::RenderFrameHost* frame,
+                              const std::string& name) {
+    return content::EvalJs(frame, content::JsReplace(R"(
+      navigator.permissions.query({name: $1}).then(r => r.state);
+    )",
+                                                     name))
+        .ExtractString();
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(ControlledFramePermissionStatusLeakTest,
+                       PermissionsStatusDoNotLeak) {
+  GURL guest_url =
+      embedded_https_test_server().GetURL("guest.com", "/empty.html");
+  url::Origin guest_origin = url::Origin::Create(guest_url);
+
+  // Set profile-wide permissions for the guest origin.
+  SetPermission(guest_url, ContentSettingsType::MEDIASTREAM_CAMERA,
+                CONTENT_SETTING_ALLOW);
+  SetPermission(guest_url, ContentSettingsType::MEDIASTREAM_MIC,
+                CONTENT_SETTING_BLOCK);
+  SetPermission(guest_url, ContentSettingsType::GEOLOCATION,
+                CONTENT_SETTING_ALLOW);
+  SetPermission(guest_url, ContentSettingsType::CLIPBOARD_READ_WRITE,
+                CONTENT_SETTING_ALLOW);
+
+  // Install and open IWA, then create ControlledFrame pointing to the guest
+  // origin.
+  auto [app_frame, controlled_frame] =
+      InstallAndOpenIwaThenCreateControlledFrame(
+          /*controlled_frame_host_name=*/"guest.com", "/empty.html");
+  ASSERT_TRUE(app_frame);
+  ASSERT_TRUE(controlled_frame);
+  ASSERT_EQ(controlled_frame->GetLastCommittedOrigin(), guest_origin);
+
+  // Geolocation (control case, already overridden to ASK, should return
+  // "prompt")
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "geolocation"));
+
+  // Camera, Microphone and Clipboard should also be isolated and return
+  // "prompt". Before the fix, these will leak.
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "camera"));
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "microphone"));
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "clipboard-read"));
+}
+
 class ControlledFramePermissionRequestPEPCTest
     : public ControlledFramePermissionRequestTest {
  public:
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 c1f8ae8..82ec28c 100644
--- a/extensions/browser/guest_view/web_view/web_view_guest.cc
+++ b/extensions/browser/guest_view/web_view/web_view_guest.cc
@@ -1654,7 +1654,11 @@
     // is.
     const blink::PermissionType permission_type =
         permissions::PermissionUtil::ContentSettingsTypeToPermissionType(type);
-    if (permission_type == blink::PermissionType::GEOLOCATION) {
+    if (permission_type == blink::PermissionType::GEOLOCATION ||
+        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) {
       return content::PermissionResult(
           content::PermissionStatus::ASK,
           content::PermissionStatusSource::UNSPECIFIED);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
index a83f86f..14d1f0cb 100644
--- a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
@@ -13,6 +13,7 @@
 #include "base/test/values_test_util.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chrome_content_browser_client.h"
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/controlled_frame/controlled_frame_permission_request_test_base.h"
 #include "chrome/browser/hid/chrome_hid_delegate.h"
 #include "chrome/browser/hid/hid_chooser_context_factory.h"
@@ -20,6 +21,8 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/hid/hid_chooser_controller.h"
 #include "chrome/common/pref_names.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/download/public/common/download_item.h"
 #include "components/permissions/mock_chooser_controller_view.h"
@@ -508,6 +511,61 @@
             content::EvalJs(controlled_frame, kTestScript).ExtractString());
 }
 
+class ControlledFramePermissionStatusLeakTest : public ControlledFrameTestBase {
+ protected:
+  void SetPermission(const GURL& url,
+                     ContentSettingsType type,
+                     ContentSetting setting) {
+    HostContentSettingsMapFactory::GetForProfile(profile())
+        ->SetContentSettingDefaultScope(url, url, type, setting);
+  }
+
+  std::string QueryPermission(content::RenderFrameHost* frame,
+                              const std::string& name) {
+    return content::EvalJs(frame, content::JsReplace(R"(
+      navigator.permissions.query({name: $1}).then(r => r.state);
+    )",
+                                                     name))
+        .ExtractString();
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(ControlledFramePermissionStatusLeakTest,
+                       PermissionsStatusDoNotLeak) {
+  GURL guest_url =
+      embedded_https_test_server().GetURL("guest.com", "/empty.html");
+  url::Origin guest_origin = url::Origin::Create(guest_url);
+
+  // Set profile-wide permissions for the guest origin.
+  SetPermission(guest_url, ContentSettingsType::MEDIASTREAM_CAMERA,
+                CONTENT_SETTING_ALLOW);
+  SetPermission(guest_url, ContentSettingsType::MEDIASTREAM_MIC,
+                CONTENT_SETTING_BLOCK);
+  SetPermission(guest_url, ContentSettingsType::GEOLOCATION,
+                CONTENT_SETTING_ALLOW);
+  SetPermission(guest_url, ContentSettingsType::CLIPBOARD_READ_WRITE,
+                CONTENT_SETTING_ALLOW);
+
+  // Install and open IWA, then create ControlledFrame pointing to the guest
+  // origin.
+  auto [app_frame, controlled_frame] =
+      InstallAndOpenIwaThenCreateControlledFrame(
+          /*controlled_frame_host_name=*/"guest.com", "/empty.html");
+  ASSERT_TRUE(app_frame);
+  ASSERT_TRUE(controlled_frame);
+  ASSERT_EQ(controlled_frame->GetLastCommittedOrigin(), guest_origin);
+
+  // Geolocation (control case, already overridden to ASK, should return
+  // "prompt")
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "geolocation"));
+
+  // Camera, Microphone and Clipboard should also be isolated and return
+  // "prompt". Before the fix, these will leak.
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "camera"));
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "microphone"));
+  EXPECT_EQ("prompt", QueryPermission(controlled_frame, "clipboard-read"));
+}
+
 class ControlledFramePermissionRequestPEPCTest
     : public ControlledFramePermissionRequestTest {
  public:
Loading diff…

Original Bug Report

reported by vm...@google.com

Controlled Frame guest leaks profile-scoped camera/microphone permission state

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 vulnerability in Chromium’s Controlled Frame permission isolation allows an embedded guest to leak profile-scoped camera and microphone permission states. This is caused by WebViewGuest::OverridePermissionResult failing to override AUDIO_CAPTURE and VIDEO_CAPTURE permissions to ASK for Controlled Frames. Consequently, permission status queries fallback to the profile-wide content settings map, leaking isolated user permissions to an embedded guest.

Affected files:

  • extensions/browser/guest_view/web_view/web_view_guest.cc

Estimated timestamp from git blame: 2024-08-27

Summary

A potential vulnerability in Chromium’s Controlled Frame permission isolation allows an embedded guest inside a <controlledframe> to leak profile-scoped camera and microphone permission states and enterprise permission allowlists. This bypasses the StoragePartition/embedder boundaries intended for Controlled Frames, allowing a malicious Isolated Web App (IWA) to learn about the user’s regular profile permission history and enterprise policy configuration.

Root Cause Analysis

WebViewGuest::OverridePermissionResult (defined in extensions/browser/guest_view/web_view/web_view_guest.cc, lines 1644-1665) implements Controlled Frame’s permission isolation, but it currently only handles the GEOLOCATION permission type:

std::optional<content::PermissionResult> WebViewGuest::OverridePermissionResult(
    ContentSettingsType type) const {
  auto result = web_view_permission_helper_->OverridePermissionResult(type);
  if (result) { return result; }

  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) {
      return content::PermissionResult(
          content::PermissionStatus::ASK,
          content::PermissionStatusSource::UNSPECIFIED);
    }
    // Returns nullopt for unhandled cases.
  }
  return std::nullopt;
}

For AUDIO_CAPTURE (microphone) and VIDEO_CAPTURE (camera), this returns std::nullopt. Because the browser-layer delegate does not override these types, the permission query is allowed to proceed since IsPermissionRequestable returns true for these types. This causes the lookup to fall back to the profile-wide HostContentSettingsMap and enterprise policy lists (e.g. VideoCaptureAllowedUrls and AudioCaptureAllowedUrls), breaking the isolation boundary between the regular profile and the Controlled Frame’s StoragePartition.

Potential Exploit Path

Note: The following are potential steps, as our testing tools do not run active code.

  1. In a regular Chrome tab, the user navigates to a target origin (e.g., https://target.example) and grants camera/microphone permission.
  2. The user installs and runs an Isolated Web App (IWA) controlled by an attacker that has the controlled-frame permission in its manifest.
  3. The malicious IWA programmatically creates a <controlledframe> element pointing to https://target.example.
  4. Once loaded, the malicious IWA injects a script into the guest using controlledFrame.executeScript to query the permission status:
    navigator.permissions.query({name: 'camera'}).then(r => {
      return r.state;
    });
    
  5. Due to the missing override, the query receives 'granted' or 'denied' matching the user’s regular profile state, leaking this private cross-partition configuration back to the guest and the host IWA.

Suggested Fix

To correct the permission isolation, add AUDIO_CAPTURE and VIDEO_CAPTURE to the list of overridden permission types in WebViewGuest::OverridePermissionResult inside extensions/browser/guest_view/web_view/web_view_guest.cc:

    const blink::PermissionType permission_type =
        permissions::PermissionUtil::ContentSettingsTypeToPermissionType(type);
    if (permission_type == blink::PermissionType::GEOLOCATION ||
        permission_type == blink::PermissionType::AUDIO_CAPTURE ||
        permission_type == blink::PermissionType::VIDEO_CAPTURE) {
      return content::PermissionResult(
          content::PermissionStatus::ASK,
          content::PermissionStatusSource::UNSPECIFIED);
    }

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