Chrome · BrowserTag
CVE-2026-78897
Logic Error in BrowserTag
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/controlled_frame/controlled_frame_apitest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/controlled_frame/controlled_frame_apitest.cc |
modified |
Files Changed
chrome/browser/controlled_frame/controlled_frame_apitest.ccchrome/browser/extensions/api/tabs/tabs_api.ccchrome/browser/extensions/api/tabs/tabs_api.hchrome/browser/extensions/chrome_extensions_browser_client.ccchrome/browser/extensions/chrome_extensions_browser_client.h
Patch
From b4f89038dec62d6ed9f362391a048aa126c419f8 Mon Sep 17 00:00:00 2001 From: Giovanni Pezzino <[email protected]> Date: Tue, 07 Jul 2026 01:20:54 -0700 Subject: [PATCH] Enforce DisableScreenshots enterprise policy in webview and controlledframe The captureVisibleRegion API used by <webview> and <controlledframe> components failed to check the DisableScreenshots enterprise policy preference. Because WebViewInternalCaptureVisibleRegionFunction resides in the extensions/ layer, it only checked IsScreenshotRestricted (DLP restrictions) and omitted global preference checks. This CL unifies screenshot restriction checks by defining ScreenshotAccessError in extensions/browser/screenshot_access.h and updating ExtensionsBrowserClient::IsScreenshotRestricted() and WebContentsCaptureClient::GetScreenshotAccess() to return base::expected<void, ScreenshotAccessError>. In ChromeExtensionsBrowserClient, this method checks both prefs::kDisableScreenshots and DLP restrictions. Both WebViewInternalCaptureVisibleRegionFunction::GetScreenshotAccess() and TabsCaptureVisibleTabFunction::GetScreenshotAccess() now directly forward to this unified method. BUG=b:513486883 TAG=agy TEST=ControlledFrameApiTest.CaptureVisibleRegionDisabledByPolicy Change-Id: I9ef600a16bf56b78980cb54b3c246eb96a6a6964 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8036583 Reviewed-by: Andrew Rayskiy <[email protected]> Reviewed-by: Devlin Cronin <[email protected]> Commit-Queue: Giovanni Pezzino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1657764} --- diff --git a/chrome/browser/controlled_frame/controlled_frame_apitest.cc b/chrome/browser/controlled_frame/controlled_frame_apitest.cc index 357088d7..3f70244 100644 --- a/chrome/browser/controlled_frame/controlled_frame_apitest.cc +++ b/chrome/browser/controlled_frame/controlled_frame_apitest.cc @@ -25,9 +25,11 @@ #include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h" #include "chrome/common/chrome_features.h" #include "chrome/common/chrome_paths.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/embedder_support/user_agent_utils.h" +#include "components/prefs/pref_service.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/service_worker_context.h" #include "content/public/browser/service_worker_running_info.h" @@ -479,6 +481,44 @@ EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest)); } +IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, + CaptureVisibleRegionDisabledByPolicy) { + std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = + web_app::IsolatedWebAppBuilder( + web_app::ManifestBuilder().AddPermissionsPolicy( + network::mojom::PermissionsPolicyFeature::kControlledFrame, + /*self=*/true, + /*origins=*/{})) + .BuildBundle(); + app->TrustSigningKey(); + ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, + app->Install(profile())); + content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); + + ASSERT_TRUE(CreateControlledFrame( + app_frame, embedded_https_test_server().GetURL("/index.html"))); + + profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots, true); + + const std::string script = R"( + (async function() { + const frame = document.getElementsByTagName('controlledframe')[0]; + if (!frame || !frame.request) { + return 'NO_FRAME'; + } + try { + await frame.captureVisibleRegion(); + return 'UNEXPECTED_SUCCESS'; + } catch (err) { + return err.message ? err.message : String(err); + } + })(); + )"; + + EXPECT_EQ("Failed to capture webview: screenshot has been disabled", + content::EvalJs(app_frame, script)); +} + IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, EnterFullScreenControlledFrame) { const GURL embed_url = embedded_https_test_server().GetURL("/index.html"); const url::Origin embed_origin = url::Origin::Create(embed_url); diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc index d09c83330..a8808f5 100644 --- a/chrome/browser/extensions/api/tabs/tabs_api.cc +++ b/chrome/browser/extensions/api/tabs/tabs_api.cc @@ -3593,20 +3593,10 @@ TabsCaptureVisibleTabFunction::TabsCaptureVisibleTabFunction() : chrome_details_(this) {} -WebContentsCaptureClient::ScreenshotAccess +base::expected<void, extensions::ScreenshotAccessError> TabsCaptureVisibleTabFunction::GetScreenshotAccess( content::WebContents* web_contents) const { - PrefService* service = - Profile::FromBrowserContext(browser_context())->GetPrefs(); - if (service->GetBoolean(prefs::kDisableScreenshots)) { - return ScreenshotAccess::kDisabledByPreferences; - } - - if (ExtensionsBrowserClient::Get()->IsScreenshotRestricted(web_contents)) { - return ScreenshotAccess::kDisabledByDlp; - } - - return ScreenshotAccess::kEnabled; + return ExtensionsBrowserClient::Get()->IsScreenshotRestricted(web_contents); } bool TabsCaptureVisibleTabFunction::ClientAllowsTransparency() { diff --git a/chrome/browser/extensions/api/tabs/tabs_api.h b/chrome/browser/extensions/api/tabs/tabs_api.h index 065f696..a5c36f80 100644 --- a/chrome/browser/extensions/api/tabs/tabs_api.h +++ b/chrome/browser/extensions/api/tabs/tabs_api.h @@ -489,7 +489,7 @@ content::WebContents* GetWebContentsForID(int window_id, std::string* error); // extensions::WebContentsCaptureClient: - ScreenshotAccess GetScreenshotAccess( + base::expected<void, extensions::ScreenshotAccessError> GetScreenshotAccess( content::WebContents* web_contents) const override; bool ClientAllowsTransparency() override; void OnCaptureSuccess(const SkBitmap& bitmap) override; diff --git a/chrome/browser/extensions/chrome_extensions_browser_client.cc b/chrome/browser/extensions/chrome_extensions_browser_client.cc index 29c7642..459bf07 100644 --- a/chrome/browser/extensions/chrome_extensions_browser_client.cc +++ b/chrome/browser/extensions/chrome_extensions_browser_client.cc @@ -93,6 +93,7 @@ #include "components/content_settings/core/browser/host_content_settings_map.h" #include "components/content_settings/core/common/pref_names.h" #include "components/embedder_support/user_agent_utils.h" +#include "components/prefs/pref_service.h" #include "components/privacy_sandbox/privacy_sandbox_prefs.h" #include "components/proxy_config/proxy_config_pref_names.h" #include "components/search_engines/template_url_service.h" @@ -122,6 +123,7 @@ #include "extensions/browser/process_manager_delegate.h" #include "extensions/browser/safe_browsing_delegate.h" #include "extensions/browser/scoped_extension_keep_alive.h" +#include "extensions/browser/screenshot_access.h" #include "extensions/browser/unpacked_installer.h" #include "extensions/browser/url_request_util.h" #include "extensions/common/extension_id.h" @@ -814,14 +816,22 @@ return util::HasIsolatedStorage(extension_id, context); } -bool ChromeExtensionsBrowserClient::IsScreenshotRestricted( +base::expected<void, extensions::ScreenshotAccessError> +ChromeExtensionsBrowserClient::IsScreenshotRestricted( content::WebContents* web_contents) const { -#if !BUILDFLAG(IS_CHROMEOS) - return false; -#else - return policy::DlpContentManager::Get()->IsScreenshotApiRestricted( - web_contents); + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + if (profile->GetPrefs()->GetBoolean(prefs::kDisableScreenshots)) { + return base::unexpected( + extensions::ScreenshotAccessError::kDisabledByPreferences); + } +#if BUILDFLAG(IS_CHROMEOS) + if (policy::DlpContentManager::Get()->IsScreenshotApiRestricted( + web_contents)) { + return base::unexpected(extensions::ScreenshotAccessError::kDisabledByDlp); + } #endif + return base::ok(); } bool ChromeExtensionsBrowserClient::IsValidTabId( diff --git a/chrome/browser/extensions/chrome_extensions_browser_client.h b/chrome/browser/extensions/chrome_extensions_browser_client.h index 8e3cbeb..5fa368c 100644 --- a/chrome/browser/extensions/chrome_extensions_browser_client.h +++ b/chrome/browser/extensions/chrome_extensions_browser_client.h @@ -12,6 +12,7 @@ #include "base/functional/callback_forward.h" #include "base/lazy_instance.h" #include "base/memory/scoped_refptr.h" +#include "base/types/expected.h" #include "base/values.h" #include "build/build_config.h" #include "build/chromeos_buildflags.h" @@ -222,8 +223,8 @@ const base::FilePath& path) override; bool HasIsolatedStorage(const ExtensionId& extension_id, content::BrowserContext* context) override;
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/controlled_frame/controlled_frame_apitest.cc b/chrome/browser/controlled_frame/controlled_frame_apitest.cc
index 357088d7..3f70244 100644
--- a/chrome/browser/controlled_frame/controlled_frame_apitest.cc
+++ b/chrome/browser/controlled_frame/controlled_frame_apitest.cc
@@ -25,9 +25,11 @@
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.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/embedder_support/user_agent_utils.h"
+#include "components/prefs/pref_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_running_info.h"
@@ -479,6 +481,44 @@
EXPECT_EQ(kEvalSuccessStr, VerifyBackgroundColorIsRed(web_view_guest));
}
+IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest,
+ CaptureVisibleRegionDisabledByPolicy) {
+ std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
+ web_app::IsolatedWebAppBuilder(
+ web_app::ManifestBuilder().AddPermissionsPolicy(
+ network::mojom::PermissionsPolicyFeature::kControlledFrame,
+ /*self=*/true,
+ /*origins=*/{}))
+ .BuildBundle();
+ app->TrustSigningKey();
+ ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
+ app->Install(profile()));
+ content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
+
+ ASSERT_TRUE(CreateControlledFrame(
+ app_frame, embedded_https_test_server().GetURL("/index.html")));
+
+ profile()->GetPrefs()->SetBoolean(prefs::kDisableScreenshots, true);
+
+ const std::string script = R"(
+ (async function() {
+ const frame = document.getElementsByTagName('controlledframe')[0];
+ if (!frame || !frame.request) {
+ return 'NO_FRAME';
+ }
+ try {
+ await frame.captureVisibleRegion();
+ return 'UNEXPECTED_SUCCESS';
+ } catch (err) {
+ return err.message ? err.message : String(err);
+ }
+ })();
+ )";
+
+ EXPECT_EQ("Failed to capture webview: screenshot has been disabled",
+ content::EvalJs(app_frame, script));
+}
+
IN_PROC_BROWSER_TEST_F(ControlledFrameApiTest, EnterFullScreenControlledFrame) {
const GURL embed_url = embedded_https_test_server().GetURL("/index.html");
const url::Origin embed_origin = url::Origin::Create(embed_url);
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