Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ContextualSearch
DescriptionUse after free in ContextualSearch
ComponentContextualSearch
Sub-componentUI>Browser>Search>ContextualSearch
Bug ClassUAF
Tracker513774784
Fix commitbd7880688f7d (chromium/src) +224/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc
modified
if
components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc
modified

Files Changed

  • components/touch_to_search/core/DEPS
  • components/touch_to_search/core/browser/contextual_search_delegate_impl_unittest.cc
  • components/touch_to_search/core/browser/contextual_search_delegate_impl.cc
From bd7880688f7db51b5bfd7de0f66b55e749a6716f Mon Sep 17 00:00:00 2001
From: Min Qin <qinmin@chromium.org>
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 <pnoland@chromium.org>
Commit-Queue: Min Qin <qinmin@chromium.org>
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",
Loading diff…

Regression Test / PoC

shipped with the fix
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);
+}
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • components/variations/net/variations_http_headers.cc
  • services/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.

  1. Incomplete Header Stripping: ContextualSearchDelegateImpl creates its loader using variations::CreateSimpleURLLoaderWithVariationsHeaderUnknownSignedIn (contextual_search_delegate_impl.cc:294). This helper installs a redirect callback (RemoveVariationsHeaderIfNeeded) that specifically removes only the X-Client-Data variations header during cross-origin redirects (variations_http_headers.cc:359). The X-Additional-Discourse-Context header is ignored and persists in the request.
  2. Lack of Custom Redirect Handling: The ResourceRequest used defaults to RedirectMode::kFollow. ContextualSearchDelegateImpl does not provide its own redirect callback to ensure its specific sensitive headers are removed when leaving a trusted origin.
  3. 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

  1. 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).
  2. The user triggers a Contextual Search resolve request (e.g., via a long-press).
  3. The resolution request is sent to an endpoint (e.g., https://www.google.com/_/contextualsearch...) with the X-Additional-Discourse-Context header.
  4. 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 SimpleURLLoader follows the redirect.
  5. The attacker receives the X-Additional-Discourse-Context header, 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.

View on issue tracker