CVE-2026-79017
Overview
Background
- `chrome.tabs.setZoom` / `chrome.tabs.setZoomSettings`
- extension APIs that change the zoom level or zoom mode of a tab, and with the default per-origin scope they change the zoom stored for the frame’s committed origin.
- Visible URL vs. last committed URL
GetVisibleURL()can return the target of a pending browser-initiated navigation before it commits, whileGetLastCommittedURL()returns the URL of the document actually loaded in the tab.- Restricted URL
- a URL such as
chrome://version/that extensions may not script or modify;PermissionsData::IsRestrictedUrl()rejects these and returns errors such askCannotAccessChromeUrl.
Root Cause Analysis
In chrome/browser/extensions/api/tabs/tabs_api.cc, two tabs zoom handlers built the URL for their restricted-URL check from web_contents->GetVisibleURL() and passed it to extension()->permissions_data()->IsRestrictedUrl(url, &error). The hunk context does not name the enclosing functions; the regression test drives them through RunSetZoomExpectError and RunSetZoomSettingsExpectError, so they are the tabs.setZoom and tabs.setZoomSettings implementations. The zoom change itself applies to the document that is actually committed (and, per the commit message, to the zoom settings for that committed origin). So the security check and the effect looked at different URLs. While a browser-initiated navigation from a restricted page to an ordinary page was still pending, the visible URL was already the ordinary target, so IsRestrictedUrl() passed while the restricted page was still committed.
The fix replaces both calls with web_contents->GetLastCommittedURL(), so the check looks at the same document the zoom change affects. This is a tightening, because an extension now gets kCannotAccessChromeUrl in that window.
GetLastCommittedURL() makes it look at the same document the zoom change actually affects.Attack Path
- Extension installed
An attacker needs an installed extension that can call the
chrome.tabszoom APIs. Ordinary web content cannot reach this path. - Restricted page committed
A tab has a restricted URL such as
chrome://version/committed, which would normally maketabs.setZoomfail. - Pending navigation changes the visible URL
A browser-initiated navigation to an unrestricted URL starts (for example, one the user starts, as the commit message notes, or the
LoadURLWithParamscall in the test).GetVisibleURL()now returns the unrestricted target, butGetLastCommittedURL()still returns the restricted page. - Zoom call during the race window
The extension calls
tabs.setZoomortabs.setZoomSettingsbefore the navigation commits. Before the fix, the check on the visible URL passed, and the zoom or zoom setting was applied while the restricted page was still committed.
Impact Assessment
chrome:// page), and through per-origin zoom, the zoom stored for that restricted origin. Extensions are not allowed to do this. It requires an installed extension with access to the tabs zoom APIs, plus a timing window during an uncommitted browser-initiated navigation away from the restricted page. The advisory describes it only as a “Race condition in Extensions”. Nothing in the patch suggests impact beyond changing zoom on restricted pages, and any wider UI-spoofing or redress use would be (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ExtensionApiPdfTestchrome/browser/extensions/api/tabs/tabs_test.cc |
modified |
Files Changed
chrome/browser/extensions/api/tabs/tabs_test.ccchrome/browser/extensions/api/tabs/tabs_api.cc
Audit Directions
- Visible-URL security checksSearch extension and browser code for
GetVisibleURL()(orGetURL()on a pending or visibleNavigationEntry) passed intoIsRestrictedUrl(),CanAccessPage(), or host-permission checks. Any check that guards an action on the current document should use the last committed URL or origin. - Check/effect mismatch on navigation stateLook for APIs whose effect applies to the committed document or origin (zoom, capture, script injection, content settings) but whose permission check reads URL state that changes when a navigation starts. Test them with an uncommitted browser-initiated navigation in flight.
- Origin-keyed side effectsWhere an action is stored per origin (such as host zoom maps), check that the origin used for the permission decision is the same one used to compute the storage key.
Patch
From b500751eac4a405ec8c988c29db0c2c7edd714a7 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Wed, 15 Jul 2026 11:43:37 -0700
Subject: [PATCH] [Extensions] Use GetLastCommittedURL() in tabs zoom functions
Changing the zoom settings for a tab affects the zoom settings for the
frame's origin, which uses the committed origin. However, the check
compared visible URL to see if it was a restricted URL. This would
allow an extension to zoom a tab on a restricted URL if the visible
URL differed (such as if a user navigation was under way).
Fix this so that the tabs zoom checks use the last committed URL, and
add a regression test.
Fixed: 533021205
Change-Id: I0dfbf22eec7ae1e08c0619f00c8dac9862bf3f92
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8087626
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Reviewed-by: Andrea Orru <andreaorru@chromium.org>
Auto-Submit: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1662763}
---
diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc
index 3e365131..efc8a9e 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_api.cc
@@ -3938,7 +3938,7 @@
return RespondNow(Error(std::move(error)));
}
- GURL url(web_contents->GetVisibleURL());
+ const GURL& url = web_contents->GetLastCommittedURL();
if (extension()->permissions_data()->IsRestrictedUrl(url, &error)) {
return RespondNow(Error(std::move(error)));
}
@@ -4000,7 +4000,7 @@
return RespondNow(Error(std::move(error)));
}
- GURL url(web_contents->GetVisibleURL());
+ const GURL& url = web_contents->GetLastCommittedURL();
if (extension()->permissions_data()->IsRestrictedUrl(url, &error)) {
return RespondNow(Error(std::move(error)));
}
diff --git a/chrome/browser/extensions/api/tabs/tabs_test.cc b/chrome/browser/extensions/api/tabs/tabs_test.cc
index d3bc73d..991a9617 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -3178,9 +3178,8 @@
error = RunSetZoomSettingsExpectError(bogus_id, "manual", "per-tab");
EXPECT_TRUE(base::MatchPattern(error, ExtensionTabUtil::kTabNotFoundError));
- const char kNewTestTabArgs[] = "chrome://version";
- params = GetOpenParams(kNewTestTabArgs);
- web_contents = browser()->OpenURL(params, /*navigation_handle_callback=*/{});
+ const char kNewTestTabArgs[] = "chrome://version/";
+ web_contents = OpenUrlAndWaitForLoad(GURL(kNewTestTabArgs));
tab_id = ExtensionTabUtil::GetTabId(web_contents);
// Test chrome.tabs.setZoom().
@@ -3194,6 +3193,44 @@
base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl));
}
+// Tests that tabs.zoom properly looks at the committed (not visible) URLs.
+// Regression test for crbug.com/533021205.
+IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest,
+ ZoomingRestrictedURLsChecksCommittedURL) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ // Open a restricted URL and wait for it to load.
+ const char kRestrictedUrl[] = "chrome://version/";
+ content::WebContents* web_contents =
+ OpenUrlAndWaitForLoad(GURL(kRestrictedUrl));
+ int tab_id = ExtensionTabUtil::GetTabId(web_contents);
+
+ ASSERT_EQ(GURL(kRestrictedUrl), web_contents->GetLastCommittedURL());
+ ASSERT_EQ(GURL(kRestrictedUrl), web_contents->GetVisibleURL());
+
+ // Start a browser-initiated navigation to an unrestricted URL, but do not
+ // wait for it to commit.
+ GURL unrestricted_url = embedded_test_server()->GetURL("/title1.html");
+ content::NavigationController::LoadURLParams load_params(unrestricted_url);
+ web_contents->GetController().LoadURLWithParams(load_params);
+
+ // The visible URL should update immediately to the pending navigation target.
+ ASSERT_EQ(unrestricted_url, web_contents->GetVisibleURL());
+ // The committed URL should still be the restricted one.
+ ASSERT_EQ(GURL(kRestrictedUrl), web_contents->GetLastCommittedURL());
+
+ // Attempt to set zoom. It should fail because the committed URL is
+ // restricted.
+ std::string error = RunSetZoomExpectError(tab_id, 3.14159);
+ EXPECT_TRUE(
+ base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl));
+
+ // Attempt to set zoom settings. It should also fail.
+ error = RunSetZoomSettingsExpectError(tab_id, "manual", "per-tab");
+ EXPECT_TRUE(
+ base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl));
+}
+
#if BUILDFLAG(ENABLE_PDF)
class ExtensionApiPdfTest : public base::test::WithFeatureOverride,
public PDFExtensionTestBase {
Regression Test / PoC
diff --git a/chrome/browser/extensions/api/tabs/tabs_test.cc b/chrome/browser/extensions/api/tabs/tabs_test.cc
index d3bc73d..991a9617 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -3178,9 +3178,8 @@
error = RunSetZoomSettingsExpectError(bogus_id, "manual", "per-tab");
EXPECT_TRUE(base::MatchPattern(error, ExtensionTabUtil::kTabNotFoundError));
- const char kNewTestTabArgs[] = "chrome://version";
- params = GetOpenParams(kNewTestTabArgs);
- web_contents = browser()->OpenURL(params, /*navigation_handle_callback=*/{});
+ const char kNewTestTabArgs[] = "chrome://version/";
+ web_contents = OpenUrlAndWaitForLoad(GURL(kNewTestTabArgs));
tab_id = ExtensionTabUtil::GetTabId(web_contents);
// Test chrome.tabs.setZoom().
@@ -3194,6 +3193,44 @@
base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl));
}
+// Tests that tabs.zoom properly looks at the committed (not visible) URLs.
+// Regression test for crbug.com/533021205.
+IN_PROC_BROWSER_TEST_F(ExtensionTabsZoomTest,
+ ZoomingRestrictedURLsChecksCommittedURL) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ // Open a restricted URL and wait for it to load.
+ const char kRestrictedUrl[] = "chrome://version/";
+ content::WebContents* web_contents =
+ OpenUrlAndWaitForLoad(GURL(kRestrictedUrl));
+ int tab_id = ExtensionTabUtil::GetTabId(web_contents);
+
+ ASSERT_EQ(GURL(kRestrictedUrl), web_contents->GetLastCommittedURL());
+ ASSERT_EQ(GURL(kRestrictedUrl), web_contents->GetVisibleURL());
+
+ // Start a browser-initiated navigation to an unrestricted URL, but do not
+ // wait for it to commit.
+ GURL unrestricted_url = embedded_test_server()->GetURL("/title1.html");
+ content::NavigationController::LoadURLParams load_params(unrestricted_url);
+ web_contents->GetController().LoadURLWithParams(load_params);
+
+ // The visible URL should update immediately to the pending navigation target.
+ ASSERT_EQ(unrestricted_url, web_contents->GetVisibleURL());
+ // The committed URL should still be the restricted one.
+ ASSERT_EQ(GURL(kRestrictedUrl), web_contents->GetLastCommittedURL());
+
+ // Attempt to set zoom. It should fail because the committed URL is
+ // restricted.
+ std::string error = RunSetZoomExpectError(tab_id, 3.14159);
+ EXPECT_TRUE(
+ base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl));
+
+ // Attempt to set zoom settings. It should also fail.
+ error = RunSetZoomSettingsExpectError(tab_id, "manual", "per-tab");
+ EXPECT_TRUE(
+ base::MatchPattern(error, manifest_errors::kCannotAccessChromeUrl));
+}
+
#if BUILDFLAG(ENABLE_PDF)
class ExtensionApiPdfTest : public base::test::WithFeatureOverride,
public PDFExtensionTestBase {