Low watch CVSS 6.5 chrome Info Disclosure 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Networking
DescriptionInformation leak in Networking
ComponentNetworking
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker502077689
Fix commit724dcd975fc3 (chromium/src) +239/-70
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-13 (per vendor advisory)
Fix landed2026-08-21 (public git)
Disclosed2026-09-22
Patch gap32 days — fix public in source before the advisory

Background

`GoogleURLLoaderThrottle`
a blink::URLLoaderThrottle that Chrome attaches to network requests to inject Google-specific headers such as SafeSearch and app-restriction controls.
`safe_search_api::kGoogleAppsAllowedDomains`
the X-GoogApps-Allowed-Domains CORS-exempt request header that carries the enterprise-configured list of Google Workspace domains a managed device is restricted to.
`allowed_domains_for_apps`
the field on dynamic_params_ holding that policy-configured domain list, which is only meaningful to Google servers.
`HttpRequestHeadersUpdateParams`
the structure whose modified_cors_exempt_headers and removed_headers fields let a throttle add or strip headers when a request is redirected.

Root Cause Analysis

When a request started against a Google domain, GoogleURLLoaderThrottle set the safe_search_api::kGoogleAppsAllowedDomains CORS-exempt header carrying dynamic_params_->allowed_domains_for_apps. On redirect, the original code only re-set this header when redirect_info->new_url still matched DomainIs("google.com"), and did nothing otherwise. Because the header had already been applied to the underlying request, taking no action on a redirect to a non-Google origin left the header in place, so it was carried across the cross-origin hop and sent to the redirect target. The invariant that the enterprise domain list must only ever reach Google endpoints was therefore violated.

The fix adds an else branch that pushes safe_search_api::kGoogleAppsAllowedDomains onto headers_update_params->removed_headers whenever the redirect target is not a Google domain, guaranteeing the header is stripped before the request leaves Google origins.

Key insight
The single mistake was treating “add the header” and “leave the header untouched” as the only two redirect outcomes, forgetting that an already-attached CORS-exempt header persists across redirects unless explicitly removed; the fix makes non-Google redirect targets an explicit removal case instead of a no-op.

Attack Path

  1. Managed configuration A device under enterprise policy has allowed_domains_for_apps populated, so requests to Google domains carry the safe_search_api::kGoogleAppsAllowedDomains header.
  2. Attacker-controlled redirect The user is induced to load a Google URL that server-side redirects, or an attacker positions a resource whose Google endpoint issues a 3xx redirect to a non-Google origin they control.
  3. Header retained on redirect During WillRedirectRequest, the old code takes the no-op path for the non-Google redirect_info->new_url, leaving the previously attached header on the request.
  4. Cross-origin leak The redirected request is sent to the attacker’s origin with the X-GoogApps-Allowed-Domains value intact, disclosing the enterprise-configured domain list.

Impact Assessment

An attacker who can trigger a redirect from a Google origin to an origin they control receives the safe_search_api::kGoogleAppsAllowedDomains header, leaking the enterprise’s configured Google Workspace domain allowlist to an untrusted server. The disclosure happens in the network request context of a managed Chrome client and requires that the device have allowed_domains_for_apps configured and that the victim follow a cross-origin redirect chain originating from a Google domain.

Changed Functions

FunctionChangeNotes
MockThrottleDelegate
chrome/common/google_url_loader_throttle_unittest.cc
modified
GoogleURLLoaderThrottleTest
chrome/common/google_url_loader_throttle_unittest.cc
modified
if
chrome/common/google_url_loader_throttle_unittest.cc
modified
GoogleURLLoaderThrottleBoundSessionCredentialsTest
chrome/common/google_url_loader_throttle_unittest.cc
modified
for
chrome/common/google_url_loader_throttle_unittest.cc
modified
TEST_F
chrome/common/google_url_loader_throttle_unittest.cc
modified

Files Changed

  • chrome/common/google_url_loader_throttle_unittest.cc
  • chrome/common/google_url_loader_throttle.cc

Audit Directions

  • Redirect header persistence
    Review every throttle or delegate that adds request headers on start but only conditionally re-applies them on redirect, since an untouched header survives cross-origin hops unless placed in removed_headers.
  • CORS-exempt and sensitive headers
    Audit code that sets modified_cors_exempt_headers or other privileged headers to confirm each has a matching strip path when the destination origin changes.
  • Domain-scoped data
    Trace policy- or account-scoped values like allowed_domains_for_apps to ensure they are gated by a DomainIs (or equivalent origin) check on both the initial request and every redirect target.
From 724dcd975fc3b693ace5a67fe134c21d2e405923 Mon Sep 17 00:00:00 2001
From: Sebastien Lalancette <[email protected]>
Date: Fri, 21 Aug 2026 13:13:26 -0700
Subject: [PATCH] Remove X-GoogApps special header on redirect to non-Google domains

Updated GoogleURLLoaderThrottle to explicitly remove the
safe_search_api::kGoogleAppsAllowedDomains header when redirecting to a
non-Google domain if allowed_domains_for_apps is configured. This
prevents the header from leaking across cross-origin redirects. Also
added unit tests in GoogleURLLoaderThrottleTest to verify header
addition and removal across start and redirect requests.

Bug: 502077689
Change-Id: I28f5df2bc312ba3e93f16ed2ab93c5b53d825b1a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8263292
Reviewed-by: Theresa Wellington <[email protected]>
Reviewed-by: Zonghan Xu <[email protected]>
Commit-Queue: Sebastien Lalancette <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1684162}
---
diff --git a/chrome/common/google_url_loader_throttle.cc b/chrome/common/google_url_loader_throttle.cc
index 2c04ddf..3333574 100644
--- a/chrome/common/google_url_loader_throttle.cc
+++ b/chrome/common/google_url_loader_throttle.cc
@@ -264,11 +264,15 @@
             dynamic_params_->youtube_restrict));
   }
 
-  if (!dynamic_params_->allowed_domains_for_apps.empty() &&
-      redirect_info->new_url.DomainIs("google.com")) {
-    headers_update_params->modified_cors_exempt_headers.SetHeader(
-        safe_search_api::kGoogleAppsAllowedDomains,
-        dynamic_params_->allowed_domains_for_apps);
+  if (!dynamic_params_->allowed_domains_for_apps.empty()) {
+    if (redirect_info->new_url.DomainIs("google.com")) {
+      headers_update_params->modified_cors_exempt_headers.SetHeader(
+          safe_search_api::kGoogleAppsAllowedDomains,
+          dynamic_params_->allowed_domains_for_apps);
+    } else {
+      headers_update_params->removed_headers.push_back(
+          safe_search_api::kGoogleAppsAllowedDomains);
+    }
   }
 
 #if BUILDFLAG(IS_ANDROID)
diff --git a/chrome/common/google_url_loader_throttle_unittest.cc b/chrome/common/google_url_loader_throttle_unittest.cc
index 531b8af..b35b9ddd 100644
--- a/chrome/common/google_url_loader_throttle_unittest.cc
+++ b/chrome/common/google_url_loader_throttle_unittest.cc
@@ -6,14 +6,19 @@
 
 #include <memory>
 #include <string_view>
+#include <vector>
 
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/time/time.h"
+#include "build/build_config.h"
 #include "build/buildflag.h"
 #include "chrome/common/renderer_configuration.mojom.h"
+#include "components/safe_search_api/safe_search_util.h"
+#include "components/signin/public/base/signin_buildflags.h"
 #include "components/signin/public/base/signin_switches.h"
+#include "net/url_request/redirect_info.h"
 #include "services/network/public/cpp/http_request_headers_update_params.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/cpp/url_loader_completion_status.h"
@@ -24,13 +29,180 @@
 #include "third_party/blink/public/common/loader/url_loader_throttle.h"
 #include "url/origin.h"
 
-// This file only contains tests relevant to the bound session credentials
-// feature.
 #if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
 #include "chrome/common/bound_session_request_throttled_handler.h"
+#endif  // BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
 
 namespace {
 
+class MockThrottleDelegate : public blink::URLLoaderThrottle::Delegate {
+ public:
+  MockThrottleDelegate() = default;
+  MOCK_METHOD(void, CancelWithError, (int, std::string_view), (override));
+  MOCK_METHOD(void, Resume, (), (override));
+};
+
+class GoogleURLLoaderThrottleTest : public ::testing::Test {
+ public:
+  const GURL kTestGoogleURL = GURL("https://google.com");
+  const GURL kGoogleSubdomainURL = GURL("https://accounts.google.com");
+
+  GoogleURLLoaderThrottleTest() = default;
+  ~GoogleURLLoaderThrottleTest() override = default;
+
+  void SetAllowedDomainsForApps(const std::string& allowed_domains) {
+    allowed_domains_for_apps_ = allowed_domains;
+  }
+
+  GoogleURLLoaderThrottle* throttle() {
+    if (!throttle_) {
+      CreateThrottle();
+    }
+    return throttle_.get();
+  }
+
+  MockThrottleDelegate* delegate() { return delegate_.get(); }
+
+ protected:
+  virtual void CreateThrottle() {
+    chrome::mojom::DynamicParamsPtr dynamic_params(
+        chrome::mojom::DynamicParams::New());
+    dynamic_params->allowed_domains_for_apps = allowed_domains_for_apps_;
+
+    delegate_ = std::make_unique<MockThrottleDelegate>();
+
+    throttle_ = std::make_unique<GoogleURLLoaderThrottle>(
+#if BUILDFLAG(IS_ANDROID)
+        "",
+#endif
+#if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
+        /*bound_session_request_throttled_handler=*/nullptr,
+#endif
+        std::move(dynamic_params));
+    throttle_->set_delegate(delegate_.get());
+  }
+
+  // Must outlive `throttle_`.
+  std::unique_ptr<MockThrottleDelegate> delegate_;
+  std::unique_ptr<GoogleURLLoaderThrottle> throttle_;
+  std::string allowed_domains_for_apps_;
+};
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillStartRequestGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  network::ResourceRequest request;
+  request.url = kTestGoogleURL;
+  bool defer = false;
+  throttle()->WillStartRequest(&request, &defer);
+
+  EXPECT_EQ(request.cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillStartRequestGoogleSubdomain) {
+  SetAllowedDomainsForApps("example.com");
+  network::ResourceRequest request;
+  request.url = kGoogleSubdomainURL;
+  bool defer = false;
+  throttle()->WillStartRequest(&request, &defer);
+
+  EXPECT_EQ(request.cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillStartRequestNonGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  network::ResourceRequest request;
+  request.url = GURL("https://example.com");
+  bool defer = false;
+  throttle()->WillStartRequest(&request, &defer);
+
+  EXPECT_FALSE(request.cors_exempt_headers.HasHeader(
+      safe_search_api::kGoogleAppsAllowedDomains));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillRedirectRequestToGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = kTestGoogleURL;
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_EQ(headers_update_params.modified_cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::Not(testing::Contains(
+                  safe_search_api::kGoogleAppsAllowedDomains)));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillRedirectRequestToGoogleSubdomain) {
+  SetAllowedDomainsForApps("example.com");
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = kGoogleSubdomainURL;
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_EQ(headers_update_params.modified_cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::Not(testing::Contains(
+                  safe_search_api::kGoogleAppsAllowedDomains)));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillRedirectRequestToNonGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("https://example.com");
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_FALSE(headers_update_params.modified_cors_exempt_headers.HasHeader(
+      safe_search_api::kGoogleAppsAllowedDomains));
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::ElementsAre(safe_search_api::kGoogleAppsAllowedDomains));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsEmptyWillRedirectRequestToNonGoogleDomain) {
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("https://example.com");
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_FALSE(headers_update_params.modified_cors_exempt_headers.HasHeader(
+      safe_search_api::kGoogleAppsAllowedDomains));
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::Not(testing::Contains(
+                  safe_search_api::kGoogleAppsAllowedDomains)));
+}
+
+#if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
 using chrome::mojom::BoundSessionThrottlerParams;
 using chrome::mojom::BoundSessionThrottlerParamsPtr;
 using RequestBoundSessionStatus =
@@ -67,13 +239,6 @@
   ResumeOrCancelThrottledRequestCallback callback_;
 };
 
-class MockThrottleDelegate : public blink::URLLoaderThrottle::Delegate {
- public:
-  MockThrottleDelegate() = default;
-  MOCK_METHOD(void, CancelWithError, (int, std::string_view), (override));
-  MOCK_METHOD(void, Resume, (), (override));
-};
-
 // std::vector<BoundSessionThrottlerParamsPtr> initializer list constructor
 // doesn't work for some reason, so tests use these helpers to reduce
 // boilerplate.
@@ -92,15 +257,12 @@
   return result;
 }
 
-class GoogleURLLoaderThrottleTest
-    : public ::testing::Test,
+class GoogleURLLoaderThrottleBoundSessionCredentialsTest
+    : public GoogleURLLoaderThrottleTest,
       public ::testing::WithParamInterface<RequestAction> {
  public:
-  const GURL kTestGoogleURL = GURL("https://google.com");
-  const GURL kGoogleSubdomainURL = GURL("https://accounts.google.com");
-
-  GoogleURLLoaderThrottleTest() = default;
-  ~GoogleURLLoaderThrottleTest() override = default;
+  GoogleURLLoaderThrottleBoundSessionCredentialsTest() = default;
+  ~GoogleURLLoaderThrottleBoundSessionCredentialsTest() override = default;
 
   void ConfigureBoundSessionThrottlerParams(const std::string& domain,
                                             const std::string& path,
@@ -115,15 +277,6 @@
     return bound_session_handler_.get();
   }
 
-  GoogleURLLoaderThrottle* throttle() {
-    if (!throttle_) {
-      CreateThrottle();
-    }
-    return throttle_.get();
-  }
-
-  MockThrottleDelegate* delegate() { return delegate_.get(); }
-
   void CallThrottleAndVerifyDeferExpectation(bool expect_defer,
                                              const GURL& url) {
     // `WillStartRequest()` has to be occur before `WillRedirectRequest()`, so
@@ -181,10 +334,11 @@
     histogram_tester_ = std::make_unique<base::HistogramTester>();
   }
 
- private:
-  void CreateThrottle() {
+ protected:
+  void CreateThrottle() override {
     chrome::mojom::DynamicParamsPtr dynamic_params(
         chrome::mojom::DynamicParams::New());
+    dynamic_params->allowed_domains_for_apps = allowed_domains_for_apps_;
     for (const auto& params : bound_session_throttler_params_) {
       dynamic_params->bound_session_throttler_params.push_back(params->Clone());
     }
@@ -203,33 +357,34 @@
     throttle_->set_delegate(delegate_.get());
   }
 
+ private:
   base::test::ScopedFeatureList feature_list_{
       switches::kEnableBoundSessionCredentials};
   base::test::TaskEnvironment task_environment_;
   raw_ptr<FakeBoundSessionRequestThrottledHandler, DanglingUntriaged>
       bound_session_handler_ = nullptr;
-  // Must outlive `throttle_`.
-  std::unique_ptr<MockThrottleDelegate> delegate_;
-  std::unique_ptr<GoogleURLLoaderThrottle> throttle_;
   std::vector<BoundSessionThrottlerParamsPtr> bound_session_throttler_params_;
   std::unique_ptr<base::HistogramTester> histogram_tester_ =
       std::make_unique<base::HistogramTester>();
 };
 
-TEST_F(GoogleURLLoaderThrottleTest, GetRequestBoundSessionStatusNullParams) {
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
+       GetRequestBoundSessionStatusNullParams) {
   EXPECT_EQ(
       GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(kTestGoogleURL, {}),
       RequestBoundSessionStatus::kNotCovered);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest, GetRequestBoundSessionStatusEmptyParams) {
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
+       GetRequestBoundSessionStatusEmptyParams) {
   EXPECT_EQ(GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(
                 kTestGoogleURL, ToVector(BoundSessionThrottlerParams::New(
                                     "", "", base::Time::Now()))),
             RequestBoundSessionStatus::kNotCovered);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest, GetRequestBoundSessionStatusCookieFresh) {
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
+       GetRequestBoundSessionStatusCookieFresh) {
   EXPECT_EQ(GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(
                 kTestGoogleURL,
                 ToVector(BoundSessionThrottlerParams::New(
@@ -237,7 +392,7 @@
             RequestBoundSessionStatus::kCoveredWithFreshCookie);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest,
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
        GetRequestBoundSessionStatusNotInBoundSession) {
   EXPECT_EQ(GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(
                 GURL("https://youtube.com"),
@@ -246,14 +401,15 @@
             RequestBoundSessionStatus::kNotCovered);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest, GetRequestBoundSessionStatusCookieExpired) {
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
+       GetRequestBoundSessionStatusCookieExpired) {
   EXPECT_EQ(GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(
                 kTestGoogleURL, ToVector(BoundSessionThrottlerParams::New(
                                     "google.com", "/", base::Time::Min()))),
             RequestBoundSessionStatus::kCoveredWithMissingCookie);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest,
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
        GetRequestBoundSessionStatusCookieExpiresNow) {
   EXPECT_EQ(GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(
                 kTestGoogleURL, ToVector(BoundSessionThrottlerParams::New(
@@ -261,7 +417,7 @@
             RequestBoundSessionStatus::kCoveredWithMissingCookie);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest,
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
        GetRequestBoundSessionStatusCookieExpiredDomainWithLeadingDot) {
   EXPECT_EQ(GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(
                 kTestGoogleURL, ToVector(BoundSessionThrottlerParams::New(
@@ -269,7 +425,8 @@
             RequestBoundSessionStatus::kCoveredWithMissingCookie);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest, GetRequestBoundSessionStatusSubdomainUrl) {
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
+       GetRequestBoundSessionStatusSubdomainUrl) {
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/common/google_url_loader_throttle_unittest.cc b/chrome/common/google_url_loader_throttle_unittest.cc
index 531b8af..b35b9ddd 100644
--- a/chrome/common/google_url_loader_throttle_unittest.cc
+++ b/chrome/common/google_url_loader_throttle_unittest.cc
@@ -6,14 +6,19 @@
 
 #include <memory>
 #include <string_view>
+#include <vector>
 
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/time/time.h"
+#include "build/build_config.h"
 #include "build/buildflag.h"
 #include "chrome/common/renderer_configuration.mojom.h"
+#include "components/safe_search_api/safe_search_util.h"
+#include "components/signin/public/base/signin_buildflags.h"
 #include "components/signin/public/base/signin_switches.h"
+#include "net/url_request/redirect_info.h"
 #include "services/network/public/cpp/http_request_headers_update_params.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/cpp/url_loader_completion_status.h"
@@ -24,13 +29,180 @@
 #include "third_party/blink/public/common/loader/url_loader_throttle.h"
 #include "url/origin.h"
 
-// This file only contains tests relevant to the bound session credentials
-// feature.
 #if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
 #include "chrome/common/bound_session_request_throttled_handler.h"
+#endif  // BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
 
 namespace {
 
+class MockThrottleDelegate : public blink::URLLoaderThrottle::Delegate {
+ public:
+  MockThrottleDelegate() = default;
+  MOCK_METHOD(void, CancelWithError, (int, std::string_view), (override));
+  MOCK_METHOD(void, Resume, (), (override));
+};
+
+class GoogleURLLoaderThrottleTest : public ::testing::Test {
+ public:
+  const GURL kTestGoogleURL = GURL("https://google.com");
+  const GURL kGoogleSubdomainURL = GURL("https://accounts.google.com");
+
+  GoogleURLLoaderThrottleTest() = default;
+  ~GoogleURLLoaderThrottleTest() override = default;
+
+  void SetAllowedDomainsForApps(const std::string& allowed_domains) {
+    allowed_domains_for_apps_ = allowed_domains;
+  }
+
+  GoogleURLLoaderThrottle* throttle() {
+    if (!throttle_) {
+      CreateThrottle();
+    }
+    return throttle_.get();
+  }
+
+  MockThrottleDelegate* delegate() { return delegate_.get(); }
+
+ protected:
+  virtual void CreateThrottle() {
+    chrome::mojom::DynamicParamsPtr dynamic_params(
+        chrome::mojom::DynamicParams::New());
+    dynamic_params->allowed_domains_for_apps = allowed_domains_for_apps_;
+
+    delegate_ = std::make_unique<MockThrottleDelegate>();
+
+    throttle_ = std::make_unique<GoogleURLLoaderThrottle>(
+#if BUILDFLAG(IS_ANDROID)
+        "",
+#endif
+#if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
+        /*bound_session_request_throttled_handler=*/nullptr,
+#endif
+        std::move(dynamic_params));
+    throttle_->set_delegate(delegate_.get());
+  }
+
+  // Must outlive `throttle_`.
+  std::unique_ptr<MockThrottleDelegate> delegate_;
+  std::unique_ptr<GoogleURLLoaderThrottle> throttle_;
+  std::string allowed_domains_for_apps_;
+};
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillStartRequestGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  network::ResourceRequest request;
+  request.url = kTestGoogleURL;
+  bool defer = false;
+  throttle()->WillStartRequest(&request, &defer);
+
+  EXPECT_EQ(request.cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillStartRequestGoogleSubdomain) {
+  SetAllowedDomainsForApps("example.com");
+  network::ResourceRequest request;
+  request.url = kGoogleSubdomainURL;
+  bool defer = false;
+  throttle()->WillStartRequest(&request, &defer);
+
+  EXPECT_EQ(request.cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillStartRequestNonGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  network::ResourceRequest request;
+  request.url = GURL("https://example.com");
+  bool defer = false;
+  throttle()->WillStartRequest(&request, &defer);
+
+  EXPECT_FALSE(request.cors_exempt_headers.HasHeader(
+      safe_search_api::kGoogleAppsAllowedDomains));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillRedirectRequestToGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = kTestGoogleURL;
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_EQ(headers_update_params.modified_cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::Not(testing::Contains(
+                  safe_search_api::kGoogleAppsAllowedDomains)));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillRedirectRequestToGoogleSubdomain) {
+  SetAllowedDomainsForApps("example.com");
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = kGoogleSubdomainURL;
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_EQ(headers_update_params.modified_cors_exempt_headers.GetHeader(
+                safe_search_api::kGoogleAppsAllowedDomains),
+            "example.com");
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::Not(testing::Contains(
+                  safe_search_api::kGoogleAppsAllowedDomains)));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsWillRedirectRequestToNonGoogleDomain) {
+  SetAllowedDomainsForApps("example.com");
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("https://example.com");
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_FALSE(headers_update_params.modified_cors_exempt_headers.HasHeader(
+      safe_search_api::kGoogleAppsAllowedDomains));
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::ElementsAre(safe_search_api::kGoogleAppsAllowedDomains));
+}
+
+TEST_F(GoogleURLLoaderThrottleTest,
+       AllowedDomainsForAppsEmptyWillRedirectRequestToNonGoogleDomain) {
+  net::RedirectInfo redirect_info;
+  redirect_info.new_url = GURL("https://example.com");
+  network::mojom::URLResponseHead response_head;
+  bool defer = false;
+  network::HttpRequestHeadersUpdateParams headers_update_params;
+
+  throttle()->WillRedirectRequest(&redirect_info, response_head, &defer,
+                                  &headers_update_params);
+
+  EXPECT_FALSE(headers_update_params.modified_cors_exempt_headers.HasHeader(
+      safe_search_api::kGoogleAppsAllowedDomains));
+  EXPECT_THAT(headers_update_params.removed_headers,
+              testing::Not(testing::Contains(
+                  safe_search_api::kGoogleAppsAllowedDomains)));
+}
+
+#if BUILDFLAG(ENABLE_BOUND_SESSION_CREDENTIALS)
 using chrome::mojom::BoundSessionThrottlerParams;
 using chrome::mojom::BoundSessionThrottlerParamsPtr;
 using RequestBoundSessionStatus =
@@ -67,13 +239,6 @@
   ResumeOrCancelThrottledRequestCallback callback_;
 };
 
-class MockThrottleDelegate : public blink::URLLoaderThrottle::Delegate {
- public:
-  MockThrottleDelegate() = default;
-  MOCK_METHOD(void, CancelWithError, (int, std::string_view), (override));
-  MOCK_METHOD(void, Resume, (), (override));
-};
-
 // std::vector<BoundSessionThrottlerParamsPtr> initializer list constructor
 // doesn't work for some reason, so tests use these helpers to reduce
 // boilerplate.
@@ -92,15 +257,12 @@
   return result;
 }
 
-class GoogleURLLoaderThrottleTest
-    : public ::testing::Test,
+class GoogleURLLoaderThrottleBoundSessionCredentialsTest
+    : public GoogleURLLoaderThrottleTest,
       public ::testing::WithParamInterface<RequestAction> {
  public:
-  const GURL kTestGoogleURL = GURL("https://google.com");
-  const GURL kGoogleSubdomainURL = GURL("https://accounts.google.com");
-
-  GoogleURLLoaderThrottleTest() = default;
-  ~GoogleURLLoaderThrottleTest() override = default;
+  GoogleURLLoaderThrottleBoundSessionCredentialsTest() = default;
+  ~GoogleURLLoaderThrottleBoundSessionCredentialsTest() override = default;
 
   void ConfigureBoundSessionThrottlerParams(const std::string& domain,
                                             const std::string& path,
@@ -115,15 +277,6 @@
     return bound_session_handler_.get();
   }
 
-  GoogleURLLoaderThrottle* throttle() {
-    if (!throttle_) {
-      CreateThrottle();
-    }
-    return throttle_.get();
-  }
-
-  MockThrottleDelegate* delegate() { return delegate_.get(); }
-
   void CallThrottleAndVerifyDeferExpectation(bool expect_defer,
                                              const GURL& url) {
     // `WillStartRequest()` has to be occur before `WillRedirectRequest()`, so
@@ -181,10 +334,11 @@
     histogram_tester_ = std::make_unique<base::HistogramTester>();
   }
 
- private:
-  void CreateThrottle() {
+ protected:
+  void CreateThrottle() override {
     chrome::mojom::DynamicParamsPtr dynamic_params(
         chrome::mojom::DynamicParams::New());
+    dynamic_params->allowed_domains_for_apps = allowed_domains_for_apps_;
     for (const auto& params : bound_session_throttler_params_) {
       dynamic_params->bound_session_throttler_params.push_back(params->Clone());
     }
@@ -203,33 +357,34 @@
     throttle_->set_delegate(delegate_.get());
   }
 
+ private:
   base::test::ScopedFeatureList feature_list_{
       switches::kEnableBoundSessionCredentials};
   base::test::TaskEnvironment task_environment_;
   raw_ptr<FakeBoundSessionRequestThrottledHandler, DanglingUntriaged>
       bound_session_handler_ = nullptr;
-  // Must outlive `throttle_`.
-  std::unique_ptr<MockThrottleDelegate> delegate_;
-  std::unique_ptr<GoogleURLLoaderThrottle> throttle_;
   std::vector<BoundSessionThrottlerParamsPtr> bound_session_throttler_params_;
   std::unique_ptr<base::HistogramTester> histogram_tester_ =
       std::make_unique<base::HistogramTester>();
 };
 
-TEST_F(GoogleURLLoaderThrottleTest, GetRequestBoundSessionStatusNullParams) {
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
+       GetRequestBoundSessionStatusNullParams) {
   EXPECT_EQ(
       GoogleURLLoaderThrottle::GetRequestBoundSessionStatus(kTestGoogleURL, {}),
       RequestBoundSessionStatus::kNotCovered);
 }
 
-TEST_F(GoogleURLLoaderThrottleTest, GetRequestBoundSessionStatusEmptyParams) {
+TEST_F(GoogleURLLoaderThrottleBoundSessionCredentialsTest,
+       GetRequestBoundSessionStatusEmptyParams) {
... (truncated)
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.