Medium chrome Cross Origin 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in CORS
DescriptionIncorrect authorization in CORS
ComponentCORS
Bug ClassCross Origin
Tracker499230506
Fix commit2ef3dc14657f (chromium/src) +295/-31
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
services/network/cors/cors_url_loader.cc
modified
CorsURLLoaderTest
services/network/cors/cors_url_loader_unittest.cc
modified
CorsURLLoaderTestWithSafeRevalidation
services/network/cors/cors_url_loader_unittest.cc
modified
CorsURLLoaderTestWithSafeRevalidation
services/network/cors/cors_url_loader_unittest.cc
modified
BadMessageTestHelper
services/network/cors/cors_url_loader_unittest.cc
modified
TEST_F
services/network/cors/cors_url_loader_unittest.cc
modified
TEST_P
services/network/cors/cors_url_loader_unittest.cc
modified

Files Changed

  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader_unittest.cc
From 2ef3dc14657fe82f7f6ce443e2367b80a14ae851 Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <[email protected]>
Date: Mon, 03 Aug 2026 03:13:07 -0700
Subject: [PATCH] OOR-CORS: Safe revalidation handling via structured metadata

This CL introduces a feature flag `kSafeRevalidation` (enabled by
default) and refactors cache revalidation handling to prevent CORS
bypass vulnerabilities.

Instead of relying on Blink to directly set raw conditional headers
(`If-None-Match`, `If-Modified-Since`) and self-report a spoofable
`is_revalidating` flag, Blink now passes structured metadata
(`revalidation_etag` and `revalidation_last_modified`) in
`ResourceRequest`.

When `kSafeRevalidation` is enabled, Network Service ignores the
renderer-supplied `is_revalidating` flag and safely constructs
conditional headers and handles 304 / CORS access checks based on the
presence of structured revalidation metadata.

Bug: 499230506
Change-Id: Ic390e9d17009df94ecde283e433b9891a5d6888c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8160285
Reviewed-by: Adam Rice <[email protected]>
Commit-Queue: Takashi Toyoshima <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1672530}
---

diff --git a/services/network/cors/cors_url_loader.cc b/services/network/cors/cors_url_loader.cc
index 33b672a..16ada06 100644
--- a/services/network/cors/cors_url_loader.cc
+++ b/services/network/cors/cors_url_loader.cc
@@ -75,6 +75,14 @@
   kDisallowedHeader
 };
 
+bool IsRevalidatingRequest(const ResourceRequest& request) {
+  if (base::FeatureList::IsEnabled(features::kSafeRevalidation)) {
+    return request.revalidation_etag.has_value() ||
+           request.revalidation_last_modified.has_value();
+  }
+  return request.is_revalidating;
+}
+
 // Returns std::nullopt when a preflight isn't needed. Otherwise returns the
 // reason why a preflight is needed.
 std::optional<PreflightRequiredReason> NeedsPreflight(
@@ -99,8 +107,13 @@
       request.trusted_params &&
       request.trusted_params->is_ad_auction_trusted_signals_request;
 
+  const bool is_revalidating_for_headers =
+      base::FeatureList::IsEnabled(features::kSafeRevalidation)
+          ? false
+          : request.is_revalidating;
+
   if (!CorsUnsafeNotForbiddenRequestHeaderNames(
-           request.headers.GetHeaderVector(), request.is_revalidating,
+           request.headers.GetHeaderVector(), is_revalidating_for_headers,
            is_ad_auction_trusted_signals_request)
            .empty()) {
     return PreflightRequiredReason::kDisallowedHeader;
@@ -611,7 +624,7 @@
 
   // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
   const bool is_304_for_revalidation =
-      request_.is_revalidating && response_head->headers &&
+      IsRevalidatingRequest(request_) && response_head->headers &&
       response_head->headers->response_code() == 304;
   if (fetch_cors_flag_ && !is_304_for_revalidation) {
     const auto result = CheckAccess(
@@ -1115,6 +1128,17 @@
 
   network_loader_start_time_ = base::TimeTicks::Now();
 
+  if (base::FeatureList::IsEnabled(features::kSafeRevalidation)) {
+    if (request_.revalidation_etag) {
+      request_.headers.SetHeader(net::HttpRequestHeaders::kIfNoneMatch,
+                                 *request_.revalidation_etag);
+    }
+    if (request_.revalidation_last_modified) {
+      request_.headers.SetHeader(net::HttpRequestHeaders::kIfModifiedSince,
+                                 *request_.revalidation_last_modified);
+    }
+  }
+
   if (sync_network_loader_factory_) {
     sync_network_loader_factory_->CreateLoaderAndStartWithSyncClient(
         network_loader_.BindNewPipeAndPassReceiver(), request_id_, options_,
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index 41414ce..512a86f 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -54,6 +54,46 @@
 
 class CorsURLLoaderTest : public CorsURLLoaderTestBase {};
 
+class CorsURLLoaderTestWithSafeRevalidation
+    : public CorsURLLoaderTestBase,
+      public testing::WithParamInterface<bool> {
+ public:
+  CorsURLLoaderTestWithSafeRevalidation() {
+    feature_list_.InitWithFeatureState(features::kSafeRevalidation, GetParam());
+  }
+
+  bool IsSafeRevalidationEnabled() const { return GetParam(); }
+
+  void SetRevalidationMetadata(ResourceRequest& request,
+                               const std::string& etag,
+                               const std::string& last_modified) {
+    if (IsSafeRevalidationEnabled()) {
+      if (!etag.empty()) {
+        request.revalidation_etag = etag;
+      }
+      if (!last_modified.empty()) {
+        request.revalidation_last_modified = last_modified;
+      }
+    } else {
+      request.is_revalidating = true;
+      if (!etag.empty()) {
+        request.headers.SetHeader(net::HttpRequestHeaders::kIfNoneMatch, etag);
+      }
+      if (!last_modified.empty()) {
+        request.headers.SetHeader(net::HttpRequestHeaders::kIfModifiedSince,
+                                  last_modified);
+      }
+    }
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         CorsURLLoaderTestWithSafeRevalidation,
+                         testing::Bool());
+
 class BadMessageTestHelper {
  public:
   BadMessageTestHelper()
@@ -1838,10 +1878,9 @@
             url::Origin::Create(origin).Serialize());
 }
 
-TEST_F(CorsURLLoaderTest, 304ForSimpleRevalidation) {
+TEST_P(CorsURLLoaderTestWithSafeRevalidation, 304ForSimpleRevalidation) {
   const GURL origin("https://example.com");
   const GURL url("https://other.example.com/foo.png");
-  const GURL new_url("https://other2.example.com/bar.png");
 
   ResourceRequest request;
   request.mode = mojom::RequestMode::kCors;
@@ -1849,10 +1888,7 @@
   request.method = "GET";
   request.url = url;
   request.request_initiator = url::Origin::Create(origin);
-  request.headers.SetHeader("If-Modified-Since", "x");
-  request.headers.SetHeader("If-None-Match", "y");
-  request.headers.SetHeader("Cache-Control", "z");
-  request.is_revalidating = true;
+  SetRevalidationMetadata(request, "y", "x");
   CreateLoaderAndStart(request);
   RunUntilCreateLoaderAndStartCalled();
 
@@ -1924,10 +1960,9 @@
   EXPECT_EQ(net::ERR_FAILED, client().completion_status().error_code);
 }
 
-TEST_F(CorsURLLoaderTest, RevalidationAndPreflight) {
+TEST_P(CorsURLLoaderTestWithSafeRevalidation, RevalidationAndPreflight) {
   const GURL origin("https://example.com");
   const GURL url("https://other.example.com/foo.png");
-  const GURL new_url("https://other2.example.com/bar.png");
 
   ResourceRequest original_request;
   original_request.mode = mojom::RequestMode::kCors;
@@ -1935,11 +1970,8 @@
   original_request.method = "GET";
   original_request.url = url;
   original_request.request_initiator = url::Origin::Create(origin);
-  original_request.headers.SetHeader("If-Modified-Since", "x");
-  original_request.headers.SetHeader("If-None-Match", "y");
-  original_request.headers.SetHeader("Cache-Control", "z");
+  SetRevalidationMetadata(original_request, "y", "x");
   original_request.headers.SetHeader("foo", "bar");
-  original_request.is_revalidating = true;
   CreateLoaderAndStart(original_request);
   RunUntilCreateLoaderAndStartCalled();
 
@@ -1959,6 +1991,9 @@
   EXPECT_EQ(2, num_created_loaders());
   EXPECT_EQ(GetRequest().url, url);
   EXPECT_EQ(GetRequest().method, "GET");
+  EXPECT_EQ(GetRequest().headers.GetHeader("If-Modified-Since"), "x");
+  EXPECT_EQ(GetRequest().headers.GetHeader("If-None-Match"), "y");
+  EXPECT_EQ(GetRequest().headers.GetHeader("foo"), "bar");
 
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "https://example.com"}});
@@ -3417,5 +3452,63 @@
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index 41414ce..512a86f 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -54,6 +54,46 @@
 
 class CorsURLLoaderTest : public CorsURLLoaderTestBase {};
 
+class CorsURLLoaderTestWithSafeRevalidation
+    : public CorsURLLoaderTestBase,
+      public testing::WithParamInterface<bool> {
+ public:
+  CorsURLLoaderTestWithSafeRevalidation() {
+    feature_list_.InitWithFeatureState(features::kSafeRevalidation, GetParam());
+  }
+
+  bool IsSafeRevalidationEnabled() const { return GetParam(); }
+
+  void SetRevalidationMetadata(ResourceRequest& request,
+                               const std::string& etag,
+                               const std::string& last_modified) {
+    if (IsSafeRevalidationEnabled()) {
+      if (!etag.empty()) {
+        request.revalidation_etag = etag;
+      }
+      if (!last_modified.empty()) {
+        request.revalidation_last_modified = last_modified;
+      }
+    } else {
+      request.is_revalidating = true;
+      if (!etag.empty()) {
+        request.headers.SetHeader(net::HttpRequestHeaders::kIfNoneMatch, etag);
+      }
+      if (!last_modified.empty()) {
+        request.headers.SetHeader(net::HttpRequestHeaders::kIfModifiedSince,
+                                  last_modified);
+      }
+    }
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         CorsURLLoaderTestWithSafeRevalidation,
+                         testing::Bool());
+
 class BadMessageTestHelper {
  public:
   BadMessageTestHelper()
@@ -1838,10 +1878,9 @@
             url::Origin::Create(origin).Serialize());
 }
 
-TEST_F(CorsURLLoaderTest, 304ForSimpleRevalidation) {
+TEST_P(CorsURLLoaderTestWithSafeRevalidation, 304ForSimpleRevalidation) {
   const GURL origin("https://example.com");
   const GURL url("https://other.example.com/foo.png");
-  const GURL new_url("https://other2.example.com/bar.png");
 
   ResourceRequest request;
   request.mode = mojom::RequestMode::kCors;
@@ -1849,10 +1888,7 @@
   request.method = "GET";
   request.url = url;
   request.request_initiator = url::Origin::Create(origin);
-  request.headers.SetHeader("If-Modified-Since", "x");
-  request.headers.SetHeader("If-None-Match", "y");
-  request.headers.SetHeader("Cache-Control", "z");
-  request.is_revalidating = true;
+  SetRevalidationMetadata(request, "y", "x");
   CreateLoaderAndStart(request);
   RunUntilCreateLoaderAndStartCalled();
 
@@ -1924,10 +1960,9 @@
   EXPECT_EQ(net::ERR_FAILED, client().completion_status().error_code);
 }
 
-TEST_F(CorsURLLoaderTest, RevalidationAndPreflight) {
+TEST_P(CorsURLLoaderTestWithSafeRevalidation, RevalidationAndPreflight) {
   const GURL origin("https://example.com");
   const GURL url("https://other.example.com/foo.png");
-  const GURL new_url("https://other2.example.com/bar.png");
 
   ResourceRequest original_request;
   original_request.mode = mojom::RequestMode::kCors;
@@ -1935,11 +1970,8 @@
   original_request.method = "GET";
   original_request.url = url;
   original_request.request_initiator = url::Origin::Create(origin);
-  original_request.headers.SetHeader("If-Modified-Since", "x");
-  original_request.headers.SetHeader("If-None-Match", "y");
-  original_request.headers.SetHeader("Cache-Control", "z");
+  SetRevalidationMetadata(original_request, "y", "x");
   original_request.headers.SetHeader("foo", "bar");
-  original_request.is_revalidating = true;
   CreateLoaderAndStart(original_request);
   RunUntilCreateLoaderAndStartCalled();
 
@@ -1959,6 +1991,9 @@
   EXPECT_EQ(2, num_created_loaders());
   EXPECT_EQ(GetRequest().url, url);
   EXPECT_EQ(GetRequest().method, "GET");
+  EXPECT_EQ(GetRequest().headers.GetHeader("If-Modified-Since"), "x");
+  EXPECT_EQ(GetRequest().headers.GetHeader("If-None-Match"), "y");
+  EXPECT_EQ(GetRequest().headers.GetHeader("foo"), "bar");
 
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "https://example.com"}});
@@ -3417,5 +3452,63 @@
   VerifyUpdateRequestForRedirect(303, "FOO");
 }
 
+TEST_F(CorsURLLoaderTest, SafeRevalidationIgnoresSpoofedIsRevalidating) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kSafeRevalidation);
+
+  const GURL origin("https://example.com");
+  const GURL url("https://other.example.com/secret");
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kOmit;
+  request.method = "GET";
+  request.url = url;
+  request.request_initiator = url::Origin::Create(origin);
+  request.is_revalidating = true;  // Spoofed flag without metadata!
+
+  CreateLoaderAndStart(request);
+  RunUntilCreateLoaderAndStartCalled();
+
+  // 304 response without Access-Control-Allow-Origin header
+  NotifyLoaderClientOnReceiveResponse(304, {});
+  NotifyLoaderClientOnComplete(net::OK);
+  RunUntilComplete();
+
+  EXPECT_TRUE(client().has_received_completion());
+  EXPECT_EQ(net::ERR_FAILED, client().completion_status().error_code);
+}
+
+TEST_F(CorsURLLoaderTest, SafeRevalidationWithStructuredMetadata) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kSafeRevalidation);
+
+  const GURL origin("https://example.com");
+  const GURL url("https://other.example.com/secret");
+
+  ResourceRequest request;
+  request.mode = mojom::RequestMode::kCors;
+  request.credentials_mode = mojom::CredentialsMode::kOmit;
+  request.method = "GET";
+  request.url = url;
+  request.request_initiator = url::Origin::Create(origin);
+  request.revalidation_etag = "\"my-etag\"";
+
+  CreateLoaderAndStart(request);
+  RunUntilCreateLoaderAndStartCalled();
+
+  EXPECT_TRUE(IsNetworkLoaderStarted());
+  EXPECT_EQ(GetRequest().headers.GetHeader("If-None-Match"), "\"my-etag\"");
+
+  // 304 response without ACAO should pass when safe revalidation metadata is
+  // present
+  NotifyLoaderClientOnReceiveResponse(304, {});
+  NotifyLoaderClientOnComplete(net::OK);
+  RunUntilComplete();
+
+  EXPECT_TRUE(client().has_received_completion());
+  EXPECT_EQ(net::OK, client().completion_status().error_code);
+}
+
 }  // namespace
 }  // namespace network::cors
diff --git a/services/network/public/cpp/url_request_mojom_traits_unittest.cc b/services/network/public/cpp/url_request_mojom_traits_unittest.cc
index c3d76ae..41825ea 100644
--- a/services/network/public/cpp/url_request_mojom_traits_unittest.cc
+++ b/services/network/public/cpp/url_request_mojom_traits_unittest.cc
@@ -38,7 +38,9 @@
 namespace network {
 namespace {
 
-TEST(URLRequestMojomTraitsTest, Roundtrips_URLRequestReferrerPolicy) {
+using URLRequestMojomTraitsTest = testing::Test;
+
+TEST_F(URLRequestMojomTraitsTest, Roundtrips_URLRequestReferrerPolicy) {
   for (auto referrer_policy :
        {net::ReferrerPolicy::CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE,
         net::ReferrerPolicy::REDUCE_GRANULARITY_ON_TRANSITION_CROSS_ORIGIN,
@@ -59,7 +61,7 @@
   }
 }
 
-TEST(URLRequestMojomTraitsTest, Roundtrips_ResourceRequest) {
+TEST_F(URLRequestMojomTraitsTest, Roundtrips_ResourceRequest) {
   network::ResourceRequest original;
   original.method = "POST";
   original.url = GURL("https://example.com/resources/dummy.xml");
@@ -181,7 +183,61 @@
   EXPECT_TRUE(original.EqualsForTesting(copied));
 }
 
-TEST(URLRequestMojomTraitsTest, Roundtrips_TrustedParams) {
+TEST_F(URLRequestMojomTraitsTest,
+       Roundtrips_ResourceRequestWithRevalidationMetadata) {
+  network::ResourceRequest original;
+  original.url = GURL("https://example.com/");
+  original.revalidation_etag = "\"12345\"";
+  original.revalidation_last_modified = "Wed, 21 Oct 2015 07:28:00 GMT";
+
+  network::ResourceRequest copied;
+  EXPECT_TRUE(
+      mojo::test::SerializeAndDeserialize<mojom::URLRequest>(original, copied));
+  EXPECT_EQ(copied.revalidation_etag, "\"12345\"");
+  EXPECT_EQ(copied.revalidation_last_modified, "Wed, 21 Oct 2015 07:28:00 GMT");
+}
+
+TEST_F(URLRequestMojomTraitsTest, RevalidationMetadata_InvalidValues) {
+  {
+    // Empty etag should fail deserialization
+    network::ResourceRequest original;
+    original.url = GURL("https://example.com/");
+    original.revalidation_etag = "";
+    network::ResourceRequest copied;
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::URLRequest>(
+        original, copied));
+  }
+  {
+    // Invalid header value (containing CRLF) in etag should fail
+    // deserialization
+    network::ResourceRequest original;
+    original.url = GURL("https://example.com/");
+    original.revalidation_etag = "invalid\r\nheader";
+    network::ResourceRequest copied;
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::URLRequest>(
+        original, copied));
+  }
+  {
+    // Empty last_modified should fail deserialization
+    network::ResourceRequest original;
+    original.url = GURL("https://example.com/");
+    original.revalidation_last_modified = "";
+    network::ResourceRequest copied;
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::URLRequest>(
+        original, copied));
+  }
+  {
+    // Invalid header value in last_modified should fail deserialization
+    network::ResourceRequest original;
+    original.url = GURL("https://example.com/");
+    original.revalidation_last_modified = "invalid\r\nheader";
+    network::ResourceRequest copied;
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<mojom::URLRequest>(
+        original, copied));
+  }
+}
+
+TEST_F(URLRequestMojomTraitsTest, Roundtrips_TrustedParams) {
   network::ResourceRequest::TrustedParams original;
   original.disable_secure_dns = true;
   original.allow_cookies_from_browser = true;
@@ -227,7 +283,7 @@
   EXPECT_TRUE(copied.response_body_stream->pipe.is_valid());
 }
 
-TEST(URLRequestMojomTraitsTest, Roundtrips_TrustedParams_NullOpt) {
+TEST_F(URLRequestMojomTraitsTest, Roundtrips_TrustedParams_NullOpt) {
   network::ResourceRequest::TrustedParams original;
   original.enabled_client_hints = std::nullopt;
   network::ResourceRequest::TrustedParams copied;
Loading diff…

Original Bug Report

reported by [email protected]

CORS bypass and cross-origin data leak via spoofed is_revalidating flag

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 without the security team.

Overview: A compromised renderer can bypass CORS checks by setting the internal is_revalidating flag to true on a resource request. This allows an attacker to send cache validation headers cross-origin without a preflight and bypass access checks on 304 Not Modified responses. Consequently, the attacker can leak cross-origin headers and create credentialed ETag/Last-Modified oracles.

Affected files:

  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader_factory.cc
  • services/network/public/cpp/cors/cors_util.cc
  • services/network/public/mojom/url_request.mojom

Estimated timestamp from git blame: 2026-02-19

Description

There is a potential vulnerability in the Network Service’s handling of CORS checks for cache revalidation requests. The network::mojom::URLRequest struct contains a boolean flag is_revalidating. This flag is populated by the renderer and passed to the Network Service via Mojo IPC.

Currently, the Network Service implicitly trusts this renderer-provided flag to relax CORS enforcements:

  1. Preflight Bypass: In CorsUnsafeNotForbiddenRequestHeaderNames (services/network/cors/cors_util.cc), if is_revalidating is true, cache validation headers like If-Modified-Since and If-None-Match are exempted from triggering a CORS preflight.
  2. Access Check Bypass: In CorsURLLoader::OnReceiveResponse (services/network/cors/cors_url_loader.cc), if is_revalidating is true and the server responds with an HTTP 304 (Not Modified), the loader sets is_304_for_revalidation = true and skips the CheckAccess() function entirely.

Because a compromised renderer process can arbitrarily construct Mojo IPC messages, an attacker can spoof this flag on a cross-origin, credentialed request. By doing so, they can force the Network Service to skip both the preflight request and the response access checks for 304 responses, resulting in the raw URLResponseHead (including all cross-origin HTTP headers) being forwarded back to the compromised renderer. This acts as a cross-origin information leak and an ETag/Last-Modified oracle.

Potential Reproduction Steps

Note: These are suggested theoretical steps to trigger the vulnerability. Our tooling agent does not actively execute code or PoCs.

  1. Compromise a Renderer: An attacker achieves arbitrary code execution within a sandboxed renderer process (e.g., via a V8 bug).
  2. Construct a Malicious Request: Using the network::mojom::URLLoaderFactory IPC interface, the attacker creates a network::ResourceRequest targeting a sensitive cross-origin endpoint (e.g., https://victim.example/api/user-data).
  3. Configure CORS and Credentials: The attacker sets mode = kCors and credentials_mode = kInclude.
  4. Inject Cache Headers and Spoof Flag: The attacker adds an If-None-Match: "target-etag" header and sets the is_revalidating boolean flag to true.
  5. Initiate Request: The attacker sends the CreateLoaderAndStart IPC message.
  6. Network Service Bypasses Preflight: The Network Service receives the request. Due to the spoofed is_revalidating flag, it skips the preflight requirement for the If-None-Match header.
  7. Server Responds: The victim server receives the credentialed GET request. If the ETag matches, it returns an HTTP 304 Not Modified (which typically lacks Access-Control-Allow-Origin headers).
  8. Network Service Bypasses Access Check: In CorsURLLoader::OnReceiveResponse, the network service sees is_revalidating == true and response_code == 304. It skips the CheckAccess evaluation.
  9. Data Leak: The Network Service forwards the URLResponseHead containing the raw cross-origin headers back to the renderer, allowing the attacker to read them and confirm the ETag state.

Suggested Fix

The Network Service should not rely solely on the untrusted, renderer-provided is_revalidating flag to bypass critical CORS security checks.

Potential remediation approaches:

  • Enforce Server-Side Tracking: The Network Service (e.g., the HTTP Cache layer) should track whether a request is genuinely an internal cache revalidation, rather than accepting the state directly from the IPC struct. The renderer should not dictate this state.
  • Header Filtering: If the renderer must control this flag, CorsURLLoader should strip sensitive/non-safelisted cross-origin headers from the 304 response before forwarding the URLResponseHead back to the untrusted renderer when CheckAccess is skipped.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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