Medium watch CVSS 6.5 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Omnibox
DescriptionIncorrect authorization in Omnibox
ComponentOmnibox
Sub-componentOmnibox
Bug ClassAuthorization Error
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.
Tracker547592631
Fix commit9e1d112b91b8 (chromium/src) +48/-10
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedArni Hardarson (Neonix Security)
Reported2026-08-17 (per vendor advisory)
Fix landed2026-08-18 (public git)
Disclosed2026-09-08
Patch gap21 days — fix public in source before the advisory

Background

AIM (AI Mode) / Contextual Tasks
an Omnibox feature that can intercept navigations to trusted search hosts and grant them privileged protocol access, gated by a host-allowlist check.
`interception_allowed_hosts`
a server-supplied list, carried in AimEligibilityResponse, of hostnames whose pages are permitted to be intercepted for AIM; entries are plain unescaped hostnames such as www.google.com.
`RE2::FullMatch`
a regular-expression full-string matcher in which . is a metacharacter matching any single character rather than a literal dot.
`base::MatchPattern`
a glob-style matcher in which only * and ? are wildcards and every other character, including ., is matched literally.

Root Cause Analysis

AimEligibilityService::IsAimHost decided whether a URL’s host was an authorized AIM host by looping over GetMostRecentResponse().interception_allowed_hosts() and calling re2::RE2::FullMatch(url.host(), host_pattern). Because the server sends unescaped hostnames (e.g. www.google.com), each . was interpreted by RE2 as a regex metacharacter matching any character, so an attacker-controlled near-domain like www0google.com or wwwagoogle.com satisfied FullMatch and was treated as an authorized host. This violated the invariant that only exactly the listed hostnames (and their explicit subdomains) are AIM-eligible, allowing an attacker domain to obtain the privileged protocol access AIM grants in Contextual Tasks.

The fix replaces RE2::FullMatch with base::MatchPattern(lower_url_host, base::ToLowerASCII(host_pattern)), so dots are matched literally and only */? act as wildcards, and additionally makes the host_override comparison and pattern matching case-insensitive so legitimate hosts still match while near-domains do not.

Key insight
The single mistake was feeding literal, unescaped hostnames into a regex full-match engine, where . silently became a “match any character” wildcard; the fix switches to base::MatchPattern, whose only wildcards are * and ?, so dots are enforced as literal separators.

Attack Path

  1. Register a near-domain An attacker registers a domain that differs from an allowlisted host only where a literal dot would be, e.g. www0google.com versus the server-provided www.google.com.
  2. Reach the eligibility check The victim navigates (or is induced to navigate) to the attacker host with the AIM-triggering URL shape, so AimEligibilityService::IsAimHost is invoked with url.host() equal to the attacker domain.
  3. Bypass the allowlist re2::RE2::FullMatch(url.host(), host_pattern) treats the . in www.google.com as “any character,” so www0google.com matches and IsAimHost returns true, marking the attacker origin as an authorized AIM host.
  4. Gain privileged access The attacker page is granted the privileged protocol access AIM extends to trusted hosts in Contextual Tasks (inferred: the specific downstream capabilities are not shown in the diff).

Impact Assessment

An attacker who controls a domain that is one character off from an allowlisted AIM host can have that origin incorrectly authorized as a trusted AIM host in the Omnibox/Contextual Tasks flow, gaining the privileged protocol access AIM reserves for legitimate search hosts (inferred beyond the allowlist bypass the patch demonstrates). Preconditions are that the AIM eligibility response lists a hostname whose dots align with the attacker’s near-domain and that a victim reaches the attacker URL with the AIM-matching path/params. The advisory classifies this as incorrect authorization in Omnibox; it does not report exploitation in the wild.

Changed Functions

FunctionChangeNotes
TEST_F
components/omnibox/browser/aim_eligibility_service_unittest.cc
modified

Files Changed

  • components/omnibox/browser/aim_eligibility_service_unittest.cc
  • components/omnibox/browser/aim_eligibility_service.cc

Audit Directions

  • Literal strings into regex engines
    Audit every call site that passes server- or config-supplied hostnames, paths, or identifiers into RE2/re2::RE2::FullMatch or std::regex without escaping; unescaped ., +, ?, and * silently widen the match.
  • Allowlist/authorization matchers
    Review host and origin allowlist comparisons for case sensitivity and for using pattern matchers where exact or glob matching is intended, since over-broad matching is an authorization bypass rather than a mere logic error.
  • Server-controlled matching data
    Trace fields like interception_allowed_hosts/interception_allowed_paths from the proto response to their comparison sinks to confirm the client treats them as trusted literals with the intended wildcard semantics.
From 9e1d112b91b8752e43d3fdd11cdd1cb6a9fc8f7b Mon Sep 17 00:00:00 2001
From: Duncan Mercer <[email protected]>
Date: Mon, 17 Aug 2026 18:01:19 -0700
Subject: [PATCH] [Omnibox] Use MatchPattern for AIM host matching

AimEligibilityService::IsAimHost previously evaluated server-provided
interception_allowed_hosts using RE2::FullMatch. Because the server
supplies unescaped hostnames (e.g. www.google.com), unescaped dots
matched any character in regex, allowing attacker-controlled near-
domains (e.g. www0google.com) to match and gain privileged protocol
access in Contextual Tasks.

This CL updates IsAimHost to use base::MatchPattern with case-
insensitive comparison so that dots are treated as literal characters
and wildcards are restricted to * and ?.

Bug: b:547592631
Change-Id: Ic0b9991722a19f2764fb36226d0c2fd672f17e31
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8254236
Reviewed-by: Justin Donnelly <[email protected]>
Commit-Queue: Justin Donnelly <[email protected]>
Auto-Submit: Duncan Mercer <[email protected]>
Reviewed-by: Matthew Jones <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1680968}
---
diff --git a/components/omnibox/browser/aim_eligibility_service.cc b/components/omnibox/browser/aim_eligibility_service.cc
index 0d6ba3f7..5e1a853 100644
--- a/components/omnibox/browser/aim_eligibility_service.cc
+++ b/components/omnibox/browser/aim_eligibility_service.cc
@@ -15,6 +15,7 @@
 #include "base/logging.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/metrics/histogram_functions.h"
+#include "base/strings/pattern.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
@@ -48,7 +49,6 @@
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "third_party/omnibox_proto/aim_eligibility_client_request.pb.h"
 #include "third_party/omnibox_proto/aim_eligibility_response.pb.h"
-#include "third_party/re2/src/re2/re2.h"
 #include "url/gurl.h"
 
 namespace {
@@ -541,16 +541,18 @@
     const GURL& url,
     std::optional<std::string> host_override) const {
   OMNIBOX_LOG("aim_url_check") << "IsAimHost: Checking host...";
-  if (host_override && host_override.value() == url.host()) {
+  if (host_override &&
+      base::EqualsCaseInsensitiveASCII(host_override.value(), url.host())) {
     OMNIBOX_LOG("aim_url_check") << "Found overridden host!";
     return true;
   }
   OMNIBOX_LOG("aim_url_check")
       << "IsAimHost: Available hosts: "
       << GetMostRecentResponse().interception_allowed_hosts().size();
+  std::string lower_url_host = base::ToLowerASCII(url.host());
   for (const auto& host_pattern :
        GetMostRecentResponse().interception_allowed_hosts()) {
-    if (re2::RE2::FullMatch(url.host(), host_pattern)) {
+    if (base::MatchPattern(lower_url_host, base::ToLowerASCII(host_pattern))) {
       OMNIBOX_LOG("aim_url_check") << "IsAimHost: Matched : " << host_pattern;
       return true;
     }
diff --git a/components/omnibox/browser/aim_eligibility_service_unittest.cc b/components/omnibox/browser/aim_eligibility_service_unittest.cc
index 5ccdbade..dd1dc30 100644
--- a/components/omnibox/browser/aim_eligibility_service_unittest.cc
+++ b/components/omnibox/browser/aim_eligibility_service_unittest.cc
@@ -253,13 +253,8 @@
   rule.mutable_required_params()->Add(CreateQueryParam("b", "2"));
   response.mutable_aim_detection_url_rule()->Add(std::move(rule));
 
-  // The example here does not represent what should be sent from the
-  // backend. If a zero-or-one subdomain needs to be accounted for two
-  // rules should be sent instead:
-  //  - google.com
-  //  - *.google.com
-  // This will avoid catching cases like fakegoogle.com.
-  response.mutable_interception_allowed_hosts()->Add(".*.?google.com");
+  response.mutable_interception_allowed_hosts()->Add("google.com");
+  response.mutable_interception_allowed_hosts()->Add("*.google.com");
 
   response.mutable_interception_allowed_paths()->Add("/search");
 
@@ -278,6 +273,47 @@
       GURL("https://google.example.com/search?a=1&b=2"), std::nullopt));
 }
 
+TEST_F(AimEligibilityServiceTest, IsAimUrl_SecurityNearDomains) {
+  omnibox::AimEligibilityResponse response;
+
+  omnibox::AimEligibilityResponse::AimDetectionUrlRule rule;
+  rule.mutable_required_params()->Add(CreateQueryParam("udm", "50"));
+  response.mutable_aim_detection_url_rule()->Add(std::move(rule));
+
+  response.mutable_interception_allowed_hosts()->Add("www.google.com");
+  response.mutable_interception_allowed_hosts()->Add("google.com");
+
+  response.mutable_interception_allowed_paths()->Add("/search");
+
+  aim_eligibility_service_->SetAimEligibilityResponse(std::move(response));
+
+  // Legitimate domains match.
+  EXPECT_TRUE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://www.google.com/search?udm=50"), std::nullopt));
+  EXPECT_TRUE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://google.com/search?udm=50"), std::nullopt));
+  EXPECT_TRUE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://WWW.GOOGLE.COM/search?udm=50"), std::nullopt));
+
+  // Attacker-controlled near-domains and similar strings MUST NOT match.
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://www0google.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://wwwagoogle.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://www-google.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://google0com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://googleicom/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://fakegoogle.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://google.com.evil.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://notgoogle.com/search?udm=50"), std::nullopt));
+}
+
 TEST_F(AimEligibilityServiceTest, HasNoCobrowseParams_ExactMatch) {
   omnibox::AimEligibilityResponse response;
   response.mutable_no_cobrowse_params()->Add(CreateQueryParam("ncb", "1"));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/omnibox/browser/aim_eligibility_service_unittest.cc b/components/omnibox/browser/aim_eligibility_service_unittest.cc
index 5ccdbade..dd1dc30 100644
--- a/components/omnibox/browser/aim_eligibility_service_unittest.cc
+++ b/components/omnibox/browser/aim_eligibility_service_unittest.cc
@@ -253,13 +253,8 @@
   rule.mutable_required_params()->Add(CreateQueryParam("b", "2"));
   response.mutable_aim_detection_url_rule()->Add(std::move(rule));
 
-  // The example here does not represent what should be sent from the
-  // backend. If a zero-or-one subdomain needs to be accounted for two
-  // rules should be sent instead:
-  //  - google.com
-  //  - *.google.com
-  // This will avoid catching cases like fakegoogle.com.
-  response.mutable_interception_allowed_hosts()->Add(".*.?google.com");
+  response.mutable_interception_allowed_hosts()->Add("google.com");
+  response.mutable_interception_allowed_hosts()->Add("*.google.com");
 
   response.mutable_interception_allowed_paths()->Add("/search");
 
@@ -278,6 +273,47 @@
       GURL("https://google.example.com/search?a=1&b=2"), std::nullopt));
 }
 
+TEST_F(AimEligibilityServiceTest, IsAimUrl_SecurityNearDomains) {
+  omnibox::AimEligibilityResponse response;
+
+  omnibox::AimEligibilityResponse::AimDetectionUrlRule rule;
+  rule.mutable_required_params()->Add(CreateQueryParam("udm", "50"));
+  response.mutable_aim_detection_url_rule()->Add(std::move(rule));
+
+  response.mutable_interception_allowed_hosts()->Add("www.google.com");
+  response.mutable_interception_allowed_hosts()->Add("google.com");
+
+  response.mutable_interception_allowed_paths()->Add("/search");
+
+  aim_eligibility_service_->SetAimEligibilityResponse(std::move(response));
+
+  // Legitimate domains match.
+  EXPECT_TRUE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://www.google.com/search?udm=50"), std::nullopt));
+  EXPECT_TRUE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://google.com/search?udm=50"), std::nullopt));
+  EXPECT_TRUE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://WWW.GOOGLE.COM/search?udm=50"), std::nullopt));
+
+  // Attacker-controlled near-domains and similar strings MUST NOT match.
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://www0google.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://wwwagoogle.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://www-google.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://google0com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://googleicom/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://fakegoogle.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://google.com.evil.com/search?udm=50"), std::nullopt));
+  EXPECT_FALSE(aim_eligibility_service_->IsAimUrl(
+      GURL("https://notgoogle.com/search?udm=50"), std::nullopt));
+}
+
 TEST_F(AimEligibilityServiceTest, HasNoCobrowseParams_ExactMatch) {
   omnibox::AimEligibilityResponse response;
   response.mutable_no_cobrowse_params()->Add(CreateQueryParam("ncb", "1"));
Loading diff…

Original Bug Report

reported by [email protected]

Unescaped AIM hostname regex lets a near-domain become Chrome's privileged Contextual Tasks protocol peer


Report description

Unescaped AIM hostname regex lets a near-domain become Chrome’s privileged Contextual Tasks protocol peer


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/chromium/src/+/242e11fd56b4443f4feb7ea9fd2ed70f1afc1118/components/omnibox/browser/aim_eligibility_service.cc


The problem

Please describe the technical details of the vulnerability

AimEligibilityService::IsAimHost passes each server-provided interception_allowed_hosts string directly to RE2::FullMatch. The live response supplied www.google.com, but the dots are not escaped, so the registrable near-domain www0google.com also matches.

For a matching /search?udm=50 URL, Chrome treats the attacker origin as a Google AIM page, intercepts the navigation, stores the complete near-domain URL as a Contextual Tasks creation URL, and loads it as the embedded WebView. That page receives Chrome’s privileged protocol handshake. It can send a valid AimToClientMessage.inject_input with submit_after_injection=true, causing the browser-owned composebox to submit attacker-selected text and return the serialized ClientToAimMessage.submit_query to the attacker origin.

I reproduced the complete diagnostic chain 3/3 on Canary 153.0.8009.0. A matched non-AIM host remained an ordinary page and received no handshake or returned query in 3/3 trials. The exact bundled-RE2 control confirms that escaping the dots prevents the near-domain match.

The minimized attachment uses loopback hostname mappings and disables AimCoBrowseEligibilityCheckEnabled, because a fresh response for my current test account sets is_cobrowse_eligible=false. It does not use DevTools to navigate the WebView. I am explicitly reporting this as a deterministic feature-gated authorization-boundary violation; universal stock remote delivery is not claimed.

Impact analysis

An attacker controlling a hostname that matches an unescaped AIM allowlist pattern can impersonate Chrome’s intended Google AIM protocol peer. For an eligible Contextual Tasks user who follows an AIM-shaped URL, the attacker page is loaded inside the privileged Contextual Tasks host instead of as an ordinary web page.

The concrete demonstrated capability is bidirectional access to a browser-managed trusted channel: the attacker forces a composebox submission without a second confirmation and receives Chrome’s resulting serialized query. This crosses the intended origin/authorization boundary between arbitrary web content and the trusted Google AIM WebView. I do not claim native code execution, sandbox escape, unrelated-tab disclosure, or universal reachability for accounts whose co-browse gate is disabled.


The cause

What version of Chrome have you found the security issue in?

  • 153.0.8009.0 Canary, macOS arm64 - 152.0.7977.42 Chrome for Testing / Beta-era build, macOS arm64

No, it is not related to a crash.

Choose the type of vulnerability

Permissions Bypass

How would you like to be publicly acknowledged for your report?

Arni Hardarson (Neonix Security)

View on issue tracker