Medium watch CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Permissions
DescriptionInsufficient validation of untrusted input in Permissions
ComponentPermissions
Sub-componentPermissions
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497008295
Fix commit6aa2c02f8a37 (chromium/src) +24/-12
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-27 (per vendor advisory)
Fix landed2026-03-30 (public git)
Disclosed2026-05-05
Patch gap36 days — fix public in source before the advisory

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; GetCanonicalOriginOverride performs this transform.
Embedding-origin check bypass
a companion path that returns true to 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, whereas DeprecatedGetOriginAsURL() compares the full scheme/host/port origin.
Regressing CL
per the commit, crrev.com/c/7515301 replaced full-origin checks with host-only checks in ChromePermissionsClient, 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.

Key insight
The regressing change validated these trusted WebUI surfaces by hostname only, allowing a non-matching-scheme/port origin sharing the host to be treated as the trusted NTP/omnibox surface; the fix compares the full origin so only the exact chrome:// origins qualify for the embedding-check bypass and DSE transform.

Attack Path

  1. Reach the permission path A request flows through ChromePermissionsClient where embedding/requesting origins are classified against the NTP/omnibox WebUI host constants.
  2. 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.
  3. 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.
  4. 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

An attacker able to present an origin that shares a trusted WebUI hostname (but not its full origin) could have the embedding-origin permission check bypassed and its permissions transformed to the DSE origin, i.e. inappropriately gaining or sharing permissions associated with trusted New Tab Page / omnibox surfaces. The precondition is reaching these permission-classification paths with such an origin; this is a regression fix for a previously tightened check. The vendor advisory rates this medium (insufficient validation of untrusted input in Permissions) and does not report exploitation in the wild.

Files Changed

  • chrome/browser/permissions/chrome_permissions_client.cc

Audit Directions

  • Host-only origin comparisons
    Audit for origin.host() == <constant> used in security decisions; these ignore scheme/port and should be full-origin equality (DeprecatedGetOriginAsURL() / url::Origin comparison), especially for chrome:// WebUI identification.
  • Regressions from check simplification
    Review 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 gates
    Examine 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.
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;
   }
 
Loading diff…

Original Bug Report

reported by vm...@google.com

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.

  1. DNS Control: An attacker on a local network (e.g., via rogue DHCP, DNS poisoning, or mDNS) configures the hostname contextual-tasks to resolve to an attacker-controlled IP address.
  2. 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.
  3. Victim Navigation: The attacker lures the victim to navigate to https://contextual-tasks/ in Chrome.
  4. Permission Request: The attacker’s webpage executes JavaScript to request a sensitive permission, such as navigator.geolocation.getCurrentPosition(...).
  5. Origin Rewrite: The permission request enters the browser process. PermissionManager::RequestPermissions calls PermissionUtil::GetCanonicalOrigin, which triggers ChromePermissionsClient::GetCanonicalOriginOverride. The host matches "contextual-tasks", so the origin is rewritten to the Google DSE origin (e.g., https://www.google.com/).
  6. Silent Grant: PermissionContextBase checks existing grants using the rewritten origin. Because permissions like Geolocation use TOP_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.

View on issue tracker