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 <giovax@google.com>
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 <greengrape@google.com>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Giovanni Pezzino <giovax@google.com>
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