CVE-2026-113208
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcomponents/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc |
modified | |
ifcomponents/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc |
modified |
Files Changed
components/touch_to_search/core/DEPScomponents/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cccomponents/touch_to_search/core/browser/contextual_search_delegate_impl.cc
Patch
From bd7880688f7db51b5bfd7de0f66b55e749a6716f Mon Sep 17 00:00:00 2001 From: Min Qin <[email protected]> Date: Tue, 15 Sep 2026 18:15:34 -0700 Subject: [PATCH] Strip discourse context header on cross-origin redirects Add a redirect callback to the ContextualSearchDelegateImpl URL loader to manage header persistence across navigation hops. When a redirect targets a different origin, the X-Additional-Discourse-Context header is explicitly added to the list of headers to remove. The redirect handler also delegates to RemoveVariationsHeaderIfNeeded to maintain consistent variations header handling across all default search engine configurations. Bug: 513774784 TAG=agy Change-Id: I5cc2f92d1934a3299f52fd0cd278a9a230a7ae2b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8410918 Reviewed-by: Patrick Noland <[email protected]> Commit-Queue: Min Qin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1698553} --- diff --git a/components/touch_to_search/core/browser/contextual_search_delegate_impl.cc b/components/touch_to_search/core/browser/contextual_search_delegate_impl.cc index d231462..833d2fa1 100644 --- a/components/touch_to_search/core/browser/contextual_search_delegate_impl.cc +++ b/components/touch_to_search/core/browser/contextual_search_delegate_impl.cc @@ -33,12 +33,14 @@ #include "net/http/http_request_headers.h" #include "net/http/http_response_headers.h" #include "net/http/http_status_code.h" +#include "net/url_request/redirect_info.h" #include "services/network/public/cpp/resource_request.h" #include "services/network/public/cpp/shared_url_loader_factory.h" #include "services/network/public/cpp/simple_url_loader.h" #include "services/network/public/mojom/url_response_head.mojom.h" #include "third_party/blink/public/mojom/frame/frame.mojom.h" #include "url/gurl.h" +#include "url/origin.h" using content::RenderFrameHost; @@ -110,6 +112,20 @@ return headers; } +// Handles header removal when following redirects. +void OnRedirect(const GURL& url_before_redirect, + const net::RedirectInfo& redirect_info, + const network::mojom::URLResponseHead& response_head, + std::vector<std::string>* to_be_removed_headers) { + variations::RemoveVariationsHeaderIfNeeded(redirect_info, response_head, + variations::InIncognito::kNo, + to_be_removed_headers); + + if (!url::IsSameOriginWith(url_before_redirect, redirect_info.new_url)) { + to_be_removed_headers->push_back(kDiscourseContextHeaderName); + } +} + } // namespace // Handles tasks for the ContextualSearchManager in a separable, testable way. @@ -297,6 +313,8 @@ // point. traffic_annotation); + url_loader_->SetOnRedirectCallback(base::BindRepeating(&OnRedirect)); + url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( url_loader_factory_.get(), base::BindOnce(&ContextualSearchDelegateImpl::OnUrlLoadComplete, diff --git a/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc b/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc index 51f9b8d..b55ab6b 100644 --- a/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc +++ b/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc @@ -26,6 +26,8 @@ #include "components/touch_to_search/core/browser/resolved_search_term.h" #include "components/touch_to_search/core/proto/client_discourse_context.pb.h" #include "components/variations/scoped_variations_ids_provider.h" +#include "components/variations/variations_ids_provider.h" +#include "net/url_request/redirect_info.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/public/mojom/url_response_head.mojom.h" #include "services/network/test/test_url_loader_factory.h" @@ -173,6 +175,20 @@ base::Unretained(this))); } + void SetContextualSearchUrlOrigin(const std::string& origin) { + TemplateURLData data; + data.SetURL(origin + "url?bar={searchTerms}"); + data.contextual_search_url = + origin + + "_/contextualsearch?" + "{google:contextualSearchVersion}{google:contextualSearchContextData}"; + TemplateURLService& template_url_service = + *search_engines_test_environment_.template_url_service(); + TemplateURL* template_url = + template_url_service.Add(std::make_unique<TemplateURL>(data)); + template_url_service.SetUserSelectedDefaultSearchProvider(template_url); + } + void SetResponseStringAndSimulateResponse(const std::string& selected_text, const std::string& mentions_start, const std::string& mentions_end) { @@ -274,7 +290,8 @@ std::unique_ptr<ContextualSearchDelegateImpl> delegate_; std::unique_ptr<ContextualSearchContext> test_context_; - network::TestURLLoaderFactory test_url_loader_factory_; + network::TestURLLoaderFactory test_url_loader_factory_{ + /*observe_loader_requests=*/true}; private: void recordSearchTermResolutionResponse( @@ -759,3 +776,190 @@ SimulateResponseReturned(response); EXPECT_TRUE(related_searches_json().empty()); } + +namespace { + +constexpr char kSampleBasePageUrl[] = "https://example.com/page?id=123"; +constexpr char16_t kSampleSurroundings[] = + u"Sample context text surrounding selection"; +constexpr char kCrossOriginRedirectTarget[] = + "https://target.example.org/destination"; +constexpr char kSameOriginRedirectTargetFoobar[] = + "https://foobar.com/_/contextualsearch_v2"; +constexpr char kSameOriginRedirectTargetGoogle[] = + "https://www.google.com/_/contextualsearch_v2"; + +} // namespace + +TEST_F(ContextualSearchDelegateImplTest, + DiscourseContextHeaderRemovedOnCrossOriginRedirect_NonGoogleDSE) { + test_context_ = + MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8"); + test_context_->SetSelectionSurroundings(0, 6, + std::u16string(kSampleSurroundings)); + CallResolveSearchTermFromContext(test_context_->AsWeakPtr()); + + base::RunLoop().RunUntilIdle(); + auto* pending = test_url_loader_factory_.GetPendingRequest(0); + ASSERT_TRUE(pending); + ASSERT_TRUE(pending->test_url_loader); + + discourse_context::ClientDiscourseContext cdc = + GetDiscourseContextFromRequest(); + ASSERT_GT(cdc.display_size(), 0); + EXPECT_EQ(kSampleBasePageUrl, cdc.display(0).uri()); + EXPECT_EQ(base::UTF16ToUTF8(kSampleSurroundings), + cdc.display(0).selection().content()); + + net::RedirectInfo redirect; + redirect.status_code = 302; + redirect.new_method = "GET"; + redirect.new_url = GURL(kCrossOriginRedirectTarget); + pending->client->OnReceiveRedirect(redirect, + network::mojom::URLResponseHead::New()); + base::RunLoop().RunUntilIdle(); + + const auto& follow_params = + pending->test_url_loader->follow_redirect_params(); + ASSERT_EQ(1u, follow_params.size()); + + bool stripped_discourse_context = false; + for (const auto& h : follow_params[0].headers_update_params.removed_headers) { + if (h == kDiscourseContextHeaderName) { + stripped_discourse_context = true; + } + } + EXPECT_TRUE(stripped_discourse_context); +} + +TEST_F(ContextualSearchDelegateImplTest, + DiscourseContextHeaderRemovedOnCrossOriginRedirect_GoogleDSE) { + ASSERT_EQ(variations::VariationsIdsProvider::ForceIdsResult::SUCCESS, + variations::VariationsIdsProvider::GetInstance() + ->ForceVariationIdsForTesting({"12"}, "")); + + SetContextualSearchUrlOrigin("https://www.google.com/"); + + test_context_ = + MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8"); + test_context_->SetSelectionSurroundings(0, 6, + std::u16string(kSampleSurroundings)); + CallResolveSearchTermFromContext(test_context_->AsWeakPtr()); + + base::RunLoop().RunUntilIdle(); + auto* pending = test_url_loader_factory_.GetPendingRequest(0); + ASSERT_TRUE(pending); + ASSERT_TRUE(pending->test_url_loader); + + ASSERT_TRUE(pending->request.cors_exempt_headers.HasHeader("X-Client-Data")); + discourse_context::ClientDiscourseContext cdc = + GetDiscourseContextFromRequest(); + ASSERT_GT(cdc.display_size(), 0); + EXPECT_EQ(kSampleBasePageUrl, cdc.display(0).uri()); + EXPECT_EQ(base::UTF16ToUTF8(kSampleSurroundings), + cdc.display(0).selection().content()); + + net::RedirectInfo redirect; + redirect.status_code = 302; + redirect.new_method = "GET"; + redirect.new_url = GURL(kCrossOriginRedirectTarget); + pending->client->OnReceiveRedirect(redirect, + network::mojom::URLResponseHead::New()); + base::RunLoop().RunUntilIdle(); + + const auto& follow_params = + pending->test_url_loader->follow_redirect_params(); + ASSERT_EQ(1u, follow_params.size()); + + bool stripped_x_client_data = false; + bool stripped_discourse_context = false; + for (const auto& h : follow_params[0].headers_update_params.removed_headers) { + if (h == "X-Client-Data") { + stripped_x_client_data = true; + } + if (h == kDiscourseContextHeaderName) { + stripped_discourse_context = true; + } + } + EXPECT_TRUE(stripped_x_client_data); + EXPECT_TRUE(stripped_discourse_context); +} + +TEST_F(ContextualSearchDelegateImplTest, + DiscourseContextHeaderPreservedOnSameOriginRedirect_NonGoogleDSE) { + test_context_ = + MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8"); + test_context_->SetSelectionSurroundings(0, 6, + std::u16string(kSampleSurroundings)); + CallResolveSearchTermFromContext(test_context_->AsWeakPtr()); + + base::RunLoop().RunUntilIdle(); + auto* pending = test_url_loader_factory_.GetPendingRequest(0); + ASSERT_TRUE(pending); + ASSERT_TRUE(pending->test_url_loader); + + net::RedirectInfo redirect; + redirect.status_code = 302; + redirect.new_method = "GET"; + redirect.new_url = GURL(kSameOriginRedirectTargetFoobar); + pending->client->OnReceiveRedirect(redirect, + network::mojom::URLResponseHead::New()); + base::RunLoop().RunUntilIdle(); + + const auto& follow_params = + pending->test_url_loader->follow_redirect_params(); + ASSERT_EQ(1u, follow_params.size()); + + bool stripped_discourse_context = false; + for (const auto& h : follow_params[0].headers_update_params.removed_headers) { + if (h == kDiscourseContextHeaderName) { + stripped_discourse_context = true; + } + } + EXPECT_FALSE(stripped_discourse_context); +} + +TEST_F(ContextualSearchDelegateImplTest, + DiscourseContextHeaderPreservedOnSameOriginRedirect_GoogleDSE) { + ASSERT_EQ(variations::VariationsIdsProvider::ForceIdsResult::SUCCESS, + variations::VariationsIdsProvider::GetInstance() + ->ForceVariationIdsForTesting({"12"}, "")); + + SetContextualSearchUrlOrigin("https://www.google.com/"); + + test_context_ = + MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8"); + test_context_->SetSelectionSurroundings(0, 6, + std::u16string(kSampleSurroundings)); + CallResolveSearchTermFromContext(test_context_->AsWeakPtr()); + + base::RunLoop().RunUntilIdle(); + auto* pending = test_url_loader_factory_.GetPendingRequest(0); + ASSERT_TRUE(pending); + ASSERT_TRUE(pending->test_url_loader); + + net::RedirectInfo redirect; + redirect.status_code = 302; + redirect.new_method = "GET"; + redirect.new_url = GURL(kSameOriginRedirectTargetGoogle); + pending->client->OnReceiveRedirect(redirect, + network::mojom::URLResponseHead::New()); + base::RunLoop().RunUntilIdle(); + + const auto& follow_params = + pending->test_url_loader->follow_redirect_params(); + ASSERT_EQ(1u, follow_params.size()); + + bool stripped_x_client_data = false; + bool stripped_discourse_context = false; + for (const auto& h : follow_params[0].headers_update_params.removed_headers) { + if (h == "X-Client-Data") { + stripped_x_client_data = true; + } + if (h == kDiscourseContextHeaderName) { + stripped_discourse_context = true; + } + } + EXPECT_FALSE(stripped_x_client_data); + EXPECT_FALSE(stripped_discourse_context); +} diff --git a/components/touch_to_search/core/DEPS b/components/touch_to_search/core/DEPS index dc5f1ff0..7071e5c 100644 --- a/components/touch_to_search/core/DEPS +++ b/components/touch_to_search/core/DEPS @@ -1,4 +1,5 @@ include_rules = [ + "+net/url_request/redirect_info.h", "+components/language_detection/core", "+components/metrics", "+components/search_engines",
Regression Test / PoC
diff --git a/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc b/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc
index 51f9b8d..b55ab6b 100644
--- a/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc
+++ b/components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc
@@ -26,6 +26,8 @@
#include "components/touch_to_search/core/browser/resolved_search_term.h"
#include "components/touch_to_search/core/proto/client_discourse_context.pb.h"
#include "components/variations/scoped_variations_ids_provider.h"
+#include "components/variations/variations_ids_provider.h"
+#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
@@ -173,6 +175,20 @@
base::Unretained(this)));
}
+ void SetContextualSearchUrlOrigin(const std::string& origin) {
+ TemplateURLData data;
+ data.SetURL(origin + "url?bar={searchTerms}");
+ data.contextual_search_url =
+ origin +
+ "_/contextualsearch?"
+ "{google:contextualSearchVersion}{google:contextualSearchContextData}";
+ TemplateURLService& template_url_service =
+ *search_engines_test_environment_.template_url_service();
+ TemplateURL* template_url =
+ template_url_service.Add(std::make_unique<TemplateURL>(data));
+ template_url_service.SetUserSelectedDefaultSearchProvider(template_url);
+ }
+
void SetResponseStringAndSimulateResponse(const std::string& selected_text,
const std::string& mentions_start,
const std::string& mentions_end) {
@@ -274,7 +290,8 @@
std::unique_ptr<ContextualSearchDelegateImpl> delegate_;
std::unique_ptr<ContextualSearchContext> test_context_;
- network::TestURLLoaderFactory test_url_loader_factory_;
+ network::TestURLLoaderFactory test_url_loader_factory_{
+ /*observe_loader_requests=*/true};
private:
void recordSearchTermResolutionResponse(
@@ -759,3 +776,190 @@
SimulateResponseReturned(response);
EXPECT_TRUE(related_searches_json().empty());
}
+
+namespace {
+
+constexpr char kSampleBasePageUrl[] = "https://example.com/page?id=123";
+constexpr char16_t kSampleSurroundings[] =
+ u"Sample context text surrounding selection";
+constexpr char kCrossOriginRedirectTarget[] =
+ "https://target.example.org/destination";
+constexpr char kSameOriginRedirectTargetFoobar[] =
+ "https://foobar.com/_/contextualsearch_v2";
+constexpr char kSameOriginRedirectTargetGoogle[] =
+ "https://www.google.com/_/contextualsearch_v2";
+
+} // namespace
+
+TEST_F(ContextualSearchDelegateImplTest,
+ DiscourseContextHeaderRemovedOnCrossOriginRedirect_NonGoogleDSE) {
+ test_context_ =
+ MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8");
+ test_context_->SetSelectionSurroundings(0, 6,
+ std::u16string(kSampleSurroundings));
+ CallResolveSearchTermFromContext(test_context_->AsWeakPtr());
+
+ base::RunLoop().RunUntilIdle();
+ auto* pending = test_url_loader_factory_.GetPendingRequest(0);
+ ASSERT_TRUE(pending);
+ ASSERT_TRUE(pending->test_url_loader);
+
+ discourse_context::ClientDiscourseContext cdc =
+ GetDiscourseContextFromRequest();
+ ASSERT_GT(cdc.display_size(), 0);
+ EXPECT_EQ(kSampleBasePageUrl, cdc.display(0).uri());
+ EXPECT_EQ(base::UTF16ToUTF8(kSampleSurroundings),
+ cdc.display(0).selection().content());
+
+ net::RedirectInfo redirect;
+ redirect.status_code = 302;
+ redirect.new_method = "GET";
+ redirect.new_url = GURL(kCrossOriginRedirectTarget);
+ pending->client->OnReceiveRedirect(redirect,
+ network::mojom::URLResponseHead::New());
+ base::RunLoop().RunUntilIdle();
+
+ const auto& follow_params =
+ pending->test_url_loader->follow_redirect_params();
+ ASSERT_EQ(1u, follow_params.size());
+
+ bool stripped_discourse_context = false;
+ for (const auto& h : follow_params[0].headers_update_params.removed_headers) {
+ if (h == kDiscourseContextHeaderName) {
+ stripped_discourse_context = true;
+ }
+ }
+ EXPECT_TRUE(stripped_discourse_context);
+}
+
+TEST_F(ContextualSearchDelegateImplTest,
+ DiscourseContextHeaderRemovedOnCrossOriginRedirect_GoogleDSE) {
+ ASSERT_EQ(variations::VariationsIdsProvider::ForceIdsResult::SUCCESS,
+ variations::VariationsIdsProvider::GetInstance()
+ ->ForceVariationIdsForTesting({"12"}, ""));
+
+ SetContextualSearchUrlOrigin("https://www.google.com/");
+
+ test_context_ =
+ MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8");
+ test_context_->SetSelectionSurroundings(0, 6,
+ std::u16string(kSampleSurroundings));
+ CallResolveSearchTermFromContext(test_context_->AsWeakPtr());
+
+ base::RunLoop().RunUntilIdle();
+ auto* pending = test_url_loader_factory_.GetPendingRequest(0);
+ ASSERT_TRUE(pending);
+ ASSERT_TRUE(pending->test_url_loader);
+
+ ASSERT_TRUE(pending->request.cors_exempt_headers.HasHeader("X-Client-Data"));
+ discourse_context::ClientDiscourseContext cdc =
+ GetDiscourseContextFromRequest();
+ ASSERT_GT(cdc.display_size(), 0);
+ EXPECT_EQ(kSampleBasePageUrl, cdc.display(0).uri());
+ EXPECT_EQ(base::UTF16ToUTF8(kSampleSurroundings),
+ cdc.display(0).selection().content());
+
+ net::RedirectInfo redirect;
+ redirect.status_code = 302;
+ redirect.new_method = "GET";
+ redirect.new_url = GURL(kCrossOriginRedirectTarget);
+ pending->client->OnReceiveRedirect(redirect,
+ network::mojom::URLResponseHead::New());
+ base::RunLoop().RunUntilIdle();
+
+ const auto& follow_params =
+ pending->test_url_loader->follow_redirect_params();
+ ASSERT_EQ(1u, follow_params.size());
+
+ bool stripped_x_client_data = false;
+ bool stripped_discourse_context = false;
+ for (const auto& h : follow_params[0].headers_update_params.removed_headers) {
+ if (h == "X-Client-Data") {
+ stripped_x_client_data = true;
+ }
+ if (h == kDiscourseContextHeaderName) {
+ stripped_discourse_context = true;
+ }
+ }
+ EXPECT_TRUE(stripped_x_client_data);
+ EXPECT_TRUE(stripped_discourse_context);
+}
+
+TEST_F(ContextualSearchDelegateImplTest,
+ DiscourseContextHeaderPreservedOnSameOriginRedirect_NonGoogleDSE) {
+ test_context_ =
+ MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8");
+ test_context_->SetSelectionSurroundings(0, 6,
+ std::u16string(kSampleSurroundings));
+ CallResolveSearchTermFromContext(test_context_->AsWeakPtr());
+
+ base::RunLoop().RunUntilIdle();
+ auto* pending = test_url_loader_factory_.GetPendingRequest(0);
+ ASSERT_TRUE(pending);
+ ASSERT_TRUE(pending->test_url_loader);
+
+ net::RedirectInfo redirect;
+ redirect.status_code = 302;
+ redirect.new_method = "GET";
+ redirect.new_url = GURL(kSameOriginRedirectTargetFoobar);
+ pending->client->OnReceiveRedirect(redirect,
+ network::mojom::URLResponseHead::New());
+ base::RunLoop().RunUntilIdle();
+
+ const auto& follow_params =
+ pending->test_url_loader->follow_redirect_params();
+ ASSERT_EQ(1u, follow_params.size());
+
+ bool stripped_discourse_context = false;
+ for (const auto& h : follow_params[0].headers_update_params.removed_headers) {
+ if (h == kDiscourseContextHeaderName) {
+ stripped_discourse_context = true;
+ }
+ }
+ EXPECT_FALSE(stripped_discourse_context);
+}
+
+TEST_F(ContextualSearchDelegateImplTest,
+ DiscourseContextHeaderPreservedOnSameOriginRedirect_GoogleDSE) {
+ ASSERT_EQ(variations::VariationsIdsProvider::ForceIdsResult::SUCCESS,
+ variations::VariationsIdsProvider::GetInstance()
+ ->ForceVariationIdsForTesting({"12"}, ""));
+
+ SetContextualSearchUrlOrigin("https://www.google.com/");
+
+ test_context_ =
+ MakeTestContext(std::string(), GURL(kSampleBasePageUrl), "utf-8");
+ test_context_->SetSelectionSurroundings(0, 6,
+ std::u16string(kSampleSurroundings));
+ CallResolveSearchTermFromContext(test_context_->AsWeakPtr());
+
+ base::RunLoop().RunUntilIdle();
+ auto* pending = test_url_loader_factory_.GetPendingRequest(0);
+ ASSERT_TRUE(pending);
+ ASSERT_TRUE(pending->test_url_loader);
+
+ net::RedirectInfo redirect;
+ redirect.status_code = 302;
+ redirect.new_method = "GET";
+ redirect.new_url = GURL(kSameOriginRedirectTargetGoogle);
+ pending->client->OnReceiveRedirect(redirect,
+ network::mojom::URLResponseHead::New());
+ base::RunLoop().RunUntilIdle();
+
+ const auto& follow_params =
+ pending->test_url_loader->follow_redirect_params();
+ ASSERT_EQ(1u, follow_params.size());
+
+ bool stripped_x_client_data = false;
+ bool stripped_discourse_context = false;
+ for (const auto& h : follow_params[0].headers_update_params.removed_headers) {
+ if (h == "X-Client-Data") {
+ stripped_x_client_data = true;
+ }
+ if (h == kDiscourseContextHeaderName) {
+ stripped_discourse_context = true;
+ }
+ }
+ EXPECT_FALSE(stripped_x_client_data);
+ EXPECT_FALSE(stripped_discourse_context);
+}
Original Bug Report
PII leak of page URL and content via X-Additional-Discourse-Context header redirects
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The sensitive X-Additional-Discourse-Context header, used in Contextual Search and Partial Translate, is not stripped during cross-origin redirects. This potentially allows unauthorized origins to capture a user’s current page URL and surrounding text context.
Affected files:
components/touch_to_search/core/browser/contextual_search_delegate_impl.cccomponents/variations/net/variations_http_headers.ccservices/network/public/cpp/simple_url_loader.cc
Estimated timestamp from git blame: 2018-09-25
Summary
A potential information disclosure vulnerability exists in the Contextual Search and Partial Translate features. When a search term resolution request is made, a sensitive header named X-Additional-Discourse-Context is attached to the request. This header contains the full URL of the currently visited page and approximately 1.5 KB of text surrounding the user’s selection (which can include content from the page DOM or iframes).
If the resolution endpoint issues a redirect to a different origin, this sensitive header is not stripped by the browser and is forwarded to the new destination. This behavior violates the security contract that sensitive page context should only be sent to authorized Google-owned services.
Root Cause Analysis
The issue stems from how the SimpleURLLoader is configured in ContextualSearchDelegateImpl.
- Incomplete Header Stripping:
ContextualSearchDelegateImplcreates its loader usingvariations::CreateSimpleURLLoaderWithVariationsHeaderUnknownSignedIn(contextual_search_delegate_impl.cc:294). This helper installs a redirect callback (RemoveVariationsHeaderIfNeeded) that specifically removes only theX-Client-Datavariations header during cross-origin redirects (variations_http_headers.cc:359). TheX-Additional-Discourse-Contextheader is ignored and persists in the request. - Lack of Custom Redirect Handling: The
ResourceRequestused defaults toRedirectMode::kFollow.ContextualSearchDelegateImpldoes not provide its own redirect callback to ensure its specific sensitive headers are removed when leaving a trusted origin. - Third-Party Search Providers: If a user has configured a non-Google search provider as their default, the variations helper does not install any redirect callback at all (variations_http_headers.cc:379), causing all headers to survive every redirect hop.
Potential Attack Scenario
- A user on Android (with Touch to Search enabled) or Desktop (Partial Translate) browses a sensitive page (e.g., a private message or a site containing PII).
- The user triggers a Contextual Search resolve request (e.g., via a long-press).
- The resolution request is sent to an endpoint (e.g.,
https://www.google.com/_/contextualsearch...) with theX-Additional-Discourse-Contextheader. - If the endpoint redirects to an attacker-controlled origin (e.g., via an open redirect on a Google host or if the user uses a malicious search provider), the
SimpleURLLoaderfollows the redirect. - The attacker receives the
X-Additional-Discourse-Contextheader, decodes the Base64 content, and gains access to the user’s current URL and surrounding page text.
Suggested Fix
ContextualSearchDelegateImpl should explicitly manage the lifetime of its sensitive headers during redirects. This can be achieved by providing a custom OnRedirectCallback to the SimpleURLLoader that ensures X-Additional-Discourse-Context is added to the removed_headers vector whenever a redirect to a non-authorized or cross-origin URL is detected.
Note: This analysis was performed using static code review and automated tools; a functional proof-of-concept has not yet been executed.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.
- https://source.chromium.org/chromium/chromium/src/+/main:components/touch_to_search/core/browser/contextual_search_delegate_impl.cc;l=294
- https://source.chromium.org/chromium/chromium/src/+/main:components/variations/net/variations_http_headers.cc;l=359
- https://source.chromium.org/chromium/chromium/src/+/main:components/variations/net/variations_http_headers.cc;l=379
- https://www.google.com/_/contextualsearch