CVE-2026-7961
Overview
Background
- Canonical origin override / DSE transform
- Chrome rewrites certain WebUI origins (NTP, omnibox popup, contextual tasks) to the Default Search Engine (DSE) origin so permissions are stored under and shared with the DSE;
GetCanonicalOriginOverrideperforms this transform. - Embedding-origin check bypass
- a companion path that returns
trueto skip the normal embedding-origin permission check for those same trusted WebUI surfaces, on the understanding they will be transformed to the DSE origin. - `GURL::host()` vs full origin
host()compares only the hostname, ignoring scheme and port, whereasDeprecatedGetOriginAsURL()compares the full scheme/host/port origin.- Regressing CL
- per the commit,
crrev.com/c/7515301replaced full-origin checks with host-only checks inChromePermissionsClient, which this patch reverts to full-origin comparisons.
Root Cause Analysis
Across several conditions in chrome_permissions_client.cc (including GetCanonicalOriginOverride, named in the commit subject; the surrounding @@ headers do not name the enclosing functions), the code decided whether an origin was the trusted NTP / New Tab Page / Omnibox Popup / Contextual Tasks surface by comparing only embedding_origin.host() / requesting_origin.host() against the WebUI host constants. Host-only matching ignores scheme and port, so any origin that merely shares one of those hostnames would satisfy the check even if it is not the genuine chrome:// WebUI origin. Those checks gate security-sensitive behavior: bypassing the embedding-origin permission check and transforming the requesting/embedding origin to the DSE origin — i.e. causing permissions to be attributed to or shared with the DSE.
The fix tightens each comparison to a full-origin equality test (embedding_origin == GURL(chrome::kChromeUINewTabURL).DeprecatedGetOriginAsURL(), and likewise for the New-Tab-Page, Omnibox-Popup, and Contextual-Tasks URLs), so only the exact chrome:// origins — correct scheme, host, and port — match. This restores the full-origin invariant the regressing CL had weakened; the branches still return the same values, but now only for genuinely trusted origins.
chrome:// origins qualify for the embedding-check bypass and DSE transform.Attack Path
- Reach the permission path
A request flows through
ChromePermissionsClientwhere embedding/requesting origins are classified against the NTP/omnibox WebUI host constants. - Present a host-matching but non-canonical origin An origin that matches one of the WebUI hosts but differs in scheme or port (what host-only comparison fails to distinguish) is supplied as the embedding or requesting origin.
- Spoof the trusted surface Pre-patch, the host-only check accepts it as the trusted NTP/omnibox origin, so the embedding-origin permission check is bypassed and/or the origin is transformed to the DSE origin.
- Permission misattribution The permission decision is made as if for the trusted surface, letting permissions be bypassed or stored/shared under the DSE origin for an origin that is not the genuine WebUI (the concrete exploitable origin is inferred; the diff establishes the host-only matching weakness and the DSE/bypass consequences).
Impact Assessment
Files Changed
chrome/browser/permissions/chrome_permissions_client.cc
Audit Directions
- Host-only origin comparisonsAudit for
origin.host() == <constant>used in security decisions; these ignore scheme/port and should be full-origin equality (DeprecatedGetOriginAsURL()/url::Origincomparison), especially forchrome://WebUI identification. - Regressions from check simplificationReview changes that “simplify” origin/URL validation (e.g. replacing origin checks with host checks) for weakened trust boundaries, and diff against the pre-simplification logic.
- DSE/permission transform gatesExamine every path that transforms an origin to the DSE origin or bypasses an embedding-origin check and confirm the trusted-surface predicate cannot be satisfied by a spoofed same-host origin.
Patch
From 6aa2c02f8a3779a4081348ccb3b97c1a936ab3fc Mon Sep 17 00:00:00 2001
From: Antonio Sartori <antoniosartori@chromium.org>
Date: Mon, 30 Mar 2026 04:54:27 -0700
Subject: [PATCH] [permissions] Fix origin matching in GetCanonicalOriginOverride
This CL fixes a regression introduced by https://crrev.com/c/7515301,
removed some full origin checks and replaced them with host-only
checks in ChromePermissionClient.
Bug: 497008295
Change-Id: I2ef8ea2d967ebdb42928dffc86b8f68a8ee9219d
Fixed: 497008295
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7707943
Auto-Submit: Antonio Sartori <antoniosartori@chromium.org>
Reviewed-by: Balazs Engedy <engedy@chromium.org>
Commit-Queue: Balazs Engedy <engedy@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1607042}
---
diff --git a/chrome/browser/permissions/chrome_permissions_client.cc b/chrome/browser/permissions/chrome_permissions_client.cc
index f87b0b5e..9d5465e 100644
--- a/chrome/browser/permissions/chrome_permissions_client.cc
+++ b/chrome/browser/permissions/chrome_permissions_client.cc
@@ -659,16 +659,20 @@
// New Tab Page:
// Bypass embedding origin check as the `requesting_origin` will later be
// transformed to the DSE origin in `GetCanonicalOriginOverride()`.
- if (embedding_origin.host() == chrome::kChromeUINewTabHost ||
- embedding_origin.host() == chrome::kChromeUINewTabPageHost) {
+ if (embedding_origin ==
+ GURL(chrome::kChromeUINewTabURL).DeprecatedGetOriginAsURL() ||
+ embedding_origin ==
+ GURL(chrome::kChromeUINewTabPageURL).DeprecatedGetOriginAsURL()) {
return true;
}
// Omnibox Popup and Contextual Tasks:
// Bypass embedding origin check as the `requesting_origin` will later be
// transformed to the DSE origin in `GetCanonicalOriginOverride()`.
- if (embedding_origin.host() == chrome::kChromeUIOmniboxPopupHost ||
- embedding_origin.host() == chrome::kChromeUIContextualTasksHost) {
+ if (embedding_origin ==
+ GURL(chrome::kChromeUIOmniboxPopupURL).DeprecatedGetOriginAsURL() ||
+ embedding_origin == GURL(chrome::kChromeUIContextualTasksURL)
+ .DeprecatedGetOriginAsURL()) {
return true;
}
@@ -681,8 +685,10 @@
// New Tab Page:
// Transform chrome:// origins to the DSE origin so that permissions are
// stored under and shared with the DSE.
- if (embedding_origin.host() == chrome::kChromeUINewTabHost) {
- if (requesting_origin.host() == chrome::kChromeUINewTabPageHost) {
+ if (embedding_origin ==
+ GURL(chrome::kChromeUINewTabURL).DeprecatedGetOriginAsURL()) {
+ if (requesting_origin ==
+ GURL(chrome::kChromeUINewTabPageURL).DeprecatedGetOriginAsURL()) {
return GURL(UIThreadSearchTermsData().GoogleBaseURLValue())
.DeprecatedGetOriginAsURL();
}
@@ -693,8 +699,10 @@
// Transform chrome:// origins to the DSE origin so that permissions are
// stored under and shared with the DSE.
if (requesting_origin == embedding_origin &&
- (requesting_origin.host() == chrome::kChromeUIOmniboxPopupHost ||
- requesting_origin.host() == chrome::kChromeUIContextualTasksHost)) {
+ (requesting_origin ==
+ GURL(chrome::kChromeUIOmniboxPopupURL).DeprecatedGetOriginAsURL() ||
+ requesting_origin == GURL(chrome::kChromeUIContextualTasksURL)
+ .DeprecatedGetOriginAsURL())) {
return GURL(UIThreadSearchTermsData().GoogleBaseURLValue())
.DeprecatedGetOriginAsURL();
}
@@ -723,8 +731,10 @@
// 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 (requesting_origin.host() == chrome::kChromeUINewTabPageHost &&
- embedding_origin.host() == chrome::kChromeUINewTabHost) {
+ if (requesting_origin ==
+ GURL(chrome::kChromeUINewTabPageURL).DeprecatedGetOriginAsURL() &&
+ embedding_origin ==
+ GURL(chrome::kChromeUINewTabURL).DeprecatedGetOriginAsURL()) {
return embedding_origin;
}
@@ -733,8 +743,10 @@
// Note that the embedding origin is later transformed to the DSE origin via
// `GetCanonicalOriginOverride()`.
if (requesting_origin == embedding_origin &&
- (requesting_origin.host() == chrome::kChromeUIOmniboxPopupHost ||
- requesting_origin.host() == chrome::kChromeUIContextualTasksHost)) {
+ (requesting_origin ==
+ GURL(chrome::kChromeUIOmniboxPopupURL).DeprecatedGetOriginAsURL() ||
+ requesting_origin == GURL(chrome::kChromeUIContextualTasksURL)
+ .DeprecatedGetOriginAsURL())) {
return embedding_origin;
}
Original Bug Report
Permission Spoofing/Bypass via Missing Scheme Check in ChromePermissionsClient
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A logic flaw in ChromePermissionsClient’s origin override logic fails to verify the chrome:// scheme for specific WebUI hosts like contextual-tasks and omnibox-popup.top-chrome. An attacker who can direct a user to an HTTPS version of these domains (e.g., via DNS spoofing or local network registration) will have their origin rewritten to the Google Default Search Engine (DSE) origin. This allows the attacker to silently inherit the user’s previously granted permissions for Google services, such as Geolocation or Camera.
Affected files:
chrome/browser/permissions/chrome_permissions_client.cc
Estimated timestamp from git blame: 2026-01-29
Description
There is a potential vulnerability in ChromePermissionsClient::GetCanonicalOriginOverride and CanBypassEmbeddingOriginCheck (chrome/browser/permissions/chrome_permissions_client.cc) where the origin’s scheme is not verified when checking for specific Chrome WebUI hosts.
At lines 695-697 in GetCanonicalOriginOverride, the code checks if the requesting origin matches specific WebUI hosts to determine if it should be rewritten to the user’s Default Search Engine (DSE) base URL (typically https://www.google.com/):
if (requesting_origin == embedding_origin &&
(requesting_origin.host() == chrome::kChromeUIOmniboxPopupHost ||
requesting_origin.host() == chrome::kChromeUIContextualTasksHost)) {
return GURL(UIThreadSearchTermsData().GoogleBaseURLValue())
.DeprecatedGetOriginAsURL();
}
Crucially, this check only verifies the .host() property ("contextual-tasks" or "omnibox-popup.top-chrome") and lacks a scheme check (e.g., requesting_origin.SchemeIs(content::kChromeUIScheme)).
If an attacker can control DNS resolution for these single-label domains on a local or enterprise network and serve a trusted HTTPS certificate, navigating the user to https://contextual-tasks/ will satisfy this condition. Because high-privilege permissions like Geolocation, Camera, and Microphone use WebsiteSettingsInfo::TOP_ORIGIN_ONLY_SCOPE, the HostContentSettingsMap ignores the embedding origin during lookup. The attacker’s page will silently inherit any permissions the user has already granted to Google, bypassing all permission prompts.
Potential Attack Steps
Note: These are suggested steps based on static analysis; our tooling agent does not currently have the capability to run a live proof-of-concept.
- DNS Control: An attacker on a local network (e.g., via rogue DHCP, DNS poisoning, or mDNS) configures the hostname
contextual-tasksto resolve to an attacker-controlled IP address. - HTTPS Setup: The attacker sets up an HTTPS server and provisions a certificate for
contextual-tasks. This could be trusted via an enterprise root CA, or the attacker might rely on the user clicking through an SSL warning. - Victim Navigation: The attacker lures the victim to navigate to
https://contextual-tasks/in Chrome. - Permission Request: The attacker’s webpage executes JavaScript to request a sensitive permission, such as
navigator.geolocation.getCurrentPosition(...). - Origin Rewrite: The permission request enters the browser process.
PermissionManager::RequestPermissionscallsPermissionUtil::GetCanonicalOrigin, which triggersChromePermissionsClient::GetCanonicalOriginOverride. The host matches"contextual-tasks", so the origin is rewritten to the Google DSE origin (e.g.,https://www.google.com/). - Silent Grant:
PermissionContextBasechecks existing grants using the rewritten origin. Because permissions like Geolocation useTOP_ORIGIN_ONLY_SCOPE, the browser finds the user’s existing grant for Google and silently allows the request, returning sensitive data to the attacker’s page without a prompt.
Suggested Fix
Update ChromePermissionsClient::GetCanonicalOriginOverride, ChromePermissionsClient::CanBypassEmbeddingOriginCheck, and ChromePermissionsClient::GetEmbeddingOriginOverride to explicitly verify that both requesting_origin and embedding_origin are using the chrome:// scheme before performing host comparisons.
For example, in GetCanonicalOriginOverride:
if (requesting_origin.SchemeIs(content::kChromeUIScheme) &&
requesting_origin == embedding_origin &&
(requesting_origin.host() == chrome::kChromeUIOmniboxPopupHost ||
requesting_origin.host() == chrome::kChromeUIContextualTasksHost)) {
return GURL(UIThreadSearchTermsData().GoogleBaseURLValue())
.DeprecatedGetOriginAsURL();
}
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.