Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in FontAccess
DescriptionIncorrect authorization in FontAccess
ComponentFontAccess
Sub-componentBlink>Storage>FontAccess
Bug ClassAuthorization Error
Tracker553150261
Fix commitba9c3cc3c294 (chromium/src) +38/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/font_access/font_access_manager_unittest.cc
modified

Files Changed

  • content/browser/font_access/font_access_manager.cc
  • content/browser/font_access/font_access_manager_unittest.cc
From ba9c3cc3c294c1608c06434b0b4e795624dbd5db Mon Sep 17 00:00:00 2001
From: Kent Tamura <tkent@chromium.org>
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 <tkent@chromium.org>
Reviewed-by: Chris Harrelson <chrishtr@chromium.org>
Auto-Submit: Kent Tamura <tkent@chromium.org>
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • components/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

  1. 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().

  1. 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 the DidAccessInitialMainDocument IPC, IsUnmodifiedBlankTab() remains true. As a result, GetVisibleEntry() returns the pending navigation entry rather than the committed entry, setting WebContents::GetVisibleURL() to the pending navigation’s target URL.

  2. 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

  1. From a compromised renderer at https://evil.example, create an iframe with sandbox="allow-scripts allow-popups" and call window.open('about:blank').
  2. 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).
  3. The compromised renderer sends a BeginNavigation IPC for the popup targeting a victim origin with a known LOCAL_FONTS grant (e.g., https://www.figma.com/) while withholding DidAccessInitialMainDocument. WebContents::GetVisibleURL() now evaluates to the victim origin.
  4. Over the popup frame’s BrowserInterfaceBroker, the renderer binds blink.mojom.FontAccessManager and calls EnumerateLocalFonts().
  5. The browser verifies visibility and evaluates the permission against GetVisibleURL() (https://www.figma.com/), finding the persisted grant and returning PermissionStatus::GRANTED.
  6. FontAccessManager::DidRequestPermission invokes FontEnumerationCache::GetFontEnumerationData and returns a base::ReadOnlySharedMemoryRegion containing the complete list of locally installed fonts (blink::FontEnumerationTable).
  7. 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

  1. Remove or Restrict GetVisibleURL Fallback: Disallow using WebContents::GetVisibleURL() for permission lookups in PermissionUtil::GetLastCommittedOriginAsURL when the frame has an opaque origin, or ensure opaque origins never inherit permission grants from uncommitted pending navigations.
  2. Validate Requesting Context in FontAccessManager: Ensure FontAccessManager::EnumerateLocalFonts explicitly 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.

View on issue tracker
Links in the report