Chrome · Omnibox
CVE-2026-87610
Logic Error in Omnibox
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/omnibox/browser/aim_eligibility_service_unittest.cc |
modified |
Files Changed
components/omnibox/browser/aim_eligibility_service.cccomponents/omnibox/browser/aim_eligibility_service_unittest.cc
Patch
From 9e1d112b91b8752e43d3fdd11cdd1cb6a9fc8f7b Mon Sep 17 00:00:00 2001
From: Duncan Mercer <mercerd@google.com>
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 <jdonnelly@chromium.org>
Commit-Queue: Justin Donnelly <jdonnelly@chromium.org>
Auto-Submit: Duncan Mercer <mercerd@google.com>
Reviewed-by: Matthew Jones <mdjones@chromium.org>
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
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page