Chrome · Browser › Controlled Frame
CVE-2026-106350
Authorization Error in Browser (Controlled Frame)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/controlled_frame/controlled_frame_media_access_handler.cc |
modified | |
ifchrome/browser/controlled_frame/controlled_frame_media_permission_cache.cc |
modified | |
HostContentSettingsMapchrome/browser/controlled_frame/controlled_frame_media_permission_cache.h |
modified | |
Profilechrome/browser/controlled_frame/controlled_frame_media_permission_cache.h |
modified | |
ControlledFrameMediaPermissionCachechrome/browser/controlled_frame/controlled_frame_media_permission_cache.h |
modified | |
GetInstancechrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.cc |
modified | |
BrowserContextchrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h |
modified | |
ControlledFrameMediaPermissionCachechrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h |
modified | |
ControlledFrameMediaPermissionCacheFactorychrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.h |
modified | |
ControlledFrameMediaPermissionCacheTestchrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc |
modified | |
TEST_Fchrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.cc |
modified |
Files Changed
chrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.hchrome/browser/controlled_frame/controlled_frame_media_permission_cache_unittest.ccchrome/browser/controlled_frame/controlled_frame_media_permission_cache.ccchrome/browser/controlled_frame/controlled_frame_media_permission_cache_factory.ccchrome/browser/profiles/BUILD.gnchrome/browser/controlled_frame/controlled_frame_media_permission_cache.hchrome/browser/controlled_frame/controlled_frame_media_access_handler.hchrome/browser/controlled_frame/controlled_frame_permission_request_browsertest.ccchrome/browser/profiles/chrome_browser_main_extra_parts_profiles.ccchrome/browser/controlled_frame/controlled_frame_media_access_handler.ccchrome/browser/controlled_frame/BUILD.gn
Patch
From 37038e1ec21110557ff8237e3a2ea6fcfa39adfc Mon Sep 17 00:00:00 2001
From: Simon Hangl <simonha@google.com>
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 <alexilin@chromium.org>
Commit-Queue: Simon Hangl <simonha@google.com>
Reviewed-by: Kevin McNee <mcnee@chromium.org>
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.
References
On This Page