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 <ananyaseelam@google.com>
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 <ananyaseelam@google.com>
Reviewed-by: Marlon Facey <mfacey@chromium.org>
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());
+ }
+}