Low watch CVSS 6.5 chrome Race 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in Extensions
DescriptionRace condition in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassRace
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker533021205
Fix commitb500751eac4a (chromium/src) +42/-5
CWECWE-367 (TOCTOU race)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-07-15 (public git)
Disclosed2026-08-25
Patch gap41 days — fix public in source before the advisory

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, while GetLastCommittedURL() 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 as kCannotAccessChromeUrl.

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.

Key insight
The restricted-URL check read the visible (possibly pending) URL, but zoom acts on the committed document. Switching the check to GetLastCommittedURL() makes it look at the same document the zoom change actually affects.

Attack Path

  1. Extension installed An attacker needs an installed extension that can call the chrome.tabs zoom APIs. Ordinary web content cannot reach this path.
  2. Restricted page committed A tab has a restricted URL such as chrome://version/ committed, which would normally make tabs.setZoom fail.
  3. 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 LoadURLWithParams call in the test). GetVisibleURL() now returns the unrestricted target, but GetLastCommittedURL() still returns the restricted page.
  4. Zoom call during the race window The extension calls tabs.setZoom or tabs.setZoomSettings before 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

A malicious or compromised extension could change the zoom level or zoom settings of a tab showing a restricted page (such as a 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

FunctionChangeNotes
ExtensionApiPdfTest
chrome/browser/extensions/api/tabs/tabs_test.cc
modified

Files Changed

  • chrome/browser/extensions/api/tabs/tabs_test.cc
  • chrome/browser/extensions/api/tabs/tabs_api.cc

Audit Directions

  • Visible-URL security checks
    Search extension and browser code for GetVisibleURL() (or GetURL() on a pending or visible NavigationEntry) passed into IsRestrictedUrl(), 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 state
    Look 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 effects
    Where 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.
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 {
Loading diff…

Regression Test / PoC

shipped with the fix
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 {
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.