CVE-2026-7953
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc |
modified |
Files Changed
chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cccomponents/omnibox/browser/enterprise_search_aggregator_provider.cccomponents/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
Patch
From 916f2fabadb2c7ad50a20e06fed1c1a081ff7cb0 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Thu, 26 Mar 2026 14:01:32 -0700
Subject: [PATCH] [omnibox] Require HTTP(S) scheme for destinationUri in EnterpriseSearchAggregatorProvider
GetMatchDestinationUrl now verifies that destinationUri has an HTTP or
HTTPS scheme for CONTENT and PEOPLE suggestions. Matches with invalid
GURLs or other schemes are discarded.
Fixed: 496379792
Change-Id: I5f9949b35b479036b41e04ccb976a1d0f03b6e4e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7705937
Reviewed-by: Justin Donnelly <jdonnelly@chromium.org>
Reviewed-by: Alex Chen <alexwchen@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1605770}
---
diff --git a/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc b/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc
index 85dfe102..fd0703c 100644
--- a/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc
+++ b/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc
@@ -408,6 +408,63 @@
})));
}
+IN_PROC_BROWSER_TEST_F(OmniboxSearchAggregatorSingleRequestTest,
+ DiscardsInvalidJavascriptUrl) {
+ net::test_server::ControllableHttpResponse search_aggregator_response(
+ embedded_test_server(), kSearchAggregatorPolicySuggestPath);
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ // 1. Start on a benign page (e.g., https://www.google.com).
+ GURL initial_url = embedded_test_server()->GetURL("/title1.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));
+
+ base::Value policy_value = CreateEnterpriseSearchAggregatorPolicyValue(
+ embedded_test_server()
+ ->GetURL(kSearchAggregatorPolicySuggestPath)
+ .spec());
+ policy::PolicyMap policies;
+ policies.Set(policy::key::kEnterpriseSearchAggregatorSettings,
+ policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
+ policy::POLICY_SOURCE_CLOUD, std::move(policy_value), nullptr);
+ policy_provider()->UpdateChromePolicy(policies);
+
+ // 2. Trigger autocomplete.
+ AutocompleteInput input(
+ kSearchInput, metrics::OmniboxEventProto::NTP,
+ ChromeAutocompleteSchemeClassifier(browser()->profile()));
+ input.set_keyword_mode_entry_method(metrics::OmniboxEventProto::TAB);
+ controller()->Start(input);
+
+ // 3. Respond with an invalid javascript: URL.
+ search_aggregator_response.WaitForRequest();
+ const std::string invalid_json = R"invalid({
+ "contentSuggestions": [
+ {
+ "suggestion": "Invalid Suggestion",
+ "document": {
+ "derivedStructData": {
+ "title": "Invalid Suggestion"
+ }
+ },
+ "destinationUri": "javascript:alert(1)",
+ "score": 0.8
+ }
+ ]
+ })invalid";
+ search_aggregator_response.Send(net::HTTP_OK, "application/json",
+ invalid_json);
+ search_aggregator_response.Done();
+
+ WaitForAutocompleteDone(browser());
+
+ // 4. Verify that no javascript: match exists.
+ const AutocompleteResult& result = controller()->result();
+ auto it = std::find_if(result.begin(), result.end(), [](const auto& match) {
+ return match.destination_url.SchemeIs(url::kJavaScriptScheme);
+ });
+ EXPECT_EQ(it, result.end());
+}
+
// TODO(crbug.com/425120649) Flaky.
IN_PROC_BROWSER_TEST_F(OmniboxSearchAggregatorSingleRequestTest,
DISABLED_RedirectedResponse) {
diff --git a/components/omnibox/browser/enterprise_search_aggregator_provider.cc b/components/omnibox/browser/enterprise_search_aggregator_provider.cc
index 929369b..b4a3231 100644
--- a/components/omnibox/browser/enterprise_search_aggregator_provider.cc
+++ b/components/omnibox/browser/enterprise_search_aggregator_provider.cc
@@ -1041,6 +1041,10 @@
ptr_to_string(result.FindString("destinationUri"));
if (suggestion_type == SuggestionType::CONTENT ||
suggestion_type == SuggestionType::PEOPLE) {
+ GURL gurl(destination_uri);
+ if (!gurl.is_valid() || !gurl.SchemeIsHTTPOrHTTPS()) {
+ return "";
+ }
return destination_uri;
}
diff --git a/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc b/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
index 82e773e..f76399e 100644
--- a/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
+++ b/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
@@ -537,6 +537,35 @@
const std::string kNonDictJsonResponse =
base::StringPrintf(R"(["test","result1","result2"])");
+const std::string kInvalidJsonResponse = R"invalid({
+ "contentSuggestions": [
+ {
+ "suggestion": "Invalid Suggestion",
+ "document": {
+ "derivedStructData": {
+ "title": "Invalid Suggestion"
+ }
+ },
+ "destinationUri": "javascript:alert(1)",
+ "score": 0.8
+ }
+ ],
+ "peopleSuggestions": [
+ {
+ "suggestion": "invalid@example.com",
+ "document": {
+ "derivedStructData": {
+ "name": {
+ "displayName": "Invalid Person"
+ }
+ }
+ },
+ "destinationUri": "javascript:alert(2)",
+ "score": 0.8
+ }
+ ]
+ })invalid";
+
// Helper methods to dynamically generate valid responses.
std::string CreateQueryResult(const std::string& query,
const float score = 0.0) {
@@ -1983,6 +2012,15 @@
EXPECT_EQ(matches[2].destination_url, GURL("https://url3/"));
}
+TEST_F(EnterpriseSearchAggregatorProviderTest, DiscardsInvalidJavascriptUrl) {
+ provider_->adjusted_input_ = CreateInput(u"john d", true);
+ StartAndComplete3Requests(200, kInvalidJsonResponse);
+
+ ACMatches matches = provider_->matches_;
+ // After fix, it should have 0 matches because javascript: URLs are discarded.
+ EXPECT_EQ(matches.size(), 0u);
+}
+
TEST_F(EnterpriseSearchAggregatorProviderSingleRequestTest,
Logging_SingleRequest) {
// The code flow is:
Regression Test / PoC
diff --git a/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc b/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc
index 85dfe102..fd0703c 100644
--- a/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc
+++ b/chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc
@@ -408,6 +408,63 @@
})));
}
+IN_PROC_BROWSER_TEST_F(OmniboxSearchAggregatorSingleRequestTest,
+ DiscardsInvalidJavascriptUrl) {
+ net::test_server::ControllableHttpResponse search_aggregator_response(
+ embedded_test_server(), kSearchAggregatorPolicySuggestPath);
+ ASSERT_TRUE(embedded_test_server()->Start());
+
+ // 1. Start on a benign page (e.g., https://www.google.com).
+ GURL initial_url = embedded_test_server()->GetURL("/title1.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));
+
+ base::Value policy_value = CreateEnterpriseSearchAggregatorPolicyValue(
+ embedded_test_server()
+ ->GetURL(kSearchAggregatorPolicySuggestPath)
+ .spec());
+ policy::PolicyMap policies;
+ policies.Set(policy::key::kEnterpriseSearchAggregatorSettings,
+ policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
+ policy::POLICY_SOURCE_CLOUD, std::move(policy_value), nullptr);
+ policy_provider()->UpdateChromePolicy(policies);
+
+ // 2. Trigger autocomplete.
+ AutocompleteInput input(
+ kSearchInput, metrics::OmniboxEventProto::NTP,
+ ChromeAutocompleteSchemeClassifier(browser()->profile()));
+ input.set_keyword_mode_entry_method(metrics::OmniboxEventProto::TAB);
+ controller()->Start(input);
+
+ // 3. Respond with an invalid javascript: URL.
+ search_aggregator_response.WaitForRequest();
+ const std::string invalid_json = R"invalid({
+ "contentSuggestions": [
+ {
+ "suggestion": "Invalid Suggestion",
+ "document": {
+ "derivedStructData": {
+ "title": "Invalid Suggestion"
+ }
+ },
+ "destinationUri": "javascript:alert(1)",
+ "score": 0.8
+ }
+ ]
+ })invalid";
+ search_aggregator_response.Send(net::HTTP_OK, "application/json",
+ invalid_json);
+ search_aggregator_response.Done();
+
+ WaitForAutocompleteDone(browser());
+
+ // 4. Verify that no javascript: match exists.
+ const AutocompleteResult& result = controller()->result();
+ auto it = std::find_if(result.begin(), result.end(), [](const auto& match) {
+ return match.destination_url.SchemeIs(url::kJavaScriptScheme);
+ });
+ EXPECT_EQ(it, result.end());
+}
+
// TODO(crbug.com/425120649) Flaky.
IN_PROC_BROWSER_TEST_F(OmniboxSearchAggregatorSingleRequestTest,
DISABLED_RedirectedResponse) {
diff --git a/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc b/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
index 82e773e..f76399e 100644
--- a/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
+++ b/components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
@@ -537,6 +537,35 @@
const std::string kNonDictJsonResponse =
base::StringPrintf(R"(["test","result1","result2"])");
+const std::string kInvalidJsonResponse = R"invalid({
+ "contentSuggestions": [
+ {
+ "suggestion": "Invalid Suggestion",
+ "document": {
+ "derivedStructData": {
+ "title": "Invalid Suggestion"
+ }
+ },
+ "destinationUri": "javascript:alert(1)",
+ "score": 0.8
+ }
+ ],
+ "peopleSuggestions": [
+ {
+ "suggestion": "invalid@example.com",
+ "document": {
+ "derivedStructData": {
+ "name": {
+ "displayName": "Invalid Person"
+ }
+ }
+ },
+ "destinationUri": "javascript:alert(2)",
+ "score": 0.8
+ }
+ ]
+ })invalid";
+
// Helper methods to dynamically generate valid responses.
std::string CreateQueryResult(const std::string& query,
const float score = 0.0) {
@@ -1983,6 +2012,15 @@
EXPECT_EQ(matches[2].destination_url, GURL("https://url3/"));
}
+TEST_F(EnterpriseSearchAggregatorProviderTest, DiscardsInvalidJavascriptUrl) {
+ provider_->adjusted_input_ = CreateInput(u"john d", true);
+ StartAndComplete3Requests(200, kInvalidJsonResponse);
+
+ ACMatches matches = provider_->matches_;
+ // After fix, it should have 0 matches because javascript: URLs are discarded.
+ EXPECT_EQ(matches.size(), 0u);
+}
+
TEST_F(EnterpriseSearchAggregatorProviderSingleRequestTest,
Logging_SingleRequest) {
// The code flow is:
Original Bug Report
UXSS via unvalidated javascript: URI in Enterprise Search Aggregator
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The EnterpriseSearchAggregatorProvider fails to validate the scheme of URLs returned by the enterprise search backend for certain suggestion types. This allows a compromised or malicious backend to inject javascript: URLs into omnibox suggestions. If a user selects one of these suggestions, the script executes in the context of the currently active tab, potentially leading to Universal Cross-Site Scripting (UXSS).
Affected files:
components/omnibox/browser/enterprise_search_aggregator_provider.ccthird_party/blink/renderer/core/frame/local_frame.cc
Estimated timestamp from git blame: 2026-01-25
Summary
A vulnerability exists in EnterpriseSearchAggregatorProvider where it blindly trusts the destinationUri provided by an enterprise search backend for CONTENT and PEOPLE suggestion types. By returning a javascript: URL, a compromised or malicious enterprise backend can execute arbitrary JavaScript in the context of the user’s currently active tab (Universal XSS).
While Chrome proactively strips javascript: schemas from URLs that users paste into the omnibox to prevent Self-XSS, this sanitization does not apply to URLs provided asynchronously by search providers via JSON responses.
Potential Execution Steps
Note: These are suggested steps for exploitation, as our setup does not have the ability to run code to verify with a working proof of concept.
- Setup: The victim is in an enterprise environment where the
EnterpriseSearchAggregatorSettingspolicy is configured. An attacker compromises the legitimate enterprise search backend (or a malicious admin configures an attacker-controlled endpoint). - Victim Action: The victim browses to a sensitive target website (e.g.,
https://bank.example.com). - Triggering the Request: The victim focuses the omnibox and types a query, triggering
EnterpriseSearchAggregatorProvider::Startand sending a request to the backend. - Malicious Response: The backend responds with a JSON payload containing a suggestion with a malicious
destinationUri:{"contentSuggestions": [{"destinationUri": "javascript:alert(document.domain)", "document": {"derivedStructData": {"title": "Important HR Document"}}}]} - Unvalidated Extraction: During parsing,
EnterpriseSearchAggregatorProvider::GetMatchDestinationUrlextracts thedestinationUristring usingptr_to_string(result.FindString("destinationUri")). ForCONTENTsuggestions, it returns this string verbatim without checking if the scheme ishttp/https. - Match Creation:
EnterpriseSearchAggregatorProvider::CreateMatchassigns this unvalidated string directly to the newAutocompleteMatch’sdestination_url. - User Selection: The victim sees the benign-looking “Important HR Document” suggestion in the omnibox dropdown and selects it.
- Navigation and Execution:
OmniboxEditModel::OpenMatchinitiates a navigation to thejavascript:URL.NavigationControllerImpl::NavigateWithoutEntryrecognizes thejavascript:scheme as a renderer debug URL (blink::IsRendererDebugURL).- Instead of a standard navigation, it routes the URL directly to the current tab’s renderer process via
RenderFrameHostImpl::HandleRendererDebugURL. - The renderer invokes
LocalFrame::LoadJavaScriptURL, which callswindow->GetScriptController().ExecuteJavaScriptURL(...)withCSPDisposition::DO_NOT_CHECK.
- UXSS Realized: The JavaScript payload executes synchronously in the Main World of the currently active document (
https://bank.example.com), allowing the attacker to steal cookies, hijack the session, or manipulate the DOM.
Suggested Fix
The EnterpriseSearchAggregatorProvider must validate the scheme of the destinationUri before creating an AutocompleteMatch.
In EnterpriseSearchAggregatorProvider::GetMatchDestinationUrl (or right before CreateMatch is called), verify that the parsed GURL has an allowed scheme (e.g., SchemeIsHTTPOrHTTPS()). If the URL uses a dangerous scheme like javascript:, file:, or chrome:, the suggestion should be discarded.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results 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. Please feel free to reach out to me if you have concerns or feedback.