Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Permissions
DescriptionIncorrect authorization in Permissions
ComponentPermissions
Sub-componentPermissions
Bug ClassAuthorization Error
Tracker502475175
Fix commit8ab54afc0c1c (chromium/src) +5/-52
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
RenderFrameHost
chrome/browser/permissions/chrome_permissions_client.h
modified

Files Changed

  • chrome/browser/permissions/chrome_permissions_client.h
  • chrome/browser/permissions/permission_delegation_browsertest.cc
  • chrome/browser/permissions/chrome_permissions_client.cc
From 8ab54afc0c1cfb1f217a52119e3af2e2231289f0 Mon Sep 17 00:00:00 2001
From: Antonio Sartori <antoniosartori@chromium.org>
Date: Thu, 08 Oct 2026 07:38:12 -0700
Subject: [PATCH] [permissions] Simplify PermissionClient::GetEmbeddingOriginOverride

It looks like there is redundant code in this function.

PermissionClient::GetEmbeddingOriginOverride() returns a std::optional
and its callers default to the main frame's origin if it returns
std::nullopt.

The cases in which GetEmbeddingOriginOverride() returns the outer
origin for an embedded <webview> seem redunant, since permission
requests coming from <webview> anyway trigger a permission request in
the embedder (assuming the embedder allows it).

Bug: 502475175
Change-Id: I57111b1ba337404e7cec27f7d97d584348960a0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7789705
Reviewed-by: Andy Paicu <andypaicu@chromium.org>
Commit-Queue: Antonio Sartori <antoniosartori@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1714491}
---
diff --git a/chrome/browser/permissions/chrome_permissions_client.cc b/chrome/browser/permissions/chrome_permissions_client.cc
index 6e3cc5c..dc0b4bd 100644
--- a/chrome/browser/permissions/chrome_permissions_client.cc
+++ b/chrome/browser/permissions/chrome_permissions_client.cc
@@ -831,36 +831,13 @@
   return std::nullopt;
 }
 
+#if BUILDFLAG(ENABLE_EXTENSIONS) && !BUILDFLAG(IS_ANDROID)
 std::optional<GURL> ChromePermissionsClient::GetEmbeddingOriginOverride(
     const GURL& requesting_origin,
     content::RenderFrameHost* render_frame_host) {
   content::WebContents* web_contents =
       content::WebContents::FromRenderFrameHost(render_frame_host);
-  content::WebContents* embedder_web_contents =
-      web_contents->GetOutermostWebContents();
-  url::Origin embedder =
-      embedder_web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin();
 
-  // New Tab Page:
-  // Use the WebContents origin as the embedding origin when the requesting
-  // origin is the NTP (chrome://new-tab-page).
-  // Note that the embedding origin is later transformed to the DSE origin via
-  // `GetCanonicalOriginOverride()`.
-  if (embedder == GetNewTabOrigin() || embedder == GetNtpOrigin()) {
-    return embedder.GetURL();
-  }
-
-  if (embedder == GetContextualTasksOrigin() ||
-      embedder == GetOmniboxPopupOrigin() ||
-      embedder == GetOmniboxEverywhereOrigin()) {
-    // Omnibox Popup, Omnibox Everywhere, and Contextual Tasks:
-    // Use the WebContents origin as the embedding origin.
-    // Note that the embedding origin is later transformed to the DSE origin via
-    // `GetCanonicalOriginOverride()`.
-    return embedder.GetURL();
-  }
-
-#if BUILDFLAG(ENABLE_EXTENSIONS) && !BUILDFLAG(IS_ANDROID)
   // When a MIME handler extension is rendered as an OOPIF, key the
   // embedding origin to the extension so prompts and content settings
   // are attributed to the extension that produced the streamed content,
@@ -886,10 +863,10 @@
       }
     }
   }
-#endif  // BUILDFLAG(ENABLE_EXTENSIONS) && !BUILDFLAG(IS_ANDROID)
 
   return std::nullopt;
 }
+#endif  // BUILDFLAG(ENABLE_EXTENSIONS) && !BUILDFLAG(IS_ANDROID)
 
 std::optional<content::PermissionResult>
 ChromePermissionsClient::GetPermissionResultOverride(
diff --git a/chrome/browser/permissions/chrome_permissions_client.h b/chrome/browser/permissions/chrome_permissions_client.h
index 8f97e8d..0280e1e 100644
--- a/chrome/browser/permissions/chrome_permissions_client.h
+++ b/chrome/browser/permissions/chrome_permissions_client.h
@@ -18,6 +18,7 @@
 #include "components/permissions/permission_uma_constants.h"
 #include "components/permissions/permissions_client.h"
 #include "components/permissions/resolvers/permission_prompt_options.h"
+#include "extensions/buildflags/buildflags.h"
 
 namespace content {
 class RenderFrameHost;
@@ -116,9 +117,11 @@
   std::optional<GURL> GetCanonicalOriginOverride(
       const GURL& requesting_origin,
       const GURL& embedding_origin) override;
+#if BUILDFLAG(ENABLE_EXTENSIONS) && !BUILDFLAG(IS_ANDROID)
   std::optional<GURL> GetEmbeddingOriginOverride(
       const GURL& requesting_origin,
       content::RenderFrameHost* render_frame_host) override;
+#endif
   std::optional<content::PermissionResult> GetPermissionResultOverride(
       content::RenderFrameHost* render_frame_host,
       const GURL& requesting_origin,
diff --git a/chrome/browser/permissions/permission_delegation_browsertest.cc b/chrome/browser/permissions/permission_delegation_browsertest.cc
index eb70d7a..c5ffbcda 100644
--- a/chrome/browser/permissions/permission_delegation_browsertest.cc
+++ b/chrome/browser/permissions/permission_delegation_browsertest.cc
@@ -176,33 +176,6 @@
 };
 
 IN_PROC_BROWSER_TEST_F(ContextualTasksPermissionDelegationBrowserTest,
-                       GetEmbeddingOriginOverride) {
-  GURL contextual_tasks_url(chrome::kChromeUIContextualTasksURL);
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), contextual_tasks_url));
-  content::WebContents* outer_web_contents = GetWebContents();
-
-  // Wait for the WebUI to initialize and spawn the inner <webview> guest
-  // WebContents.
-  ASSERT_TRUE(base::test::RunUntil(
-      [&] { return !outer_web_contents->GetInnerWebContents().empty(); }));
-
-  content::WebContents* inner_web_contents =
-      outer_web_contents->GetInnerWebContents()[0];
-  ASSERT_TRUE(inner_web_contents);
-  EXPECT_EQ(outer_web_contents, inner_web_contents->GetOuterWebContents());
-
-  // Verify that GetEmbeddingOriginOverride correctly traverses from the inner
-  // <webview> guest WebContents up to the outer WebUI WebContents.
-  GURL requesting_origin("https://example.com");
-  std::optional<GURL> override_origin =
-      ChromePermissionsClient::GetInstance()->GetEmbeddingOriginOverride(
-          requesting_origin, inner_web_contents->GetPrimaryMainFrame());
-
-  ASSERT_TRUE(override_origin.has_value());
-  EXPECT_EQ(contextual_tasks_url, *override_origin);
-}
-
-IN_PROC_BROWSER_TEST_F(ContextualTasksPermissionDelegationBrowserTest,
                        RequestPermissionInWebView) {
   GURL default_ai_url =
       contextual_tasks::ContextualTasksUiServiceFactory::GetForBrowserContext(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/permissions/permission_delegation_browsertest.cc b/chrome/browser/permissions/permission_delegation_browsertest.cc
index eb70d7a..c5ffbcda 100644
--- a/chrome/browser/permissions/permission_delegation_browsertest.cc
+++ b/chrome/browser/permissions/permission_delegation_browsertest.cc
@@ -176,33 +176,6 @@
 };
 
 IN_PROC_BROWSER_TEST_F(ContextualTasksPermissionDelegationBrowserTest,
-                       GetEmbeddingOriginOverride) {
-  GURL contextual_tasks_url(chrome::kChromeUIContextualTasksURL);
-  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), contextual_tasks_url));
-  content::WebContents* outer_web_contents = GetWebContents();
-
-  // Wait for the WebUI to initialize and spawn the inner <webview> guest
-  // WebContents.
-  ASSERT_TRUE(base::test::RunUntil(
-      [&] { return !outer_web_contents->GetInnerWebContents().empty(); }));
-
-  content::WebContents* inner_web_contents =
-      outer_web_contents->GetInnerWebContents()[0];
-  ASSERT_TRUE(inner_web_contents);
-  EXPECT_EQ(outer_web_contents, inner_web_contents->GetOuterWebContents());
-
-  // Verify that GetEmbeddingOriginOverride correctly traverses from the inner
-  // <webview> guest WebContents up to the outer WebUI WebContents.
-  GURL requesting_origin("https://example.com");
-  std::optional<GURL> override_origin =
-      ChromePermissionsClient::GetInstance()->GetEmbeddingOriginOverride(
-          requesting_origin, inner_web_contents->GetPrimaryMainFrame());
-
-  ASSERT_TRUE(override_origin.has_value());
-  EXPECT_EQ(contextual_tasks_url, *override_origin);
-}
-
-IN_PROC_BROWSER_TEST_F(ContextualTasksPermissionDelegationBrowserTest,
                        RequestPermissionInWebView) {
   GURL default_ai_url =
       contextual_tasks::ContextualTasksUiServiceFactory::GetForBrowserContext(
Loading diff…

Original Bug Report

reported by vm...@google.com

Privilege Escalation via NTP chrome-untrusted:// Iframe Permission Canonicalization

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 without the Chrome Security team.

Overview: A logic error in permission canonicalization causes hardware permission requests from the untrusted OneGoogleBar iframe to be incorrectly attributed to the privileged chrome://new-tab-page origin. This allows an attacker who has compromised the untrusted iframe to spoof permission prompts and gain silent, persistent camera or display-capture access across sessions.

Affected files:

  • chrome/browser/permissions/chrome_permissions_client.cc
  • chrome/browser/ui/webui/new_tab_page/untrusted_source.cc
  • chrome/browser/resources/new_tab_page/app.html
  • components/permissions/permission_util.cc
  • chrome/browser/ui/views/permissions/permission_prompt_base_view.cc
  • chrome/browser/media/webrtc/display_media_access_handler.cc

Estimated timestamp from git blame: 2026-03-30

Description

The New Tab Page (NTP) top frame (chrome://new-tab-page/) embeds the OneGoogleBar (OGB) within an iframe sourced from chrome-untrusted://new-tab-page/. This untrusted iframe is explicitly granted camera and display-capture capabilities via the allow attribute in the HTML (chrome/browser/resources/new_tab_page/app.html:18). The chrome-untrusted:// scheme is designed to isolate external, network-fetched content from the privileged WebUI.

However, a logic error in ChromePermissionsClient causes permission requests from this untrusted iframe to bypass the intended isolation. When an iframe requests a delegated permission (like camera), the permission system attempts to override the embedding origin for specific Chrome UI pages.

In ChromePermissionsClient::GetEmbeddingOriginOverride and ChromePermissionsClient::GetCanonicalOriginOverride, the code checks if the embedding_origin matches chrome::kChromeUINewTabURL (chrome://newtab/). But for the modern WebUI NTP, the actual committed origin of the main frame is chrome://new-tab-page/ (defined as kChromeUINewTabPageURL). Because chrome://new-tab-page/ != chrome://newtab/, the overrides fail and return std::nullopt.

As a result, PermissionUtil::GetCanonicalOrigin falls back to its default behavior for PermissionDelegationMode::kDelegated permissions, which is to return the embedding_origin. Since the main frame’s actual committed origin is chrome://new-tab-page/, this highly privileged WebUI origin becomes the canonical origin for the request.

Consequences

  1. UI Attribution Spoofing: When the permission prompt for getUserMedia is shown, PermissionPromptBaseView::GetUrlIdentity uses the canonicalized origin, attributing the request to the privileged chrome://new-tab-page instead of the untrusted iframe. (Similarly, getDisplayMedia spoofing occurs because DisplayMediaAccessHandler::GetApplicationTitle explicitly reads the main frame’s origin directly).
  2. Incorrect Grant Persistence: If a user grants the permission, ContentSettingPermissionContextBase::UpdateContentSetting stores the grant in the HostContentSettingsMap keyed by chrome://new-tab-page/ for both the primary and secondary patterns. Any subsequent compromise of the chrome-untrusted://new-tab-page/ frame allows the attacker to access the camera or screen capture silently, riding the previously stored grant for the privileged origin.

Potential Reproduction Steps

Note: These steps are based on static analysis; our tooling cannot execute a full proof of concept.

  1. Gain code execution within the chrome-untrusted://new-tab-page/ iframe (e.g., via XSS on the network-fetched OneGoogleBar payload, or by simulating a compromised renderer).
  2. Execute navigator.mediaDevices.getUserMedia({video:true}) from the iframe context.
  3. Observe the permission prompt. The bubble title will display chrome://new-tab-page (the privileged WebUI origin) instead of the chrome-untrusted:// origin.
  4. Accept the prompt.
  5. Close the tab, navigate to a new NTP, and execute the getUserMedia call from the untrusted frame again.
  6. Observe that camera access is granted silently without a prompt, as the grant was persisted under the top-level chrome://new-tab-page/ origin.

Suggested Fix

Update ChromePermissionsClient::GetEmbeddingOriginOverride and ChromePermissionsClient::GetCanonicalOriginOverride to account for chrome::kChromeUINewTabPageURL alongside chrome::kChromeUINewTabURL.

For example, CanBypassEmbeddingOriginCheck in the same file correctly handles this by checking both:

if (embedding_origin == GURL(chrome::kChromeUINewTabURL).DeprecatedGetOriginAsURL() ||
    embedding_origin == GURL(chrome::kChromeUINewTabPageURL).DeprecatedGetOriginAsURL()) {
  return true;
}

Applying a similar check in the override functions ensures that the NTP origins are consistently canonicalized to the Default Search Engine origin, preventing the untrusted iframe from piggybacking on the WebUI’s privileges.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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