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 <[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.
References
On This Page