Low chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Browser
DescriptionIncorrect authorization in Browser
ComponentBrowser
Sub-componentControlled Frame
Bug ClassAuthorization Error
Tracker553270559
Fix commit37038e1ec211 (chromium/src) +452/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
modified
if
chrome/browser/controlled_frame/controlled_frame_media_permission_cache.cc
modified
HostContentSettingsMap
chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h
modified
Profile
chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h
modified
ControlledFrameMediaPermissionCache
chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h
modified
GetInstance
chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.cc
modified
BrowserContext
chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h
modified
ControlledFrameMediaPermissionCache
chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h
modified
ControlledFrameMediaPermissionCacheFactory
chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h
modified
ControlledFrameMediaPermissionCacheTest
chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc
modified
TEST_F
chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc
modified

Files Changed

  • chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h
  • chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc
  • chrome/browser/controlled_frame/controlled_frame_media_permission_cache.cc
  • chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.cc
  • chrome/browser/profiles/BUILD.gn
  • chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h
  • chrome/browser/controlled_frame/controlled_frame_media_access_handler.h
  • chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
  • chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
  • chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
  • chrome/browser/controlled_frame/BUILD.gn
From 37038e1ec21110557ff8237e3a2ea6fcfa39adfc Mon Sep 17 00:00:00 2001
From: Simon Hangl <[email protected]>
Date: Wed, 02 Sep 2026 09:36:06 -0700
Subject: [PATCH] [Controlled Frame] Scope media permissions cache to BrowserContext

Previously, ControlledFrameMediaAccessHandler cached camera and
microphone approvals in a process-wide, in-memory map keyed only by
origin strings. This led to cross-profile hardware fingerprinting leaks
for Isolated Web Apps (IWAs) and allowed the cache to bypass Clear
Browsing Data (CBD) actions.

This CL introduces ControlledFrameMediaPermissionCache, a KeyedService
that ties the media permission cache to the BrowserContext. It also
observes HostContentSettingsMap, ensuring the cache is instantly cleared
when a user revokes media permissions or clears their browsing data.

Design Doc:
https://docs.google.com/document/d/1P7Wanip5xfqZ4sZwdgisVF9ddWdwvG7RBMweBzcDmLg/edit?usp=sharing

Bug: 553270559
Change-Id: Ie301718b0b033eaa752514078b2482776e108a34
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8301775
Reviewed-by: Alex Ilin <[email protected]>
Commit-Queue: Simon Hangl <[email protected]>
Reviewed-by: Kevin McNee <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1690831}
---
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
index 6b0bfb2..78ad38e 100644
--- a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.cc
@@ -5,6 +5,8 @@
 #include "chrome/browser/controlled_frame/controlled_frame_media_access_handler.h"
 
 #include "base/types/expected.h"
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h"
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h"
 #include "chrome/browser/media/webrtc/media_stream_device_permissions.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/pref_names.h"
@@ -90,14 +92,16 @@
 
   // Technically, Controlled Frame permission check needs to be done
   // asynchronously (via an event handled by the embedder). However, this method
-  // must return immediately. |requests_| is used as a caching mechanism. An
-  // embedder origin + requesting origin pair in |requests_| must have already
-  // passed the asynchronous checks at least once. Unfortunately, this means
-  // once a permission is granted, it cannot be revoked in the same session.
+  // must return immediately. `ControlledFrameMediaPermissionCache` is used as a
+  // caching mechanism. An embedder origin + requesting origin pair in the cache
+  // must have already passed the asynchronous checks at least once.
   // Note that the type check can be omitted here because WebView Permission
   // Request API does not differentiate audio and video requests, they are both
   // treated as "media".
-  if (!requests_[embedder_origin].contains(requesting_origin)) {
+  auto* cache =
+      ControlledFrameMediaPermissionCacheFactory::GetForBrowserContext(
+          render_frame_host->GetBrowserContext());
+  if (!cache || !cache->HasPermission(embedder_origin, requesting_origin)) {
     return false;
   }
 
@@ -128,7 +132,12 @@
       web_view->embedder_rfh()->GetLastCommittedOrigin();
   const url::Origin& requesting_origin = request.url_origin;
 
-  requests_[embedder_origin].insert(requesting_origin);
+  auto* cache =
+      ControlledFrameMediaPermissionCacheFactory::GetForBrowserContext(
+          web_contents->GetBrowserContext());
+  if (cache) {
+    cache->AddPermission(embedder_origin, requesting_origin);
+  }
 
   if (request.audio_type !=
           blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE &&
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h
index 08e7135e..53e2760f 100644
--- a/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h
+++ b/chrome/browser/controlled_frame/controlled_frame_media_access_handler.h
@@ -5,9 +5,6 @@
 #ifndef CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_ACCESS_HANDLER_H_
 #define CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_ACCESS_HANDLER_H_
 
-#include <map>
-#include <set>
-
 #include "chrome/browser/media/media_access_handler.h"
 
 namespace content {
@@ -60,7 +57,6 @@
                                     const url::Origin& requesting_origin,
                                     blink::mojom::MediaStreamType type);
 
-  std::map<url::Origin, std::set<url::Origin>> requests_;
 };
 
 }  // namespace controlled_frame
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_permission_cache.cc b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache.cc
new file mode 100644
index 0000000..f343bba
--- /dev/null
+++ b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache.cc
@@ -0,0 +1,57 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h"
+
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+
+namespace controlled_frame {
+
+ControlledFrameMediaPermissionCache::ControlledFrameMediaPermissionCache(
+    Profile* profile) {
+  auto* hcsm = HostContentSettingsMapFactory::GetForProfile(profile);
+  if (hcsm) {
+    hcsm_observation_.Observe(hcsm);
+  }
+}
+
+ControlledFrameMediaPermissionCache::~ControlledFrameMediaPermissionCache() =
+    default;
+
+void ControlledFrameMediaPermissionCache::Shutdown() {
+  hcsm_observation_.Reset();
+}
+
+void ControlledFrameMediaPermissionCache::OnContentSettingChanged(
+    const ContentSettingsPattern& primary_pattern,
+    const ContentSettingsPattern& secondary_pattern,
+    ContentSettingsTypeSet content_type_set) {
+  // We only cache Camera and Microphone permissions. Ignore all other content
+  // setting changes (like Cookies or Geolocation) to prevent spurious cache
+  // invalidations that would force the user to re-grant permissions.
+  if (content_type_set.Contains(ContentSettingsType::MEDIASTREAM_CAMERA) ||
+      content_type_set.Contains(ContentSettingsType::MEDIASTREAM_MIC)) {
+    requests_.clear();
+  }
+}
+
+void ControlledFrameMediaPermissionCache::AddPermission(
+    const url::Origin& embedder_origin,
+    const url::Origin& requesting_origin) {
+  requests_[embedder_origin].insert(requesting_origin);
+}
+
+bool ControlledFrameMediaPermissionCache::HasPermission(
+    const url::Origin& embedder_origin,
+    const url::Origin& requesting_origin) const {
+  auto it = requests_.find(embedder_origin);
+  if (it == requests_.end()) {
+    return false;
+  }
+  return it->second.contains(requesting_origin);
+}
+
+}  // namespace controlled_frame
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h
new file mode 100644
index 0000000..860bb8c
--- /dev/null
+++ b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h
@@ -0,0 +1,59 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_PERMISSION_CACHE_H_
+#define CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_PERMISSION_CACHE_H_
+
+#include <map>
+#include <set>
+
+#include "base/scoped_observation.h"
+#include "components/content_settings/core/browser/content_settings_observer.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "url/origin.h"
+
+class HostContentSettingsMap;
+
+class Profile;
+
+namespace controlled_frame {
+
+// Caches Controlled Frame media permission requests per profile.
+// The cache is cleared when the profile is destroyed or when camera/microphone
+// content settings are changed or cleared (e.g. by Clear Browsing Data).
+class ControlledFrameMediaPermissionCache : public KeyedService,
+                                            public content_settings::Observer {
+ public:
+  explicit ControlledFrameMediaPermissionCache(Profile* profile);
+  ~ControlledFrameMediaPermissionCache() override;
+
+  ControlledFrameMediaPermissionCache(
+      const ControlledFrameMediaPermissionCache&) = delete;
+  ControlledFrameMediaPermissionCache& operator=(
+      const ControlledFrameMediaPermissionCache&) = delete;
+
+  // KeyedService:
+  void Shutdown() override;
+
+  // content_settings::Observer:
+  void OnContentSettingChanged(
+      const ContentSettingsPattern& primary_pattern,
+      const ContentSettingsPattern& secondary_pattern,
+      ContentSettingsTypeSet content_type_set) override;
+
+  // Permission cache operations.
+  void AddPermission(const url::Origin& embedder_origin,
+                     const url::Origin& requesting_origin);
+  bool HasPermission(const url::Origin& embedder_origin,
+                     const url::Origin& requesting_origin) const;
+
+ private:
+  std::map<url::Origin, std::set<url::Origin>> requests_;
+  base::ScopedObservation<HostContentSettingsMap, content_settings::Observer>
+      hcsm_observation_{this};
+};
+
+}  // namespace controlled_frame
+
+#endif  // CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_PERMISSION_CACHE_H_
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.cc b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.cc
new file mode 100644
index 0000000..e3b4a740
--- /dev/null
+++ b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.cc
@@ -0,0 +1,52 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h"
+
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h"
+#include "chrome/browser/profiles/profile.h"
+
+namespace controlled_frame {
+
+// static
+ControlledFrameMediaPermissionCache*
+ControlledFrameMediaPermissionCacheFactory::GetForBrowserContext(
+    content::BrowserContext* context) {
+  return static_cast<ControlledFrameMediaPermissionCache*>(
+      GetInstance()->GetServiceForBrowserContext(context, true));
+}
+
+// static
+ControlledFrameMediaPermissionCacheFactory*
+ControlledFrameMediaPermissionCacheFactory::GetInstance() {
+  static base::NoDestructor<ControlledFrameMediaPermissionCacheFactory>
+      instance;
+  return instance.get();
+}
+
+ControlledFrameMediaPermissionCacheFactory::
+    ControlledFrameMediaPermissionCacheFactory()
+    : ProfileKeyedServiceFactory(
+          "ControlledFrameMediaPermissionCache",
+          ProfileSelections::Builder()
+              .WithRegular(ProfileSelection::kOwnInstance)
+              .WithGuest(ProfileSelection::kOwnInstance)
+              .WithSystem(ProfileSelection::kNone)
+              .WithAshInternals(ProfileSelection::kNone)
+              .Build()) {
+  DependsOn(HostContentSettingsMapFactory::GetInstance());
+}
+
+ControlledFrameMediaPermissionCacheFactory::
+    ~ControlledFrameMediaPermissionCacheFactory() = default;
+
+std::unique_ptr<KeyedService> ControlledFrameMediaPermissionCacheFactory::
+    BuildServiceInstanceForBrowserContext(
+        content::BrowserContext* context) const {
+  return std::make_unique<ControlledFrameMediaPermissionCache>(
+      Profile::FromBrowserContext(context));
+}
+
+}  // namespace controlled_frame
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h
new file mode 100644
index 0000000..2e42912
--- /dev/null
+++ b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h
@@ -0,0 +1,45 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_PERMISSION_CACHE_FACTORY_H_
+#define CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_PERMISSION_CACHE_FACTORY_H_
+
+#include "base/no_destructor.h"
+#include "chrome/browser/profiles/profile_keyed_service_factory.h"
+
+namespace content {
+class BrowserContext;
+}
+
+namespace controlled_frame {
+
+class ControlledFrameMediaPermissionCache;
+
+class ControlledFrameMediaPermissionCacheFactory
+    : public ProfileKeyedServiceFactory {
+ public:
+  static ControlledFrameMediaPermissionCache* GetForBrowserContext(
+      content::BrowserContext* context);
+
+  static ControlledFrameMediaPermissionCacheFactory* GetInstance();
+
+  ControlledFrameMediaPermissionCacheFactory(
+      const ControlledFrameMediaPermissionCacheFactory&) = delete;
+  ControlledFrameMediaPermissionCacheFactory& operator=(
+      const ControlledFrameMediaPermissionCacheFactory&) = delete;
+
+ private:
+  friend base::NoDestructor<ControlledFrameMediaPermissionCacheFactory>;
+
+  ControlledFrameMediaPermissionCacheFactory();
+  ~ControlledFrameMediaPermissionCacheFactory() override;
+
+  // BrowserContextKeyedServiceFactory:
+  std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
+      content::BrowserContext* context) const override;
+};
+
+}  // namespace controlled_frame
+
+#endif  // CHROME_BROWSER_CONTROLLED_FRAME_CONTROLLED_FRAME_MEDIA_PERMISSION_CACHE_FACTORY_H_
diff --git a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
index c76f4b7..191ecd9 100644
--- a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
+++ b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
@@ -557,6 +557,7 @@
 #if BUILDFLAG(ENABLE_EXTENSIONS)
 #include "apps/browser_context_keyed_service_factories.h"
 #include "chrome/browser/apps/platform_apps/browser_context_keyed_service_factories.h"
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h"
 #include "chrome/browser/sync_file_system/sync_file_system_service_factory.h"
 #include "chrome/browser/ui/web_applications/web_app_metrics_factory.h"
 #include "chrome/browser/web_applications/web_app_provider_factory.h"
@@ -932,6 +933,9 @@
   glic::ContextualCueingServiceFactory::GetInstance();
 #endif
   ContextualSearchServiceFactory::GetInstance();
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+  controlled_frame::ControlledFrameMediaPermissionCacheFactory::GetInstance();
+#endif
   CookieSettingsFactory::GetInstance();
 #if !BUILDFLAG(IS_ANDROID)
   metrics::CriticalUserJourneyServiceFactory::GetInstance();
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc
new file mode 100644
index 0000000..e2c82c4
--- /dev/null
+++ b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc
@@ -0,0 +1,107 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h"
+
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace controlled_frame {
+
+class ControlledFrameMediaPermissionCacheTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    profile_ = std::make_unique<TestingProfile>();
+    cache_ =
+        std::make_unique<ControlledFrameMediaPermissionCache>(profile_.get());
+  }
+
+  void TearDown() override { cache_->Shutdown(); }
+
+  TestingProfile* profile() { return profile_.get(); }
+  ControlledFrameMediaPermissionCache* cache() { return cache_.get(); }
+
+ private:
+  content::BrowserTaskEnvironment task_environment_;
+  std::unique_ptr<TestingProfile> profile_;
+  std::unique_ptr<ControlledFrameMediaPermissionCache> cache_;
+};
+
+TEST_F(ControlledFrameMediaPermissionCacheTest, AddAndCheckPermission) {
+  url::Origin embedder =
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc
new file mode 100644
index 0000000..e2c82c4
--- /dev/null
+++ b/chrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc
@@ -0,0 +1,107 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/controlled_frame/controlled_frame_media_permission_cache.h"
+
+#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/content_settings/core/browser/host_content_settings_map.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace controlled_frame {
+
+class ControlledFrameMediaPermissionCacheTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    profile_ = std::make_unique<TestingProfile>();
+    cache_ =
+        std::make_unique<ControlledFrameMediaPermissionCache>(profile_.get());
+  }
+
+  void TearDown() override { cache_->Shutdown(); }
+
+  TestingProfile* profile() { return profile_.get(); }
+  ControlledFrameMediaPermissionCache* cache() { return cache_.get(); }
+
+ private:
+  content::BrowserTaskEnvironment task_environment_;
+  std::unique_ptr<TestingProfile> profile_;
+  std::unique_ptr<ControlledFrameMediaPermissionCache> cache_;
+};
+
+TEST_F(ControlledFrameMediaPermissionCacheTest, AddAndCheckPermission) {
+  url::Origin embedder =
+      url::Origin::Create(GURL("https://embedder.example.com"));
+  url::Origin requesting =
+      url::Origin::Create(GURL("https://requesting.example.com"));
+  url::Origin other = url::Origin::Create(GURL("https://other.example.com"));
+
+  EXPECT_FALSE(cache()->HasPermission(embedder, requesting));
+
+  cache()->AddPermission(embedder, requesting);
+
+  EXPECT_TRUE(cache()->HasPermission(embedder, requesting));
+  EXPECT_FALSE(cache()->HasPermission(embedder, other));
+  EXPECT_FALSE(cache()->HasPermission(other, requesting));
+}
+
+TEST_F(ControlledFrameMediaPermissionCacheTest, ClearsOnCameraSettingChanged) {
+  url::Origin embedder =
+      url::Origin::Create(GURL("https://embedder.example.com"));
+  url::Origin requesting =
+      url::Origin::Create(GURL("https://requesting.example.com"));
+
+  cache()->AddPermission(embedder, requesting);
+  EXPECT_TRUE(cache()->HasPermission(embedder, requesting));
+
+  auto* hcsm = HostContentSettingsMapFactory::GetForProfile(profile());
+  hcsm->SetContentSettingDefaultScope(embedder.GetURL(), embedder.GetURL(),
+                                      ContentSettingsType::MEDIASTREAM_CAMERA,
+                                      CONTENT_SETTING_BLOCK);
+
+  // The cache should be cleared.
+  EXPECT_FALSE(cache()->HasPermission(embedder, requesting));
+}
+
+TEST_F(ControlledFrameMediaPermissionCacheTest, ClearsOnMicSettingChanged) {
+  url::Origin embedder =
+      url::Origin::Create(GURL("https://embedder.example.com"));
+  url::Origin requesting =
+      url::Origin::Create(GURL("https://requesting.example.com"));
+
+  cache()->AddPermission(embedder, requesting);
+  EXPECT_TRUE(cache()->HasPermission(embedder, requesting));
+
+  auto* hcsm = HostContentSettingsMapFactory::GetForProfile(profile());
+  hcsm->SetContentSettingDefaultScope(embedder.GetURL(), embedder.GetURL(),
+                                      ContentSettingsType::MEDIASTREAM_MIC,
+                                      CONTENT_SETTING_BLOCK);
+
+  // The cache should be cleared.
+  EXPECT_FALSE(cache()->HasPermission(embedder, requesting));
+}
+
+TEST_F(ControlledFrameMediaPermissionCacheTest,
+       DoesNotClearOnUnrelatedSettingChanged) {
+  url::Origin embedder =
+      url::Origin::Create(GURL("https://embedder.example.com"));
+  url::Origin requesting =
+      url::Origin::Create(GURL("https://requesting.example.com"));
+
+  cache()->AddPermission(embedder, requesting);
+  EXPECT_TRUE(cache()->HasPermission(embedder, requesting));
+
+  auto* hcsm = HostContentSettingsMapFactory::GetForProfile(profile());
+  hcsm->SetContentSettingDefaultScope(embedder.GetURL(), embedder.GetURL(),
+                                      ContentSettingsType::COOKIES,
+                                      CONTENT_SETTING_BLOCK);
+
+  // The cache should NOT be cleared.
+  EXPECT_TRUE(cache()->HasPermission(embedder, requesting));
+}
+
+}  // namespace controlled_frame
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 1fc5f10..fbbcbdcb 100644
--- a/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.cc
@@ -822,3 +822,105 @@
 }
 
 }  // namespace controlled_frame
+
+namespace controlled_frame {
+
+class ControlledFrameMediaPermissionCacheBrowserTest
+    : 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();
+  }
+
+  std::string RunGetVideo(content::RenderFrameHost* frame) {
+    return content::EvalJs(frame, R"(
+      (async function() {
+        try {
+          const stream =
+              await navigator.mediaDevices.getUserMedia({video: true});
+          const success = stream.getVideoTracks().length > 0;
+          stream.getTracks().forEach(t => t.stop());
+          return success ? 'SUCCESS' : 'FAIL';
+        } catch (err) {
+          return 'FAIL: ' + err.name;
+        }
+      })();
+    )")
+        .ExtractString();
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(ControlledFrameMediaPermissionCacheBrowserTest,
+                       ClearBrowsingDataClearsCache) {
+  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);
+
+  // Install and open IWA, then create ControlledFrame pointing to the guest
+  // origin.
+  web_app::ManifestBuilder manifest_builder = web_app::ManifestBuilder();
+  manifest_builder.AddPermissionsPolicyWildcard(
+      network::mojom::PermissionsPolicyFeature::kCamera);
+
+  auto [app_frame, controlled_frame] =
+      InstallAndOpenIwaThenCreateControlledFrame(
+          /*controlled_frame_host_name=*/"guest.com", "/empty.html",
+          manifest_builder);
+  ASSERT_TRUE(app_frame);
+  ASSERT_TRUE(controlled_frame);
+  ASSERT_EQ(controlled_frame->GetLastCommittedOrigin(), guest_origin);
+
+  // Set profile-wide permissions for the embedder IWA origin.
+  SetPermission(app_frame->GetLastCommittedOrigin().GetURL(),
+                ContentSettingsType::MEDIASTREAM_CAMERA, CONTENT_SETTING_ALLOW);
+
+  // Setup the embedder listener to approve unless should_deny is true.
+  EXPECT_TRUE(content::ExecJs(app_frame, R"(
+    window.should_deny = false;
+    const cf = document.querySelector('controlledframe');
+    cf.addEventListener('permissionrequest', (e) => {
+      if (window.should_deny) {
+        e.request.deny();
+      } else {
+        e.request.allow();
+      }
+    });
+  )"));
+
+  // The first request will succeed because the embedder approves it and the
+  // content setting is ALLOW.
+  EXPECT_EQ("SUCCESS", RunGetVideo(controlled_frame));
+
+  // Clear the content settings for Camera (simulating Clear Browsing Data)
+  HostContentSettingsMapFactory::GetForProfile(profile())
+      ->ClearSettingsForOneType(ContentSettingsType::MEDIASTREAM_CAMERA);
+
+  // Now change the embedder's approval logic to deny requests to verify that
+  // the cache was cleared and it falls back to the embedder listener.
+  EXPECT_TRUE(content::ExecJs(app_frame, R"(
+    window.should_deny = true;
+  )"));
+
+  // This request should now fail because the cache was wiped, and the
+  // embedder listener now explicitly denies the request. If the cache was not
+  // wiped, this would incorrectly return SUCCESS.
+  EXPECT_EQ("FAIL: NotAllowedError", RunGetVideo(controlled_frame));
+}
+
+}  // namespace controlled_frame
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.