CVE-2026-7961
Overview
Files Changed
chrome/browser/permissions/chrome_permissions_client.cc
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.