CVE-2026-106404
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/font_access/font_access_manager_unittest.cc |
modified |
Files Changed
content/browser/font_access/font_access_manager.cccontent/browser/font_access/font_access_manager_unittest.cc
Patch
From ba9c3cc3c294c1608c06434b0b4e795624dbd5db Mon Sep 17 00:00:00 2001 From: Kent Tamura <[email protected]> Date: Wed, 02 Sep 2026 17:47:21 -0700 Subject: [PATCH] Font Access: Deny enumeration for frames with opaque origins Documents with opaque origins (e.g. sandboxed frames, data: URLs) have no persistable permission identity. Without this gate, the permission lookup falls back to WebContents::GetVisibleURL(), which could allow a compromised renderer to borrow permissions from another origin. This CL rejects local font enumeration requests from documents with opaque origins and adds a unit test to verify this behavior. Bug: 553150261 Change-Id: Ie0925e383bf061040ba5bc09c3f1e0d09aa672ad Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8332627 Commit-Queue: Kent Tamura <[email protected]> Reviewed-by: Chris Harrelson <[email protected]> Auto-Submit: Kent Tamura <[email protected]> Cr-Commit-Position: refs/heads/main@{#1691238} --- diff --git a/content/browser/font_access/font_access_manager.cc b/content/browser/font_access/font_access_manager.cc index 8b0e907..eb6e1a5 100644 --- a/content/browser/font_access/font_access_manager.cc +++ b/content/browser/font_access/font_access_manager.cc @@ -102,6 +102,23 @@ return; } + // Documents with opaque origins (sandboxed frames/popups, data: URLs) have + // no persistable permission identity: the LOCAL_FONTS content setting can + // never be keyed to them. Without this gate, the permission lookup below + // falls back to WebContents::GetVisibleURL() for opaque-origin primary main + // frames (PermissionUtil::GetLastCommittedOriginAsURL), which a compromised + // renderer can steer to an arbitrary victim origin via a never-committing + // pending navigation and thereby borrow that origin's persisted grant. + // Reject instead of CHECKing because this state is reachable by + // legitimate-looking renderer traffic. + // See crbug.com/553150261. + if (rfh->GetLastCommittedOrigin().opaque()) { + std::move(callback).Run( + blink::mojom::FontEnumerationStatus::kPermissionDenied, + base::ReadOnlySharedMemoryRegion()); + return; + } + content::PermissionController* permission_controller = rfh->GetBrowserContext()->GetPermissionController(); DCHECK(permission_controller); diff --git a/content/browser/font_access/font_access_manager_unittest.cc b/content/browser/font_access/font_access_manager_unittest.cc index 5862d7ba..e994830 100644 --- a/content/browser/font_access/font_access_manager_unittest.cc +++ b/content/browser/font_access/font_access_manager_unittest.cc @@ -293,6 +293,27 @@ EXPECT_EQ(status, FontEnumerationStatus::kPermissionDenied); } +TEST_F(FontAccessManagerTest, PermissionDeniedForOpaqueOrigin) { + AutoGrantPermission(); + SimulateUserActivation(); + + NavigateAndCommit(GURL("data:text/html,test")); + ASSERT_TRUE(main_rfh()->GetLastCommittedOrigin().opaque()); + + const int process_id = main_rfh()->GetProcess()->GetDeprecatedID(); + const int routing_id = main_rfh()->GetRoutingID(); + const GlobalRenderFrameHostId main_frame_id(process_id, routing_id); + + mojo::Remote<blink::mojom::FontAccessManager> manager_remote; + manager_->BindReceiver(main_frame_id, + manager_remote.BindNewPipeAndPassReceiver()); + FontAccessManagerSync sync_manager(manager_remote.get()); + + const auto [status, region] = sync_manager.EnumerateLocalFonts(); + EXPECT_EQ(status, FontEnumerationStatus::kPermissionDenied); + EXPECT_FALSE(region.IsValid()); +} + } // namespace } // namespace content
Regression Test / PoC
diff --git a/content/browser/font_access/font_access_manager_unittest.cc b/content/browser/font_access/font_access_manager_unittest.cc
index 5862d7ba..e994830 100644
--- a/content/browser/font_access/font_access_manager_unittest.cc
+++ b/content/browser/font_access/font_access_manager_unittest.cc
@@ -293,6 +293,27 @@
EXPECT_EQ(status, FontEnumerationStatus::kPermissionDenied);
}
+TEST_F(FontAccessManagerTest, PermissionDeniedForOpaqueOrigin) {
+ AutoGrantPermission();
+ SimulateUserActivation();
+
+ NavigateAndCommit(GURL("data:text/html,test"));
+ ASSERT_TRUE(main_rfh()->GetLastCommittedOrigin().opaque());
+
+ const int process_id = main_rfh()->GetProcess()->GetDeprecatedID();
+ const int routing_id = main_rfh()->GetRoutingID();
+ const GlobalRenderFrameHostId main_frame_id(process_id, routing_id);
+
+ mojo::Remote<blink::mojom::FontAccessManager> manager_remote;
+ manager_->BindReceiver(main_frame_id,
+ manager_remote.BindNewPipeAndPassReceiver());
+ FontAccessManagerSync sync_manager(manager_remote.get());
+
+ const auto [status, region] = sync_manager.EnumerateLocalFonts();
+ EXPECT_EQ(status, FontEnumerationStatus::kPermissionDenied);
+ EXPECT_FALSE(region.IsValid());
+}
+
} // namespace
} // namespace content
Original Bug Report
Local font enumeration bypass via opaque origin GetVisibleURL fallback
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: In FontAccessManager::EnumerateLocalFonts, permission resolution for a document with an opaque origin can fall back to WebContents::GetVisibleURL(). A compromised renderer hosting an unmodified blank popup can steer the visible URL to an arbitrary victim origin via a pending navigation and borrow its persisted LOCAL_FONTS grant. Because granted permissions bypass the transient user activation requirement, an attacker could silently enumerate all installed local fonts without user interaction.
Affected files:
content/browser/font_access/font_access_manager.cccomponents/permissions/permission_util.cc
Estimated timestamp from git blame: 2024-08-13
Summary
FontAccessManager::EnumerateLocalFonts resolves the permission status of the calling frame through PermissionController::GetPermissionStatusForCurrentDocument. During this check, PermissionUtil::GetLastCommittedOriginAsURL falls back to WebContents::GetVisibleURL() when the frame’s committed origin is opaque. A compromised renderer can exploit this fallback by opening an opaque-origin popup and initiating an uncommitted navigation to a victim origin (such as Figma or Canva) that has a persisted LOCAL_FONTS grant. When GetPermissionStatusForCurrentDocument returns GRANTED, FontAccessManager bypasses the transient user activation requirement and directly returns the full system font enumeration table.
(Note: These analysis findings and steps are theoretical/potential based on static source analysis, as our tooling environment does not execute dynamic code.)
Root Cause Analysis
- Opaque Origin Fallback to Visible URL (
components/permissions/permission_util.cc:491-497):
GURL origin = render_frame_host->GetLastCommittedOrigin().GetURL();
if (origin.is_empty() && render_frame_host->IsInPrimaryMainFrame()) {
if (!web_contents->GetVisibleURL().is_empty()) {
origin = web_contents->GetVisibleURL().DeprecatedGetOriginAsURL();
}
}
return origin;
For documents with opaque origins (e.g. sandboxed popups), url::Origin::GetURL() returns an empty GURL(). If the document is in a primary main frame, the origin fallback extracts the origin from WebContents::GetVisibleURL().
-
Visible URL Steering via Pending Entry (
content/browser/renderer_host/navigation_controller_impl.cc:1127-1155, 3513-3516): When a renderer initiates navigation in a new blank tab and withholds theDidAccessInitialMainDocumentIPC,IsUnmodifiedBlankTab()remains true. As a result,GetVisibleEntry()returns the pending navigation entry rather than the committed entry, settingWebContents::GetVisibleURL()to the pending navigation’s target URL. -
User Activation Bypass on Granted Permissions (
content/browser/font_access/font_access_manager.cc:109-121):
auto status = permission_controller->GetPermissionStatusForCurrentDocument(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::LOCAL_FONTS),
rfh);
if (status != blink::mojom::PermissionStatus::ASK) {
// Permission has been requested before.
DidRequestPermission(
std::move(callback),
PermissionResult(status, PermissionStatusSource::UNSPECIFIED));
return;
}
When status is GRANTED (due to the victim’s persisted grant in HostContentSettingsMap), execution jumps directly to DidRequestPermission, completely skipping the transient user activation check (rfh->HasTransientUserActivation()).
Potential Attack Steps
- From a compromised renderer at
https://evil.example, create an iframe withsandbox="allow-scripts allow-popups"and callwindow.open('about:blank'). - The popup opens in the attacker’s renderer process with an opaque origin derived via
SetOriginDependentStateOfNewFrame(content/browser/renderer_host/render_frame_host_impl.cc:5838-5904). - The compromised renderer sends a
BeginNavigationIPC for the popup targeting a victim origin with a knownLOCAL_FONTSgrant (e.g.,https://www.figma.com/) while withholdingDidAccessInitialMainDocument.WebContents::GetVisibleURL()now evaluates to the victim origin. - Over the popup frame’s
BrowserInterfaceBroker, the renderer bindsblink.mojom.FontAccessManagerand callsEnumerateLocalFonts(). - The browser verifies visibility and evaluates the permission against
GetVisibleURL()(https://www.figma.com/), finding the persisted grant and returningPermissionStatus::GRANTED. FontAccessManager::DidRequestPermissioninvokesFontEnumerationCache::GetFontEnumerationDataand returns abase::ReadOnlySharedMemoryRegioncontaining the complete list of locally installed fonts (blink::FontEnumerationTable).- The attacker can repeat this process across multiple candidate origins to silently probe the user’s permission state (acting as a cross-origin permission oracle).
Suggested Remediation
- Remove or Restrict
GetVisibleURLFallback: Disallow usingWebContents::GetVisibleURL()for permission lookups inPermissionUtil::GetLastCommittedOriginAsURLwhen the frame has an opaque origin, or ensure opaque origins never inherit permission grants from uncommitted pending navigations. - Validate Requesting Context in FontAccessManager: Ensure
FontAccessManager::EnumerateLocalFontsexplicitly rejects calls originating from opaque or sandboxed execution contexts, or enforces that the frame’s actual committed origin matches the origin evaluated for permission grants.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.