Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Permissions
DescriptionInformation leak in Permissions
ComponentPermissions
Sub-componentPermissions
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker515493668
Fix commit46c64c0bd343 (chromium/src) +3/-61
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-22 (per vendor advisory)
Fix landed2026-09-17 (public git)
Disclosed2026-09-17

Background

`initial_permission_statuses`
A field of the CommitNavigationParams mojo struct that pre-seeds the renderer’s permission cache at document commit time so the new document can answer permission queries without a browser round-trip.
`RenderFrameHostImpl::GetCachedPermissionStatuses`
The (now removed) browser-side helper that built a base::flat_map of PermissionName to PermissionStatus for a fixed set of permissions (VIDEO_CAPTURE, AUDIO_CAPTURE, WEB_APP_INSTALLATION, GEOLOCATION) to populate that field.
`RenderFrameHostImpl::GetCombinedPermissionStatus`
The (now removed) helper it called, which resolved each permission (including device state via GetCombinedPermissionAndDeviceStatus for device permissions) against the PermissionController for the current document.
Source vs. destination document
In a navigation the “source” is the RenderFrameHostImpl currently occupying the frame, while the “destination” is the document being navigated to; permission grants are keyed per-origin, so these two can be different origins.

Root Cause Analysis

When constructing CommitNavigationParams for a navigation, the code set initial_permission_statuses (and passed the same value into the commit argument list in navigation_request.cc) from frame_tree_node->current_frame_host()->GetCachedPermissionStatuses(). As the commit message states, that computation was “wrong, because it was computed for the source document of the navigation” — i.e. it queried the PermissionController against the RenderFrameHostImpl still hosting the previous document rather than the document being committed. Because permission grants are per-origin, the destination document could therefore be seeded with permission statuses (camera, microphone, geolocation, and on desktop web-app-installation) belonging to the previous origin, violating the invariant that a document only observes permission state for its own origin.

The fix removes GetCachedPermissionStatuses and GetCombinedPermissionStatus entirely and replaces every call site with /*initial_permission_statuses=*/std::nullopt, so no pre-computed (and possibly cross-origin) permission data is propagated to the renderer at commit. This closes the leak by falling back to the correct behavior where the new document must query permissions itself against its own origin.

Key insight
The single mistake is that the permission cache seeded into a navigation’s commit params was computed against the source (previously committed) document instead of the destination, leaking one origin’s permission grants to the next document; the fix simply stops sending any pre-computed statuses (std::nullopt) and deletes the flawed computation.

Attack Path

  1. Grant permissions on an origin The user grants a permission such as geolocation, camera, or microphone (or, on desktop, web-app-installation) to origin A, so its combined permission status is something other than ASK.
  2. Navigate to attacker origin A navigation occurs from origin A to an attacker-controlled origin B in the same frame, where B’s document is what will be committed.
  3. Wrong document queried During commit-param construction, GetCachedPermissionStatuses() is evaluated against current_frame_host(), which still hosts origin A, so the resulting flat_map reflects A’s grants and is stored in initial_permission_statuses.
  4. Statuses delivered to destination renderer The commit sends those statuses to the renderer that will host origin B, seeding B’s document with A’s permission state (inferred: B can then read the previous origin’s permission grants, including device availability for device permissions).
  5. Information leak Attacker origin B learns whether the prior origin had been granted sensitive permissions, information it should not be able to observe (inferred).

Impact Assessment

An attacker page can obtain the permission-grant state (camera, microphone, geolocation, and desktop web-app-installation) that belongs to the previously committed document/origin, giving a cross-document information leak observable in the destination renderer process. The precondition is that the frame navigated from another document that had a non-default (non-ASK) status for one of these permissions. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3. Any escalation beyond the described disclosure is not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
GetCachedPermissionStatuses
content/browser/renderer_host/render_frame_host_impl.cc
modified
for
content/browser/renderer_host/render_frame_host_impl.cc
modified
if
content/browser/renderer_host/render_frame_host_impl.cc
modified
CreateAutoPipReasonCallback
content/browser/renderer_host/render_frame_host_impl.cc
modified
CONTENT_EXPORT
content/browser/renderer_host/render_frame_host_impl.h
modified

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.h
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/browser/renderer_host/navigation_controller_impl.cc

Audit Directions

  • Wrong-frame data capture at navigation time
    Audit other values snapshotted from current_frame_host() / the source RenderFrameHostImpl while building CommitNavigationParams, since those may reflect the previous document rather than the one being committed.
  • Per-origin state seeded into the renderer
    Review any commit-time propagation of origin-scoped state (permissions, storage access, content settings) and confirm it is computed against the destination origin, not whatever document currently occupies the frame.
  • Optional pre-populated caches
    Trace initial_permission_statuses and similar std::optional “initial” fields in mojo commit structs to ensure a missing value cleanly forces a fresh browser query rather than exposing stale or cross-origin data.
From 46c64c0bd34333086b7afb972009de05171ef489 Mon Sep 17 00:00:00 2001
From: Thomas Nguyen <tungnh@chromium.org>
Date: Thu, 17 Sep 2026 01:02:26 -0700
Subject: [PATCH] Reland "[permissions] Remove cached permission status computation"

This is a reland of commit ba2f55f04eb4a9ce2ed77c127445054e4e8d7d13.

The cached permission status computation was wrong, because it was
computed for the source document of the navigation. Let's remove it for
now.

Fixed: 515493668
Fixed: 514321719
Bug: 534615429
Change-Id: Ib94f0d78073836e3954daf2fb817e41085edc05e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8411465
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Auto-Submit: Thomas Nguyen <tungnh@chromium.org>
Commit-Queue: Thomas Nguyen <tungnh@chromium.org>
Reviewed-by: Antonio Sartori <antoniosartori@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699587}
---
diff --git a/content/browser/renderer_host/navigation_controller_impl.cc b/content/browser/renderer_host/navigation_controller_impl.cc
index e588886..1f688907 100644
--- a/content/browser/renderer_host/navigation_controller_impl.cc
+++ b/content/browser/renderer_host/navigation_controller_impl.cc
@@ -4781,7 +4781,7 @@
           /*lcpp_hint=*/nullptr, blink::CreateDefaultRendererContentSettings(),
           /*visited_link_salt=*/std::nullopt,
           /*local_surface_id=*/std::nullopt,
-          node->current_frame_host()->GetCachedPermissionStatuses(),
+          /*initial_permission_statuses=*/std::nullopt,
           /*should_skip_screentshot=*/false,
           /*force_new_document_sequence_number=*/false,
           /*navigation_metrics_token=*/base::UnguessableToken::Create(),
@@ -4979,8 +4979,6 @@
           frame_tree_node->AncestorOrSelfHasCSPEE(),
           soft_navigation_heuristics_task_id);
   commit_params->post_content_type = post_content_type;
-  commit_params->initial_permission_statuses =
-      frame_tree_node->current_frame_host()->GetCachedPermissionStatuses();
 
   if (common_params->url.IsAboutSrcdoc()) {
     // TODO(wjmaclean): initialize this in NavigationRequest's constructor
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 185d79b..e314ca8 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -1406,7 +1406,7 @@
           /*lcpp_hint=*/nullptr, blink::CreateDefaultRendererContentSettings(),
           /*visited_link_salt=*/std::nullopt,
           /*local_surface_id=*/std::nullopt,
-          frame_tree_node->current_frame_host()->GetCachedPermissionStatuses(),
+          /*initial_permission_statuses=*/std::nullopt,
           /*should_skip_screenshot=*/false,
           /*force_new_document_sequence_number=*/false,
           /*navigation_metrics_token=*/base::UnguessableToken::Create(),
@@ -1580,7 +1580,7 @@
           /*lcpp_hint=*/nullptr, blink::CreateDefaultRendererContentSettings(),
           /*visited_link_salt=*/std::nullopt,
           /*local_surface_id=*/std::nullopt,
-          render_frame_host->GetCachedPermissionStatuses(),
+          /*initial_permission_statuses=*/std::nullopt,
           /*should_skip_screenshot=*/false,
           /*force_new_document_sequence_number=*/false,
           /*navigation_metrics_token=*/base::UnguessableToken::Create(),
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 6e495d4..c2c0b70 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -20785,51 +20785,6 @@
       out);
 }
 
-std::optional<base::flat_map<blink::mojom::PermissionName,
-                             blink::mojom::PermissionStatus>>
-RenderFrameHostImpl::GetCachedPermissionStatuses() {
-  using blink::PermissionType;
-  using blink::mojom::PermissionName;
-  static constexpr auto kPermissions =
-      std::to_array<std::pair<PermissionName, PermissionType>>(
-          {{PermissionName::VIDEO_CAPTURE, PermissionType::VIDEO_CAPTURE},
-           {PermissionName::AUDIO_CAPTURE, PermissionType::AUDIO_CAPTURE},
-#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
-           // `WEB_APP_INSTALLATION` is only registered for desktop platforms
-           // via `WebsiteSettingsRegistry::DESKTOP`.
-           {PermissionName::WEB_APP_INSTALLATION,
-            PermissionType::WEB_APP_INSTALLATION},
-#endif
-           {PermissionName::GEOLOCATION, PermissionType::GEOLOCATION}});
-
-  base::flat_map<PermissionName, PermissionStatus> permission_map;
-  for (const auto& permission : kPermissions) {
-    PermissionStatus status = GetCombinedPermissionStatus(permission.second);
-    // Default value is ASK, we don't need add the permission status in this
-    // case.
-    if (status != PermissionStatus::ASK) {
-      permission_map.emplace(permission.first, status);
-    }
-  }
-
-  return permission_map;
-}
-
-blink::mojom::PermissionStatus RenderFrameHostImpl::GetCombinedPermissionStatus(
-    blink::PermissionType permission_type) {
-  auto descriptor = content::PermissionDescriptorUtil::
-      CreatePermissionDescriptorForPermissionType(permission_type);
-  if (PermissionUtil::IsDevicePermission(descriptor)) {
-    return GetBrowserContext()
-        ->GetPermissionController()
-        ->GetCombinedPermissionAndDeviceStatus(descriptor, this);
-  }
-  return GetBrowserContext()
-      ->GetPermissionController()
-      ->GetPermissionResultForCurrentDocument(descriptor, this)
-      .status;
-}
-
 media::PictureInPictureEventsInfo::AutoPipReasonCallback
 RenderFrameHostImpl::CreateAutoPipReasonCallback() {
   return base::BindRepeating(
diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h
index 18b3066..59f4091 100644
--- a/content/browser/renderer_host/render_frame_host_impl.h
+++ b/content/browser/renderer_host/render_frame_host_impl.h
@@ -334,10 +334,6 @@
     void(RenderFrameHostImpl*, ax::mojom::Event, int)>
     AccessibilityCallbackForTesting;
 
-using CachedPermissionMap =
-    std::optional<base::flat_map<blink::mojom::PermissionName,
-                                 blink::mojom::PermissionStatus>>;
-
 class CONTENT_EXPORT RenderFrameHostImpl
     : public RenderFrameHost,
       public base::SupportsUserData,
@@ -3144,15 +3140,8 @@
   // this instance's AXNodeIdDelegate implementation.
   size_t GetAxUniqueIdCountForTesting() const { return ax_unique_ids_.size(); }
 
-  // Query necessary permission statues in order to propagate to the renderer.
-  // Right now, we're only caring about permissions for Geolocation, Camera, and
-  // Microphone. The permission statuses already take into account the device's
-  // status.
-  CachedPermissionMap GetCachedPermissionStatuses();
-
   // Returns the Storage Access API status for this instance.
   net::StorageAccessApiStatus GetStorageAccessApiStatus();
-
   // Allows tests to disable the unload event timer to simulate bugs that
   // happen before it fires (to avoid flakiness).
   void DisableUnloadTimerForTesting();
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.