CVE-2026-93383
Overview
Background
- `initial_permission_statuses`
- A field of the
CommitNavigationParamsmojo 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_mapofPermissionNametoPermissionStatusfor 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
GetCombinedPermissionAndDeviceStatusfor device permissions) against thePermissionControllerfor the current document. - Source vs. destination document
- In a navigation the “source” is the
RenderFrameHostImplcurrently 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.
std::nullopt) and deletes the flawed computation.Attack Path
- 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. - 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.
- Wrong document queried
During commit-param construction,
GetCachedPermissionStatuses()is evaluated againstcurrent_frame_host(), which still hosts origin A, so the resultingflat_mapreflects A’s grants and is stored ininitial_permission_statuses. - 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).
- 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
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
| Function | Change | Notes |
|---|---|---|
GetCachedPermissionStatusescontent/browser/renderer_host/render_frame_host_impl.cc |
modified | |
forcontent/browser/renderer_host/render_frame_host_impl.cc |
modified | |
ifcontent/browser/renderer_host/render_frame_host_impl.cc |
modified | |
CreateAutoPipReasonCallbackcontent/browser/renderer_host/render_frame_host_impl.cc |
modified | |
CONTENT_EXPORTcontent/browser/renderer_host/render_frame_host_impl.h |
modified |
Files Changed
content/browser/renderer_host/render_frame_host_impl.hcontent/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/navigation_request.cccontent/browser/renderer_host/navigation_controller_impl.cc
Audit Directions
- Wrong-frame data capture at navigation timeAudit other values snapshotted from
current_frame_host()/ the sourceRenderFrameHostImplwhile buildingCommitNavigationParams, since those may reflect the previous document rather than the one being committed. - Per-origin state seeded into the rendererReview 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 cachesTrace
initial_permission_statusesand similarstd::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.
Patch
From 46c64c0bd34333086b7afb972009de05171ef489 Mon Sep 17 00:00:00 2001 From: Thomas Nguyen <[email protected]> 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 <[email protected]> Auto-Submit: Thomas Nguyen <[email protected]> Commit-Queue: Thomas Nguyen <[email protected]> Reviewed-by: Antonio Sartori <[email protected]> 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();