CVE-2026-106244
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
RenderFrameHostchrome/browser/permissions/chrome_permissions_client.h |
modified |
Files Changed
chrome/browser/permissions/chrome_permissions_client.hchrome/browser/permissions/permission_delegation_browsertest.ccchrome/browser/permissions/chrome_permissions_client.cc
Patch
From 8ab54afc0c1cfb1f217a52119e3af2e2231289f0 Mon Sep 17 00:00:00 2001 From: Antonio Sartori <[email protected]> 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 <[email protected]> Commit-Queue: Antonio Sartori <[email protected]> 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(
Regression Test / PoC
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(
Original Bug Report
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.ccchrome/browser/ui/webui/new_tab_page/untrusted_source.ccchrome/browser/resources/new_tab_page/app.htmlcomponents/permissions/permission_util.ccchrome/browser/ui/views/permissions/permission_prompt_base_view.ccchrome/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
- UI Attribution Spoofing: When the permission prompt for
getUserMediais shown,PermissionPromptBaseView::GetUrlIdentityuses the canonicalized origin, attributing the request to the privilegedchrome://new-tab-pageinstead of the untrusted iframe. (Similarly,getDisplayMediaspoofing occurs becauseDisplayMediaAccessHandler::GetApplicationTitleexplicitly reads the main frame’s origin directly). - Incorrect Grant Persistence: If a user grants the permission,
ContentSettingPermissionContextBase::UpdateContentSettingstores the grant in theHostContentSettingsMapkeyed bychrome://new-tab-page/for both the primary and secondary patterns. Any subsequent compromise of thechrome-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.
- 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). - Execute
navigator.mediaDevices.getUserMedia({video:true})from the iframe context. - Observe the permission prompt. The bubble title will display
chrome://new-tab-page(the privileged WebUI origin) instead of thechrome-untrusted://origin. - Accept the prompt.
- Close the tab, navigate to a new NTP, and execute the
getUserMediacall from the untrusted frame again. - 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.