Medium watch CVSS 3.7 chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
3.7
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactServer-side request forgery in Omnibox
DescriptionServer-side request forgery in Omnibox
ComponentOmnibox
Sub-componentOmnibox
Bug ClassLogic 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.
Tracker511832293
Fix commit8d61b9c6aa3f (chromium/src) +80/-13
CWECWE-918
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-10 (per vendor advisory)
Fix landed2026-08-17 (public git)
Disclosed2026-09-17
Patch gap31 days — fix public in source before the advisory

Background

`BaseSearchProvider::CreateSearchSuggestion`
the omnibox helper (named in the commit message; its name does not appear in the @@ hunk headers) that turns a parsed suggest-server result into an AutocompleteMatch displayed in the address bar.
Third-party search engine (`TemplateURL`)
Chrome lets the user set any site as the default or keyword search engine, and that engine’s server supplies the suggest/autocomplete responses (suggest_template_info, entity_info, answer_template) that populate matches.
`match.image_url` / `match.answer_template`
fields on the match carrying a server-supplied thumbnail URL and rich-answer payload; when the suggestion is rendered, the browser issues a network fetch for these image URLs.
`search::TemplateURLIsGoogle`
predicate returning whether the engine that produced the suggestion is Google, i.e. a trusted first-party source.

Root Cause Analysis

In CreateSearchSuggestion, the fields image_url, image_dominant_color, answer_template, and answer_type were populated unconditionally from the suggest response, regardless of which search engine produced it. The invariant that only Google-supplied (trusted) suggestions may carry browser-fetched image and rich-answer payloads was not enforced here, even though the same function already gated Actions-in-Suggest behind an is_google check. A malicious or attacker-influenced non-Google search engine could therefore return a suggest response whose image_url (or entity_info().image_url(), or an image inside answer_template) pointed at an arbitrary host, and Chrome would later fetch it.

The fix hoists the bool is_google = search::TemplateURLIsGoogle(...) computation to the top of the function and wraps the image/answer assignments in if (is_google), so untrusted engines no longer contribute those fetch-triggering URLs. This mirrors the pre-existing gate on Action-in-Suggest and closes the untrusted-input path.

Key insight
The single mistake was populating fetch-triggering, server-controlled fields (image_url, answer_template) without checking that the suggesting engine was trusted (Google); the fix adds the missing if (is_google) gate around those assignments, matching the trust check already applied elsewhere in the same function.

Attack Path

  1. Attacker controls a search engine the user has a non-Google engine set as default or keyword search (attacker-operated, or one whose suggest endpoint the attacker can influence), so its server responses reach CreateSearchSuggestion.
  2. Malicious suggest response the engine returns a suggest result whose entity_info().image_url() / suggest_template_info image URL / answer_template references an attacker-chosen target (e.g. an internal or private-network host).
  3. Unchecked field population pre-patch, CreateSearchSuggestion copies image_url, image_dominant_color, answer_template, and answer_type into the match with no is_google check, so the untrusted URL is accepted.
  4. Browser fetches the URL when the omnibox renders the suggestion, the browser issues a request to the attacker-supplied URL, producing a blind SSRF / Private Network Access bypass (inferred; the fetch sink is downstream of the diff and not shown in it).

Impact Assessment

An attacker who controls or influences a non-Google search engine can cause the browser to issue requests to arbitrary attacker-chosen URLs, yielding a blind server-side request forgery in the Omnibox as described by the advisory, with potential to reach private-network resources via a PNA bypass (inferred). The request originates from the browser’s own network context, and the precondition is that the victim uses an affected third-party search engine that supplies the crafted suggest response. No exploited-in-the-wild status or severity rating is stated in the advisory, and any escalation beyond issuing the forged request is not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
components/omnibox/browser/base_search_provider.cc
modified
TEST_F
chrome/browser/autocomplete/search_provider_unittest.cc
modified
TEST_F
components/omnibox/browser/base_search_provider_unittest.cc
modified

Files Changed

  • components/omnibox/browser/base_search_provider.cc
  • chrome/browser/autocomplete/search_provider_unittest.cc
  • components/omnibox/browser/base_search_provider_unittest.cc

Audit Directions

  • Trust-gating of server-supplied URLs
    audit every place a match/suggestion field derived from a suggest response becomes a URL the browser later fetches or navigates, and confirm each is gated on the source engine’s trust (TemplateURLIsGoogle) rather than populated unconditionally.
  • Inconsistent gates within one function
    where a function already trust-checks one feature (e.g. Actions-in-Suggest) but copies other server fields outside that check, treat the ungated fields as suspect and verify they cannot carry attacker-controlled URLs or payloads.
  • Third-party search-engine input surface
    review all consumers of entity_info, suggest_template_info, and answer_template from non-Google engines for SSRF, PNA-bypass, or content-injection risk, since these are attacker-controllable whenever the default/keyword engine is untrusted.
From 8d61b9c6aa3f45b1ce0840d757ffd6cd10f9df51 Mon Sep 17 00:00:00 2001
From: Ananya Seelam <[email protected]>
Date: Mon, 17 Aug 2026 12:55:57 -0700
Subject: [PATCH] [omnibox] Restrict rich answers and suggest images to Google

BaseSearchProvider::CreateSearchSuggestion() now only populates
answer_template, answer_type, suggest_template, image_url, and
image_dominant_color when the suggesting engine is Google, mirroring the
existing gate on Action-in-Suggest in the same function.

Note: While Answers in Suggest (AiS) has been sunset server-side and its
client-side code will be deprecated/removed soon, this gate is still
necessary to prevent untrusted third-party search engines from supplying
payloads that trigger blind SSRF / PNA bypasses, as well as to protect
active non-AiS image paths (e.g. Entity Suggestions).

Adds BaseSearchProviderTest.AnswerAndImageOnlyPopulatedForGoogle and
updates existing tests to use a Google search URL.

Bug: 511832293
Change-Id: I46c5a69f731bbe37faea7a26abb94a214f237389
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8262150
Commit-Queue: Ananya Seelam <[email protected]>
Reviewed-by: Marlon Facey <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1680716}
---
diff --git a/components/omnibox/browser/base_search_provider.cc b/components/omnibox/browser/base_search_provider.cc
index eba94e96..1407be3 100644
--- a/components/omnibox/browser/base_search_provider.cc
+++ b/components/omnibox/browser/base_search_provider.cc
@@ -128,27 +128,32 @@
   if (!template_url)
     return match;
   match.keyword = template_url->keyword();
+  bool is_google = search::TemplateURLIsGoogle(template_url, search_terms_data);
   // If SuggestTemplateInfo is available, use it. Otherwise, continue
   // populating information from EntityInfo.
   const auto& suggest_template_info = suggestion.suggest_template_info();
   if (suggest_template_info) {
     match.suggest_template = suggest_template_info;
-    if (suggest_template_info->has_image()) {
-      match.image_dominant_color =
-          suggest_template_info->image().dominant_color();
-      match.image_url = GURL(suggest_template_info->image().url());
+  }
+  if (is_google) {
+    if (suggest_template_info) {
+      if (suggest_template_info->has_image()) {
+        match.image_dominant_color =
+            suggest_template_info->image().dominant_color();
+        match.image_url = GURL(suggest_template_info->image().url());
+      }
+    } else {
+      match.image_dominant_color = suggestion.entity_info().dominant_color();
+      match.image_url = GURL(suggestion.entity_info().image_url());
     }
-  } else {
-    match.image_dominant_color = suggestion.entity_info().dominant_color();
-    match.image_url = GURL(suggestion.entity_info().image_url());
+    match.answer_template = suggestion.answer_template();
+    match.answer_type = suggestion.answer_type();
   }
   match.entity_id = suggestion.entity_info().entity_id();
   match.website_uri = suggestion.entity_info().website_uri();
   match.contents = suggestion.match_contents();
   match.contents_class = suggestion.match_contents_class();
   match.suggestion_group_id = suggestion.suggestion_group_id();
-  match.answer_template = suggestion.answer_template();
-  match.answer_type = suggestion.answer_type();
   match.suggest_type = suggestion.suggest_type();
   for (const int subtype : suggestion.subtypes()) {
     match.subtypes.insert(SuggestSubtypeForNumber(subtype));
@@ -231,7 +236,6 @@
   match.transition = suggestion.from_keyword() ? ui::PAGE_TRANSITION_KEYWORD
                                                : ui::PAGE_TRANSITION_GENERATED;
 
-  bool is_google = search::TemplateURLIsGoogle(template_url, search_terms_data);
   // Attach Actions in Suggest to the newly created match on Android if Google
   // is the default search engine.
   if ((is_android || is_ios) && is_google) {
diff --git a/chrome/browser/autocomplete/search_provider_unittest.cc b/chrome/browser/autocomplete/search_provider_unittest.cc
index dfc05612..1adab10 100644
--- a/chrome/browser/autocomplete/search_provider_unittest.cc
+++ b/chrome/browser/autocomplete/search_provider_unittest.cc
@@ -4077,6 +4077,19 @@
 }
 
 TEST_F(SearchProviderTest, CopyAnswerToVerbatim) {
+  TemplateURLData google_template_url_data;
+  google_template_url_data.SetShortName(u"g");
+  google_template_url_data.SetURL(
+      "https://www.google.com/search?q={searchTerms}");
+  google_template_url_data.suggestions_url =
+      "https://www.google.com/suggest?q={searchTerms}";
+
+  TemplateURLService* turl_model =
+      TemplateURLServiceFactory::GetForProfile(profile_.get());
+  TemplateURL* template_url =
+      turl_model->Add(std::make_unique<TemplateURL>(google_template_url_data));
+  turl_model->SetUserSelectedDefaultSearchProvider(template_url);
+
   QueryForInput(u"weather los angeles ", false, false);
 
   AutocompleteMatch match;
diff --git a/components/omnibox/browser/base_search_provider_unittest.cc b/components/omnibox/browser/base_search_provider_unittest.cc
index 431df02a..c4353bc2 100644
--- a/components/omnibox/browser/base_search_provider_unittest.cc
+++ b/components/omnibox/browser/base_search_provider_unittest.cc
@@ -123,7 +123,7 @@
 
 TEST_F(BaseSearchProviderTest, PreserveAnswersWhenDeduplicating) {
   TemplateURLData data;
-  data.SetURL("http://foo.com/url?bar={searchTerms}");
+  data.SetURL("https://www.google.com/search?q={searchTerms}");
   auto template_url = std::make_unique<TemplateURL>(data);
 
   TestBaseSearchProvider::MatchMap map;
@@ -205,7 +205,7 @@
 
 TEST_F(BaseSearchProviderTest, PreserveImageWhenDeduplicating) {
   TemplateURLData data;
-  data.SetURL("http://foo.com/url?bar={searchTerms}");
+  data.SetURL("https://www.google.com/search?q={searchTerms}");
   auto template_url = std::make_unique<TemplateURL>(data);
 
   TestBaseSearchProvider::MatchMap map;
@@ -774,7 +774,7 @@
 
 TEST_F(BaseSearchProviderTest, SuggestTemplateInfoPopulatesMatch) {
   TemplateURLData data;
-  data.SetURL("http://foo.com/url?bar={searchTerms}");
+  data.SetURL("https://www.google.com/search?q={searchTerms}");
   auto template_url = std::make_unique<TemplateURL>(data);
 
   TestBaseSearchProvider::MatchMap map;
@@ -818,3 +818,53 @@
             match.image_dominant_color);
   EXPECT_EQ("gs_ssp=abc", match.search_terms_args->additional_query_params);
 }
+
+TEST_F(BaseSearchProviderTest, AnswerAndImageOnlyPopulatedForGoogle) {
+  std::u16string query = u"weather";
+  omnibox::RichAnswerTemplate answer_template;
+  answer_template.add_answers()->mutable_headline()->set_text("72°F");
+
+  omnibox::EntityInfo entity_info;
+  entity_info.set_image_url("https://example.com/image.png");
+  entity_info.set_dominant_color("#ffffff");
+
+  SearchSuggestionParser::SuggestResult result(
+      query, AutocompleteMatchType::SEARCH_SUGGEST, omnibox::TYPE_QUERY,
+      /*subtypes=*/{}, /*from_keyword=*/false,
+      /*navigational_intent=*/omnibox::NAV_INTENT_NONE,
+      /*relevance=*/1300, /*relevance_from_server=*/true,
+      /*input_text=*/query);
+  result.SetRichAnswerTemplate(answer_template);
+  result.SetEntityInfo(entity_info);
+
+  // 1. Non-Google search engine: fields should NOT be populated.
+  {
+    TemplateURLData non_google_data;
+    non_google_data.SetURL("https://evil.com/search?q={searchTerms}");
+    auto non_google_turl = std::make_unique<TemplateURL>(non_google_data);
+    TestBaseSearchProvider::MatchMap map;
+    provider_->AddMatchToMap(
+        result, AutocompleteInput(), non_google_turl.get(),
+        client_->GetTemplateURLService()->search_terms_data(),
+        TemplateURLRef::NO_SUGGESTION_CHOSEN, false, false, &map);
+    ASSERT_EQ(1U, map.size());
+    EXPECT_FALSE(map.begin()->second.answer_template.has_value());
+    EXPECT_TRUE(map.begin()->second.image_url.is_empty());
+  }
+
+  // 2. Google search engine: fields SHOULD be populated.
+  {
+    TemplateURLData google_data;
+    google_data.SetURL("https://www.google.com/search?q={searchTerms}");
+    auto google_turl = std::make_unique<TemplateURL>(google_data);
+    TestBaseSearchProvider::MatchMap map;
+    provider_->AddMatchToMap(
+        result, AutocompleteInput(), google_turl.get(),
+        client_->GetTemplateURLService()->search_terms_data(),
+        TemplateURLRef::NO_SUGGESTION_CHOSEN, false, false, &map);
+    ASSERT_EQ(1U, map.size());
+    EXPECT_TRUE(map.begin()->second.answer_template.has_value());
+    EXPECT_EQ("https://example.com/image.png",
+              map.begin()->second.image_url.spec());
+  }
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/autocomplete/search_provider_unittest.cc b/chrome/browser/autocomplete/search_provider_unittest.cc
index dfc05612..1adab10 100644
--- a/chrome/browser/autocomplete/search_provider_unittest.cc
+++ b/chrome/browser/autocomplete/search_provider_unittest.cc
@@ -4077,6 +4077,19 @@
 }
 
 TEST_F(SearchProviderTest, CopyAnswerToVerbatim) {
+  TemplateURLData google_template_url_data;
+  google_template_url_data.SetShortName(u"g");
+  google_template_url_data.SetURL(
+      "https://www.google.com/search?q={searchTerms}");
+  google_template_url_data.suggestions_url =
+      "https://www.google.com/suggest?q={searchTerms}";
+
+  TemplateURLService* turl_model =
+      TemplateURLServiceFactory::GetForProfile(profile_.get());
+  TemplateURL* template_url =
+      turl_model->Add(std::make_unique<TemplateURL>(google_template_url_data));
+  turl_model->SetUserSelectedDefaultSearchProvider(template_url);
+
   QueryForInput(u"weather los angeles ", false, false);
 
   AutocompleteMatch match;
diff --git a/components/omnibox/browser/base_search_provider_unittest.cc b/components/omnibox/browser/base_search_provider_unittest.cc
index 431df02a..c4353bc2 100644
--- a/components/omnibox/browser/base_search_provider_unittest.cc
+++ b/components/omnibox/browser/base_search_provider_unittest.cc
@@ -123,7 +123,7 @@
 
 TEST_F(BaseSearchProviderTest, PreserveAnswersWhenDeduplicating) {
   TemplateURLData data;
-  data.SetURL("http://foo.com/url?bar={searchTerms}");
+  data.SetURL("https://www.google.com/search?q={searchTerms}");
   auto template_url = std::make_unique<TemplateURL>(data);
 
   TestBaseSearchProvider::MatchMap map;
@@ -205,7 +205,7 @@
 
 TEST_F(BaseSearchProviderTest, PreserveImageWhenDeduplicating) {
   TemplateURLData data;
-  data.SetURL("http://foo.com/url?bar={searchTerms}");
+  data.SetURL("https://www.google.com/search?q={searchTerms}");
   auto template_url = std::make_unique<TemplateURL>(data);
 
   TestBaseSearchProvider::MatchMap map;
@@ -774,7 +774,7 @@
 
 TEST_F(BaseSearchProviderTest, SuggestTemplateInfoPopulatesMatch) {
   TemplateURLData data;
-  data.SetURL("http://foo.com/url?bar={searchTerms}");
+  data.SetURL("https://www.google.com/search?q={searchTerms}");
   auto template_url = std::make_unique<TemplateURL>(data);
 
   TestBaseSearchProvider::MatchMap map;
@@ -818,3 +818,53 @@
             match.image_dominant_color);
   EXPECT_EQ("gs_ssp=abc", match.search_terms_args->additional_query_params);
 }
+
+TEST_F(BaseSearchProviderTest, AnswerAndImageOnlyPopulatedForGoogle) {
+  std::u16string query = u"weather";
+  omnibox::RichAnswerTemplate answer_template;
+  answer_template.add_answers()->mutable_headline()->set_text("72°F");
+
+  omnibox::EntityInfo entity_info;
+  entity_info.set_image_url("https://example.com/image.png");
+  entity_info.set_dominant_color("#ffffff");
+
+  SearchSuggestionParser::SuggestResult result(
+      query, AutocompleteMatchType::SEARCH_SUGGEST, omnibox::TYPE_QUERY,
+      /*subtypes=*/{}, /*from_keyword=*/false,
+      /*navigational_intent=*/omnibox::NAV_INTENT_NONE,
+      /*relevance=*/1300, /*relevance_from_server=*/true,
+      /*input_text=*/query);
+  result.SetRichAnswerTemplate(answer_template);
+  result.SetEntityInfo(entity_info);
+
+  // 1. Non-Google search engine: fields should NOT be populated.
+  {
+    TemplateURLData non_google_data;
+    non_google_data.SetURL("https://evil.com/search?q={searchTerms}");
+    auto non_google_turl = std::make_unique<TemplateURL>(non_google_data);
+    TestBaseSearchProvider::MatchMap map;
+    provider_->AddMatchToMap(
+        result, AutocompleteInput(), non_google_turl.get(),
+        client_->GetTemplateURLService()->search_terms_data(),
+        TemplateURLRef::NO_SUGGESTION_CHOSEN, false, false, &map);
+    ASSERT_EQ(1U, map.size());
+    EXPECT_FALSE(map.begin()->second.answer_template.has_value());
+    EXPECT_TRUE(map.begin()->second.image_url.is_empty());
+  }
+
+  // 2. Google search engine: fields SHOULD be populated.
+  {
+    TemplateURLData google_data;
+    google_data.SetURL("https://www.google.com/search?q={searchTerms}");
+    auto google_turl = std::make_unique<TemplateURL>(google_data);
+    TestBaseSearchProvider::MatchMap map;
+    provider_->AddMatchToMap(
+        result, AutocompleteInput(), google_turl.get(),
+        client_->GetTemplateURLService()->search_terms_data(),
+        TemplateURLRef::NO_SUGGESTION_CHOSEN, false, false, &map);
+    ASSERT_EQ(1U, map.size());
+    EXPECT_TRUE(map.begin()->second.answer_template.has_value());
+    EXPECT_EQ("https://example.com/image.png",
+              map.begin()->second.image_url.spec());
+  }
+}
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.