Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactCross-site request forgery in DeviceBoundSessionCredentials
DescriptionCross-site request forgery in DeviceBoundSessionCredentials
ComponentDeviceBoundSessionCredentials
Bug ClassLogic Error
Tracker501850947
Fix commit6dce8e75d9b2 (chromium/src) +584/-64
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Files Changed

  • net/device_bound_sessions/registration_fetcher.cc
From 6dce8e75d9b25edc36726a8b58ac63b81bfe8a13 Mon Sep 17 00:00:00 2001
From: Ali Hijazi <[email protected]>
Date: Sat, 15 Aug 2026 08:55:16 -0700
Subject: [PATCH] [DBSC] Enforce Fetch-Metadata and secure redirects

Network fetches for Device Bound Session Credentials (DBSC) bypass the
network::URLLoader, which ordinarily injects standard W3C Fetch-Metadata
headers. Without explicit metadata decoration, outgoing Discovery,
Registration, and Refresh requests lacked `Origin`, `Sec-Fetch-Mode`,
`Sec-Fetch-Dest`, and `Sec-Fetch-Site` assertions. Furthermore,
`URLFetcher`'s redirect handling previously allowed unvalidated HTTP 3xx
protocol downgrades (exposing DBSC cryptographic tokens and session
cookies to plaintext leakage) and failed to synchronize `Sec-Fetch-Site`
metadata upon traversing same-site or cross-origin redirect boundaries.

- Installs W3C Resource-Isolation headers (`no-cors`, `empty`,
  `Origin`, and `Sec-Fetch-Site`) across all Discovery and
  Registration/Refresh pipelines.
- Implements rigorous redirect defenses in `URLFetcher::OnReceivedRedirect`:
  * Aborts plaintext (HTTP) protocol-downgrade transitions via
    `net::ERR_UNSAFE_REDIRECT`, neutralizing token-leakage risks.
  * Dynamically re-calculates and mutates `Sec-Fetch-Site` header
    values at each redirect hop relative to the baseline `referring_origin`.

Bug: 501850947
Change-Id: I8a2ea35547d58567f9551e2557b61fcd2951cfef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8139249
Reviewed-by: Adam Rice <[email protected]>
Commit-Queue: Ali Hijazi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1680192}
---

diff --git a/net/device_bound_sessions/registration_fetcher.cc b/net/device_bound_sessions/registration_fetcher.cc
index efe2d49..1ad0ee2 100644
--- a/net/device_bound_sessions/registration_fetcher.cc
+++ b/net/device_bound_sessions/registration_fetcher.cc
@@ -16,6 +16,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/functional/concurrent_closures.h"
 #include "base/metrics/histogram_functions.h"
+#include "base/notreached.h"
 #include "base/rand_util.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/time/time.h"
@@ -27,6 +28,7 @@
 #include "net/base/features.h"
 #include "net/base/net_errors.h"
 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
+#include "net/base/url_util.h"
 #include "net/device_bound_sessions/registration_request_param.h"
 #include "net/device_bound_sessions/session_binding_utils.h"
 #include "net/device_bound_sessions/session_challenge_param.h"
@@ -35,6 +37,7 @@
 #include "net/device_bound_sessions/session_key.h"
 #include "net/device_bound_sessions/session_params.h"
 #include "net/device_bound_sessions/url_fetcher.h"
+#include "net/http/http_request_headers.h"
 #include "net/log/net_log_event_type.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "net/url_request/url_request_context.h"
@@ -205,6 +208,7 @@
  public:
   RegistrationFetcherImpl(
       const GURL& fetcher_endpoint,
+      url::Origin referring_origin,
       std::optional<std::string> session_identifier,
       SessionService& session_service,
       unexportable_keys::UnexportableKeyService& key_service,
@@ -215,6 +219,7 @@
       const std::optional<url::Origin>& original_request_initiator,
       unexportable_keys::BackgroundTaskPriority priority)
       : fetcher_endpoint_(fetcher_endpoint),
+        referring_origin_(std::move(referring_origin)),
         session_identifier_(std::move(session_identifier)),
         session_service_(session_service),
         key_service_(key_service),
@@ -374,13 +379,9 @@
     // TODO(crbug.com/495096658): Assert that `IsForRefreshRequest()` is false
     // once the tests are fixed.
     url_fetcher_ = std::make_unique<URLFetcher>(
-        context_, well_known_url, net_log_source_, IsForRefreshRequest());
-    url_fetcher_->request().set_method("GET");
-    url_fetcher_->request().set_disallow_credentials();
-    url_fetcher_->request().set_site_for_cookies(
-        isolation_info_.site_for_cookies());
-    url_fetcher_->request().set_initiator(original_request_initiator_);
-    url_fetcher_->request().set_isolation_info(isolation_info_);
+        context_, well_known_url, referring_origin_, net_log_source_,
+        IsForRefreshRequest());
+    ConfigureWellKnownRequest(url_fetcher_->request());
     url_fetcher_->Start(base::BindOnce(
         &RegistrationFetcherImpl::OnProviderWellKnownRequestComplete,
         GetWeakPtr(), request_params.TakeChallenge(),
@@ -409,9 +410,40 @@
   }
 
  private:
+  // Consolidates common URLRequest initialization, credential isolation, and
+  // W3C Resource-Isolation 'no-cors'/'empty' Fetch-Metadata parameters utilized
+  // uniformly across both Discovery (GET) and Registration (POST) DBSC
+  // operations.
+  // TODO(crbug.com/546625323): Stop duplicating request header and
+  // Fetch-Metadata population logic, and explore offloading header construction
+  // to `SessionService` or reusing `//services/network/sec_header_helpers.h`.
+  void SetupCommonFetchMetadata(URLRequest& request) {
+    request.set_site_for_cookies(site_for_cookies_);
+    request.set_initiator(original_request_initiator_);
+    request.set_isolation_info(isolation_info_);
+
+    request.SetExtraRequestHeaderByName(kSecFetchModeHeaderName, "no-cors",
+                                        /*overwrite=*/true);
+    request.SetExtraRequestHeaderByName(kSecFetchDestHeaderName, "empty",
+                                        /*overwrite=*/true);
+    request.SetExtraRequestHeaderByName(
+        kSecFetchSiteHeaderName,
+        SecFetchSiteForReferringOrigin(referring_origin_, request.url()),
+        /*overwrite=*/true);
+  }
+
+  // Configures and decorates an outbound `.well-known` DBSC discovery
+  // URLRequest via a side-effect-free, credentialless HTTP GET operation.
+  void ConfigureWellKnownRequest(URLRequest& request) {
+    request.set_method("GET");
+    request.set_disallow_credentials();
+    SetupCommonFetchMetadata(request);
+  }
+
   void StartFetcherEndpointRequest() {
     url_fetcher_ = std::make_unique<URLFetcher>(
-        context_, fetcher_endpoint_, net_log_source_, IsForRefreshRequest());
+        context_, fetcher_endpoint_, referring_origin_, net_log_source_,
+        IsForRefreshRequest());
     ConfigureRequest(url_fetcher_->request());
     if (last_registration_token_.has_value()) {
       url_fetcher_->request().SetExtraRequestHeaderByName(
@@ -438,13 +470,9 @@
     replacements.SetPathStr("/.well-known/device-bound-sessions");
     GURL well_known_url = fetcher_endpoint_.ReplaceComponents(replacements);
     url_fetcher_ = std::make_unique<URLFetcher>(
-        context_, well_known_url, net_log_source_, IsForRefreshRequest());
-    url_fetcher_->request().set_method("GET");
-    url_fetcher_->request().set_disallow_credentials();
-    url_fetcher_->request().set_site_for_cookies(
-        isolation_info_.site_for_cookies());
-    url_fetcher_->request().set_initiator(original_request_initiator_);
-    url_fetcher_->request().set_isolation_info(isolation_info_);
+        context_, well_known_url, referring_origin_, net_log_source_,
+        IsForRefreshRequest());
+    ConfigureWellKnownRequest(url_fetcher_->request());
     url_fetcher_->Start(base::BindOnce(
         &RegistrationFetcherImpl::OnRelyingPartyWellKnownRequestComplete,
         GetWeakPtr(), std::move(challenge), std::move(authorization)));
@@ -624,9 +652,17 @@
     request.set_method("POST");
     request.SetLoadFlags(LOAD_DISABLE_CACHE);
 
-    request.set_site_for_cookies(site_for_cookies_);
-    request.set_initiator(original_request_initiator_);
-    request.set_isolation_info(isolation_info_);
+    // Apply baseline W3C Fetch-Metadata ('no-cors', 'empty', 'Sec-Fetch-Site')
+    // and isolation boundaries utilized across all DBSC request pipelines.
+    SetupCommonFetchMetadata(request);
+
+    // The endpoint may be a different (same-site) origin from the response or
+    // session that configured it, so attach `Origin` and Fetch Metadata
+    // headers reflecting that relationship. These requests don't go through
+    // `network::URLLoader`, which would otherwise add them.
+    request.SetExtraRequestHeaderByName(HttpRequestHeaders::kOrigin,
+                                        referring_origin_.Serialize(),
+                                        /*overwrite=*/true);
 
     if (IsForRefreshRequest()) {
       request.SetExtraRequestHeaderByName(
@@ -808,13 +844,9 @@
       GURL well_known_url =
           fetcher_endpoint_.ReplaceComponents(std::move(replacements));
       url_fetcher_ = std::make_unique<URLFetcher>(
-          context_, well_known_url, net_log_source_, /*is_refresh=*/false);
-      url_fetcher_->request().set_method("GET");
-      url_fetcher_->request().set_disallow_credentials();
-      url_fetcher_->request().set_site_for_cookies(
-          isolation_info_.site_for_cookies());
-      url_fetcher_->request().set_initiator(original_request_initiator_);
-      url_fetcher_->request().set_isolation_info(isolation_info_);
+          context_, well_known_url, referring_origin_, net_log_source_,
+          /*is_refresh=*/false);
+      ConfigureWellKnownRequest(url_fetcher_->request());
       url_fetcher_->Start(base::BindOnce(
           &RegistrationFetcherImpl::
               OnSubdomainRegistrationWellKnownRequestComplete,
@@ -927,6 +959,10 @@
   // Refers to the endpoint this class will use when triggering a registration
   // or refresh request.
   GURL fetcher_endpoint_;
+  // The origin that configured `fetcher_endpoint_`: the origin of the response
+  // carrying the registration header, or the scope origin of the session being
+  // refreshed.
+  url::Origin referring_origin_;
   // Populated iff this is a refresh request (not a registration request).
   std::optional<std::string> session_identifier_;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/net/device_bound_sessions/registration_fetcher_param_unittest.cc b/net/device_bound_sessions/registration_fetcher_param_unittest.cc
index 5e21cb3..ddc297c 100644
--- a/net/device_bound_sessions/registration_fetcher_param_unittest.cc
+++ b/net/device_bound_sessions/registration_fetcher_param_unittest.cc
@@ -64,6 +64,8 @@
   const auto& param = params[0];
   EXPECT_EQ(param.registration_endpoint(),
             GURL("https://www.example.com/startsession"));
+  EXPECT_EQ(param.referring_origin(),
+            url::Origin::Create(registration_request));
   EXPECT_THAT(param.supported_algos(),
               UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
   EXPECT_EQ(param.challenge(), "c1");
@@ -471,6 +473,8 @@
   const auto& param = params[0];
   EXPECT_EQ(param.registration_endpoint(),
             GURL("https://accounts.example.com/startsession"));
+  EXPECT_EQ(param.referring_origin(),
+            url::Origin::Create(registration_request));
   EXPECT_THAT(param.supported_algos(),
               UnorderedElementsAre(ECDSA_SHA256, RSA_PKCS1_SHA256));
   EXPECT_EQ(param.challenge(), "c1");
diff --git a/net/device_bound_sessions/registration_fetcher_unittest.cc b/net/device_bound_sessions/registration_fetcher_unittest.cc
index f053118..314ad00f 100644
--- a/net/device_bound_sessions/registration_fetcher_unittest.cc
+++ b/net/device_bound_sessions/registration_fetcher_unittest.cc
@@ -72,11 +72,13 @@
 using ::base::test::ValueIs;
 using ::testing::_;
 using ::testing::AllOf;
+using ::testing::Contains;
 using ::testing::ElementsAre;
 using ::testing::Eq;
 using ::testing::Invoke;
 using ::testing::Not;
 using ::testing::Optional;
+using ::testing::Pair;
 using ::testing::Property;
 using ::testing::Return;
 using ::testing::WithArg;
@@ -211,7 +213,6 @@
     context_ = context_builder->Build();
   }
 
-
   unexportable_keys::UnexportableKeyService& unexportable_key_service() {
     return unexportable_key_service_;
   }
@@ -1941,6 +1942,123 @@
           "auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
 }
 
+std::unique_ptr<test_server::HttpResponse> CaptureFetchMetadataHeaders(
+    test_server::HttpRequest::HeaderMap* out_headers,
+    const test_server::HttpRequest& request) {
+  for (const char* name :
+       {"Origin", "Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"}) {
+    auto it = request.headers.find(name);
+    if (it != request.headers.end()) {
+      (*out_headers)[name] = it->second;
+    }
+  }
+  return ReturnResponse(HTTP_OK, kBasicValidJson, request);
+}
+
+TEST_F(RegistrationTest, RefreshSendsOriginAndFetchMetadata) {
+  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
+  test_server::HttpRequest::HeaderMap received_headers;
+  server_.RegisterRequestHandler(base::BindRepeating(
+      &CaptureFetchMetadataHeaders, base::Unretained(&received_headers)));
+  ASSERT_TRUE(server_.Start());
+
+  TestRegistrationCallback callback;
+  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
+  auto request_param = RegistrationRequestParam::CreateForTesting(
+      GetBaseURL(), kSessionIdentifier, kChallenge,
+      /*authorization=*/std::nullopt);
+  UnexportableSigningKeyId key = CreateSigningKey();
+  std::unique_ptr<RegistrationFetcher> fetcher =
+      RegistrationFetcher::CreateFetcher(
+          request_param, session_service(),
+          std::ref(unexportable_key_service()), context_.get(),
+          std::ref(isolation_info), net::SiteForCookies(),
+          /*net_log_source=*/std::nullopt,
+          /*original_request_initiator=*/std::nullopt,
+          unexportable_keys::BackgroundTaskPriority::kBestEffort);
+  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
+                                     callback.callback());
+  callback.WaitForCall();
+  callback.outcome().SessionForTesting();
+
+  EXPECT_THAT(
+      received_headers,
+      Contains(Pair("Origin", url::Origin::Create(GetBaseURL()).Serialize())));
+  EXPECT_THAT(received_headers,
+              Contains(Pair("Sec-Fetch-Site", "same-origin")));
+  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
+  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
+}
+
+TEST_F(RegistrationTest, RefreshSendsSameSiteFetchMetadataForCrossOrigin) {
+  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
+  test_server::HttpRequest::HeaderMap received_headers;
+  server_.RegisterRequestHandler(base::BindRepeating(
+      &CaptureFetchMetadataHeaders, base::Unretained(&received_headers)));
+  ASSERT_TRUE(server_.Start());
+
+  // The session is scoped to a sibling origin within the same site as the
+  // refresh endpoint.
+  url::Origin scope_origin =
+      url::Origin::Create(server_.GetURL("other.a.test", "/"));
+
+  TestRegistrationCallback callback;
+  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
+  auto request_param = RegistrationRequestParam::CreateForTesting(
+      GetBaseURL(), kSessionIdentifier, kChallenge,
+      /*authorization=*/std::nullopt, AttestationMode::kNone, scope_origin);
+  UnexportableSigningKeyId key = CreateSigningKey();
+  std::unique_ptr<RegistrationFetcher> fetcher =
+      RegistrationFetcher::CreateFetcher(
+          request_param, session_service(),
+          std::ref(unexportable_key_service()), context_.get(),
+          std::ref(isolation_info), net::SiteForCookies(),
+          /*net_log_source=*/std::nullopt,
+          /*original_request_initiator=*/std::nullopt,
+          unexportable_keys::BackgroundTaskPriority::kBestEffort);
+  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
+                                     callback.callback());
+  callback.WaitForCall();
+  callback.outcome().SessionForTesting();
+
+  EXPECT_THAT(received_headers,
+              Contains(Pair("Origin", scope_origin.Serialize())));
+  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Site", "same-site")));
+  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
+  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
+}
+
+TEST_F(RegistrationTest, RegistrationSendsOriginAndFetchMetadata) {
+  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
+  test_server::HttpRequest::HeaderMap received_headers;
+  server_.RegisterRequestHandler(base::BindRepeating(
+      &CaptureFetchMetadataHeaders, base::Unretained(&received_headers)));
+  ASSERT_TRUE(server_.Start());
+
+  TestRegistrationCallback callback;
+  auto param = GetBasicParam();
+  std::unique_ptr<RegistrationFetcher> fetcher =
+      RegistrationFetcher::CreateFetcher(
+          param, session_service(), unexportable_key_service(), context_.get(),
+          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
+          net::SiteForCookies(),
+          /*net_log_source=*/std::nullopt,
+          /*original_request_initiator=*/std::nullopt,
+          unexportable_keys::BackgroundTaskPriority::kBestEffort);
+  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
+                                    callback.callback());
+  callback.WaitForCall();
+  callback.outcome().SessionForTesting();
+
+  EXPECT_THAT(
+      received_headers,
+      Contains(Pair("Origin", url::Origin::Create(GetBaseURL()).Serialize())));
+  EXPECT_THAT(received_headers,
+              Contains(Pair("Sec-Fetch-Site", "same-origin")));
+  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
+  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
+}
+
 TEST_F(RegistrationTest, FetchRegistrationAndChallengeRequired) {
   crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
   server_.RegisterRequestHandler(base::BindRepeating(&ReturnForbidden));
@@ -3038,6 +3156,93 @@
             SessionError::kSubdomainRegistrationWellKnownUnavailable);
 }
 
+TEST_F(RegistrationTest, FederatedWellKnownDiscoverySendsFetchMetadata) {
+  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
+
+  test_server::HttpRequest::HeaderMap captured_headers;
+  bool well_known_fetched = false;
+
+  // 1. Initial Endpoint Response: Instruct the Fetcher to redirect onto a
+  // Subdomain scope, implicitly triggering the DBSC cross-origin .well-known
+  // verification pipeline.
+  server_.RegisterRequestHandler(base::BindLambdaForTesting(
+      [&](const test_server::HttpRequest& request)
+          -> std::unique_ptr<test_server::HttpResponse> {
+        if (request.relative_url != "/") {
+          return nullptr;
+        }
+        auto response = std::make_unique<test_server::BasicHttpResponse>();
+        response->set_code(HTTP_FOUND);
+        response->AddCustomHeader(
+            "Location", server_.GetURL("subdomain.a.test", "/dbsc").spec());
+        return response;
+      }));
+
+  // 2. Mock Config Delivery for the Subdomain path.
+  server_.RegisterRequestHandler(base::BindLambdaForTesting(
+      [&](const test_server::HttpRequest& request)
+          -> std::unique_ptr<test_server::HttpResponse> {
+        if (request.relative_url != "/dbsc") {
+          return nullptr;
+        }
+        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
+      }));
+
+  // 3. .well-known Interceptor: Capture the outbound GET metadata for the
+  // cross-origin validation check.
+  server_.RegisterRequestHandler(base::BindLambdaForTesting(
+      [&](const test_server::HttpRequest& request)
+          -> std::unique_ptr<test_server::HttpResponse> {
+        if (request.relative_url != "/.well-known/device-bound-sessions") {
+          return nullptr;
+        }
+
+        for (const char* name :
+             {"Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"}) {
+          auto it = request.headers.find(name);
+          if (it != request.headers.end()) {
+            captured_headers[name] = it->second;
+          }
+        }
+
+        well_known_fetched = true;
+        // Yield a 404 to cleanly isolate the test strictly to the pre-flight
+        // discovery dispatch.
+        return ReturnResponse(HTTP_NOT_FOUND, "", request);
+      }));
+
+  ASSERT_TRUE(server_.Start());
+
+  GURL registration_url = server_.GetURL("a.test", "/");
+  TestRegistrationCallback callback;
+
+  // Trigger Subdomain Cross-Origin Registration, implicitly firing the
+  // .well-known discovery step on the server-bound origin.
+  auto param = GetBasicParam(registration_url);
+  std::unique_ptr<RegistrationFetcher> fetcher =
+      RegistrationFetcher::CreateFetcher(
+          param, session_service(), unexportable_key_service(), context_.get(),
+          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
+          net::SiteForCookies(),
+          /*net_log_source=*/std::nullopt,
+          /*original_request_initiator=*/std::nullopt,
+          unexportable_keys::BackgroundTaskPriority::kBestEffort);
+  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
+                                    callback.callback());
+  callback.WaitForCall();
+
+  // 1. Assert the .well-known validator was indeed engaged by the DBSC
+  // state-machine.
+  EXPECT_TRUE(well_known_fetched);
+
+  // 2. Validate that our ConfigureWellKnownRequest pipeline successfully
+  // computed and appended W3C 'no-cors', 'empty', and Origin-relative Metadata.
+  EXPECT_THAT(captured_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
+  EXPECT_THAT(captured_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
+  EXPECT_THAT(captured_headers,
+              Contains(Pair("Sec-Fetch-Site", "same-origin")));
+}
+
 TEST_F(RegistrationTest, FederatedSuccess) {
   crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
diff --git a/net/device_bound_sessions/session_binding_utils_unittest.cc b/net/device_bound_sessions/session_binding_utils_unittest.cc
index b2a982f..a88c193 100644
--- a/net/device_bound_sessions/session_binding_utils_unittest.cc
+++ b/net/device_bound_sessions/session_binding_utils_unittest.cc
@@ -22,6 +22,7 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 namespace net::device_bound_sessions {
 
@@ -336,4 +337,37 @@
       std::nullopt);
 }
 
+TEST(SessionBindingUtilsTest, SecFetchSiteForReferringOrigin) {
+  // Validate that W3C Sec-Fetch-Site string-literal translation correctly
+  // resolves 'same-origin', 'same-site', and 'cross-site' origin relationships,
+  // including strictness against unencrypted/HTTP protocol-mismatches.
+  url::Origin referring_origin =
+      url::Origin::Create(GURL("https://www.example.com"));
+
+  // 1. Same-Origin: Matching cryptographic scheme, eTLD+1, and subdomain.
+  EXPECT_EQ(
+      SecFetchSiteForReferringOrigin(
+          referring_origin, GURL("https://www.example.com/path/to/resource")),
+      "same-origin");
+
+  // 2. Same-Site: Matching cryptographic scheme and eTLD+1, but differing
+  // subdomain boundaries.
+  EXPECT_EQ(
+      SecFetchSiteForReferringOrigin(
+          referring_origin, GURL("https://subdomain.example.com/endpoint")),
+      "same-site");
... (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.