CVE-2026-93384
Overview
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 anAutocompleteMatchdisplayed 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.
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
- 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. - Malicious suggest response
the engine returns a suggest result whose
entity_info().image_url()/suggest_template_infoimage URL /answer_templatereferences an attacker-chosen target (e.g. an internal or private-network host). - Unchecked field population
pre-patch,
CreateSearchSuggestioncopiesimage_url,image_dominant_color,answer_template, andanswer_typeinto the match with nois_googlecheck, so the untrusted URL is accepted. - 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/omnibox/browser/base_search_provider.cc |
modified | |
TEST_Fchrome/browser/autocomplete/search_provider_unittest.cc |
modified | |
TEST_Fcomponents/omnibox/browser/base_search_provider_unittest.cc |
modified |
Files Changed
components/omnibox/browser/base_search_provider.ccchrome/browser/autocomplete/search_provider_unittest.cccomponents/omnibox/browser/base_search_provider_unittest.cc
Audit Directions
- Trust-gating of server-supplied URLsaudit 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 functionwhere 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 surfacereview all consumers of
entity_info,suggest_template_info, andanswer_templatefrom non-Google engines for SSRF, PNA-bypass, or content-injection risk, since these are attacker-controllable whenever the default/keyword engine is untrusted.
Patch
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()); + } +}
Regression Test / PoC
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());
+ }
+}