Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Omnibox
DescriptionInsufficient validation of untrusted input in Omnibox
ComponentOmnibox
Bug ClassLogic Error
Tracker496379792
Fix commit916f2fabadb2 (chromium/src) +99/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

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

Files Changed

  • chrome/browser/ui/omnibox/omnibox_search_aggregator_browsertest.cc
  • components/omnibox/browser/enterprise_search_aggregator_provider.cc
  • components/omnibox/browser/enterprise_search_aggregator_provider_unittest.cc
From 916f2fabadb2c7ad50a20e06fed1c1a081ff7cb0 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
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 <[email protected]>
Reviewed-by: Alex Chen <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
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": "[email protected]",
+        "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:
Loading diff…

Regression Test / PoC

shipped with the fix
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": "[email protected]",
+        "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:
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_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.

  1. Setup: The victim is in an enterprise environment where the EnterpriseSearchAggregatorSettings policy is configured. An attacker compromises the legitimate enterprise search backend (or a malicious admin configures an attacker-controlled endpoint).
  2. Victim Action: The victim browses to a sensitive target website (e.g., https://bank.example.com).
  3. Triggering the Request: The victim focuses the omnibox and types a query, triggering EnterpriseSearchAggregatorProvider::Start and sending a request to the backend.
  4. 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"}}}]}
  5. Unvalidated Extraction: During parsing, EnterpriseSearchAggregatorProvider::GetMatchDestinationUrl extracts the destinationUri string using ptr_to_string(result.FindString("destinationUri")). For CONTENT suggestions, it returns this string verbatim without checking if the scheme is http/https.
  6. Match Creation: EnterpriseSearchAggregatorProvider::CreateMatch assigns this unvalidated string directly to the new AutocompleteMatch’s destination_url.
  7. User Selection: The victim sees the benign-looking “Important HR Document” suggestion in the omnibox dropdown and selects it.
  8. Navigation and Execution:
    • OmniboxEditModel::OpenMatch initiates a navigation to the javascript: URL.
    • NavigationControllerImpl::NavigateWithoutEntry recognizes the javascript: 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 calls window->GetScriptController().ExecuteJavaScriptURL(...) with CSPDisposition::DO_NOT_CHECK.
  9. 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.

View on issue tracker
Links in the report