Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Network
DescriptionInappropriate implementation in Network
ComponentNetwork
Bug ClassLogic Error
Tracker503719488
Fix commit59bf0e200cae (chromium/src) +131/-48
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • content/browser/network/cross_origin_embedder_policy_reporter.cc
  • content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc
From 59bf0e200cae8f2ac8f62fc4ef251be67d426e15 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Thu, 30 Apr 2026 10:40:12 -0700
Subject: [PATCH] Strip fragments and credentials from COEP and DIP violation reports

The CrossOriginEmbedderPolicyReporter and
DocumentIsolationPolicyReporter were incorrectly preserving URL
fragments in the blockedURL field of violation reports. This violates
the specifications and can leak sensitive information.

This CL introduces network::SerializeResponseUrlForReporting, which
correctly strips fragments, usernames, and passwords from URLs for
reporting purposes, following the Fetch specification.

Spec links:
- Fetch: https://fetch.spec.whatwg.org/#serialize-a-response-url-for-reporting
- COEP: https://html.spec.whatwg.org/multipage/browsers.html#queue-a-cross-origin-embedder-policy-inheritance-violation
- DIP: https://wicg.github.io/document-isolation-policy/#queue-a-document-isolation-policy-corp-violation-report

The new function uses GURL::Replacements instead of
GURL::GetAsReferrer() to ensure that local schemes like data: and blob:
are supported. Using GetAsReferrer() would sanitize such URLs to empty
strings, which caused regressions in COEP WPTs.

Note: CrossOriginOpenerPolicyReporter currently uses GetAsReferrer() via
a local SanitizedURL helper. While the COOP specification (specifically
the "sanitize a URL for a report" algorithm [1]) does not have scheme
restrictions and thus should support non-referrer schemes, this CL
intentionally leaves COOP unchanged for now to maintain existing
behavior and avoid unintentional side effects, as COOP's scheme
constraints are currently different in implementation and consistency
with the net/ reporting service [2] is currently preserved there.

Both COOP and the general Reporting service currently deviate from their
respective specifications by using GetAsReferrer(), which rejects
non-referrer schemes like data: and blob:. This CL documents these
discrepancies while ensuring COEP and DIP are spec-compliant.

[1]
https://html.spec.whatwg.org/multipage/browsers.html#sanitize-url-report

[2] https://w3c.github.io/reporting/#strip-url-for-use-in-reports

Fixed: 503719488
Force-WPT-Export: True
Change-Id: Id14221951618e4f37512beb82ac5f10642f13760
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7790791
Reviewed-by: Camille Lamy <clamy@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Eric Orth <ericorth@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1623313}
---

diff --git a/content/browser/network/cross_origin_embedder_policy_reporter.cc b/content/browser/network/cross_origin_embedder_policy_reporter.cc
index 2c8d004..765eb50 100644
--- a/content/browser/network/cross_origin_embedder_policy_reporter.cc
+++ b/content/browser/network/cross_origin_embedder_policy_reporter.cc
@@ -9,6 +9,7 @@
 #include "base/values.h"
 #include "content/public/browser/storage_partition.h"
 #include "services/network/public/cpp/request_destination.h"
+#include "services/network/public/cpp/url_util.h"
 #include "services/network/public/mojom/network_context.mojom.h"
 
 namespace content {
@@ -17,13 +18,6 @@
 
 constexpr char kType[] = "coep";
 
-GURL StripUsernameAndPassword(const GURL& url) {
-  GURL::Replacements replacements;
-  replacements.ClearUsername();
-  replacements.ClearPassword();
-  return url.ReplaceComponents(replacements);
-}
-
 }  // namespace
 
 CrossOriginEmbedderPolicyReporter::CrossOriginEmbedderPolicyReporter(
@@ -55,7 +49,7 @@
     const GURL& blocked_url,
     network::mojom::RequestDestination destination,
     bool report_only) {
-  GURL url_to_pass = StripUsernameAndPassword(blocked_url);
+  GURL url_to_pass = network::SerializeResponseUrlForReporting(blocked_url);
   QueueAndNotify(
       {std::make_pair("type", "corp"),
        std::make_pair("blockedURL", url_to_pass.spec()),
@@ -72,7 +66,7 @@
 void CrossOriginEmbedderPolicyReporter::QueueNavigationReport(
     const GURL& blocked_url,
     bool report_only) {
-  GURL url_to_pass = StripUsernameAndPassword(blocked_url);
+  GURL url_to_pass = network::SerializeResponseUrlForReporting(blocked_url);
   QueueAndNotify({std::make_pair("type", "navigation"),
                   std::make_pair("blockedURL", url_to_pass.spec())},
                  report_only);
@@ -81,7 +75,7 @@
 void CrossOriginEmbedderPolicyReporter::QueueWorkerInitializationReport(
     const GURL& blocked_url,
     bool report_only) {
-  GURL url_to_pass = StripUsernameAndPassword(blocked_url);
+  GURL url_to_pass = network::SerializeResponseUrlForReporting(blocked_url);
   QueueAndNotify({std::make_pair("type", "worker initialization"),
                   std::make_pair("blockedURL", url_to_pass.spec())},
                  report_only);
diff --git a/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc b/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc
index ebe965d..8ec1a69 100644
--- a/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc
+++ b/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc
@@ -233,7 +233,7 @@
   EXPECT_EQ(r1.group, "e1");
   EXPECT_EQ(r1.url, kContextUrl);
   EXPECT_EQ(r1.network_anonymization_key, kNetworkIsolationKey);
-  EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x#foo?bar=baz",
+  EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x",
                                        RequestDestination::kScript, "enforce"));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.group, "e2");
@@ -250,12 +250,14 @@
       GetStoragePartition(), kContextUrl, "e1", "e2",
       base::UnguessableToken::Create(), net::NetworkAnonymizationKey());
 
-  reporter.QueueCorpViolationReport(GURL("https://u:p@www1.example.com/x"),
-                                    RequestDestination::kImage,
-                                    /*report_only=*/false);
-  reporter.QueueCorpViolationReport(GURL("https://u:p@www2.example.com/y"),
-                                    RequestDestination::kScript,
-                                    /*report_only=*/true);
+  reporter.QueueCorpViolationReport(
+      GURL("https://u:p@www1.example.com/x#fragment"),
+      RequestDestination::kImage,
+      /*report_only=*/false);
+  reporter.QueueCorpViolationReport(
+      GURL("https://u:p@www2.example.com/y#fragment"),
+      RequestDestination::kScript,
+      /*report_only=*/true);
 
   ASSERT_EQ(2u, network_context().reports().size());
   const Report& r1 = network_context().reports()[0];
@@ -375,8 +377,8 @@
   EXPECT_EQ(r1.type, "coep");
   EXPECT_EQ(r1.group, "e1");
   EXPECT_EQ(r1.url, kContextUrl);
-  EXPECT_EQ(r1.body, CreateBodyForNavigation(
-                         "https://www1.example.com/x#foo?bar=baz", "enforce"));
+  EXPECT_EQ(r1.body,
+            CreateBodyForNavigation("https://www1.example.com/x", "enforce"));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.group, "e2");
   EXPECT_EQ(r2.url, kContextUrl);
@@ -406,8 +408,8 @@
   EXPECT_EQ(r1.type, "coep");
   EXPECT_EQ(r1.url, kContextUrl);
   EXPECT_TRUE(mojo::Equals(
-      r1.body, CreateMojomBodyForNavigation(
-                   "https://www1.example.com/x#foo?bar=baz", "enforce")));
+      r1.body,
+      CreateMojomBodyForNavigation("https://www1.example.com/x", "enforce")));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.url, kContextUrl);
   EXPECT_TRUE(
@@ -420,10 +422,12 @@
   CrossOriginEmbedderPolicyReporter reporter(
       GetStoragePartition(), kContextUrl, "e1", "e2",
       base::UnguessableToken::Create(), net::NetworkAnonymizationKey());
-  reporter.QueueNavigationReport(GURL("https://u:p@www1.example.com/x"),
-                                 /*report_only=*/false);
-  reporter.QueueNavigationReport(GURL("https://u:p@www2.example.com/y"),
-                                 /*report_only=*/true);
+  reporter.QueueNavigationReport(
+      GURL("https://u:p@www1.example.com/x#fragment"),
+      /*report_only=*/false);
+  reporter.QueueNavigationReport(
+      GURL("https://u:p@www2.example.com/y#fragment"),
+      /*report_only=*/true);
 
   ASSERT_EQ(2u, network_context().reports().size());
   const Report& r1 = network_context().reports()[0];
@@ -510,9 +514,9 @@
 
   EXPECT_EQ(r1.type, "coep");
   EXPECT_EQ(r1.url, kContextUrl);
-  EXPECT_TRUE(mojo::Equals(
-      r1.body, CreateMojomBodyForWorkerInitialization(
-                   "https://www1.example.com/x.js#foo?bar=baz", "enforce")));
+  EXPECT_TRUE(
+      mojo::Equals(r1.body, CreateMojomBodyForWorkerInitialization(
+                                "https://www1.example.com/x.js", "enforce")));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.url, kContextUrl);
   EXPECT_TRUE(mojo::Equals(
@@ -527,10 +531,10 @@
       GetStoragePartition(), kContextUrl, "e1", "e2",
       base::UnguessableToken::Create(), net::NetworkAnonymizationKey());
   reporter.QueueWorkerInitializationReport(
-      GURL("https://u:p@www1.example.com/x.js"),
+      GURL("https://u:p@www1.example.com/x.js#fragment"),
       /*report_only=*/false);
   reporter.QueueWorkerInitializationReport(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc b/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc
index ebe965d..8ec1a69 100644
--- a/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc
+++ b/content/browser/network/cross_origin_embedder_policy_reporter_unittest.cc
@@ -233,7 +233,7 @@
   EXPECT_EQ(r1.group, "e1");
   EXPECT_EQ(r1.url, kContextUrl);
   EXPECT_EQ(r1.network_anonymization_key, kNetworkIsolationKey);
-  EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x#foo?bar=baz",
+  EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x",
                                        RequestDestination::kScript, "enforce"));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.group, "e2");
@@ -250,12 +250,14 @@
       GetStoragePartition(), kContextUrl, "e1", "e2",
       base::UnguessableToken::Create(), net::NetworkAnonymizationKey());
 
-  reporter.QueueCorpViolationReport(GURL("https://u:p@www1.example.com/x"),
-                                    RequestDestination::kImage,
-                                    /*report_only=*/false);
-  reporter.QueueCorpViolationReport(GURL("https://u:p@www2.example.com/y"),
-                                    RequestDestination::kScript,
-                                    /*report_only=*/true);
+  reporter.QueueCorpViolationReport(
+      GURL("https://u:p@www1.example.com/x#fragment"),
+      RequestDestination::kImage,
+      /*report_only=*/false);
+  reporter.QueueCorpViolationReport(
+      GURL("https://u:p@www2.example.com/y#fragment"),
+      RequestDestination::kScript,
+      /*report_only=*/true);
 
   ASSERT_EQ(2u, network_context().reports().size());
   const Report& r1 = network_context().reports()[0];
@@ -375,8 +377,8 @@
   EXPECT_EQ(r1.type, "coep");
   EXPECT_EQ(r1.group, "e1");
   EXPECT_EQ(r1.url, kContextUrl);
-  EXPECT_EQ(r1.body, CreateBodyForNavigation(
-                         "https://www1.example.com/x#foo?bar=baz", "enforce"));
+  EXPECT_EQ(r1.body,
+            CreateBodyForNavigation("https://www1.example.com/x", "enforce"));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.group, "e2");
   EXPECT_EQ(r2.url, kContextUrl);
@@ -406,8 +408,8 @@
   EXPECT_EQ(r1.type, "coep");
   EXPECT_EQ(r1.url, kContextUrl);
   EXPECT_TRUE(mojo::Equals(
-      r1.body, CreateMojomBodyForNavigation(
-                   "https://www1.example.com/x#foo?bar=baz", "enforce")));
+      r1.body,
+      CreateMojomBodyForNavigation("https://www1.example.com/x", "enforce")));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.url, kContextUrl);
   EXPECT_TRUE(
@@ -420,10 +422,12 @@
   CrossOriginEmbedderPolicyReporter reporter(
       GetStoragePartition(), kContextUrl, "e1", "e2",
       base::UnguessableToken::Create(), net::NetworkAnonymizationKey());
-  reporter.QueueNavigationReport(GURL("https://u:p@www1.example.com/x"),
-                                 /*report_only=*/false);
-  reporter.QueueNavigationReport(GURL("https://u:p@www2.example.com/y"),
-                                 /*report_only=*/true);
+  reporter.QueueNavigationReport(
+      GURL("https://u:p@www1.example.com/x#fragment"),
+      /*report_only=*/false);
+  reporter.QueueNavigationReport(
+      GURL("https://u:p@www2.example.com/y#fragment"),
+      /*report_only=*/true);
 
   ASSERT_EQ(2u, network_context().reports().size());
   const Report& r1 = network_context().reports()[0];
@@ -510,9 +514,9 @@
 
   EXPECT_EQ(r1.type, "coep");
   EXPECT_EQ(r1.url, kContextUrl);
-  EXPECT_TRUE(mojo::Equals(
-      r1.body, CreateMojomBodyForWorkerInitialization(
-                   "https://www1.example.com/x.js#foo?bar=baz", "enforce")));
+  EXPECT_TRUE(
+      mojo::Equals(r1.body, CreateMojomBodyForWorkerInitialization(
+                                "https://www1.example.com/x.js", "enforce")));
   EXPECT_EQ(r2.type, "coep");
   EXPECT_EQ(r2.url, kContextUrl);
   EXPECT_TRUE(mojo::Equals(
@@ -527,10 +531,10 @@
       GetStoragePartition(), kContextUrl, "e1", "e2",
       base::UnguessableToken::Create(), net::NetworkAnonymizationKey());
   reporter.QueueWorkerInitializationReport(
-      GURL("https://u:p@www1.example.com/x.js"),
+      GURL("https://u:p@www1.example.com/x.js#fragment"),
       /*report_only=*/false);
   reporter.QueueWorkerInitializationReport(
-      GURL("https://u:p@www2.example.com/y.js"),
+      GURL("https://u:p@www2.example.com/y.js#fragment"),
       /*report_only=*/true);
 
   ASSERT_EQ(2u, network_context().reports().size());
diff --git a/content/browser/security/dip/document_isolation_policy_reporter_unittest.cc b/content/browser/security/dip/document_isolation_policy_reporter_unittest.cc
index 6376f05..ae9086a 100644
--- a/content/browser/security/dip/document_isolation_policy_reporter_unittest.cc
+++ b/content/browser/security/dip/document_isolation_policy_reporter_unittest.cc
@@ -178,7 +178,7 @@
   EXPECT_EQ(r1.group, "e1");
   EXPECT_EQ(r1.url, kContextUrl);
   EXPECT_EQ(r1.network_anonymization_key, kNetworkIsolationKey);
-  EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x#foo?bar=baz",
+  EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x",
                                        RequestDestination::kScript, "enforce"));
   EXPECT_EQ(r2.type, "dip");
   EXPECT_EQ(r2.group, "e2");
@@ -195,12 +195,14 @@
       GetStoragePartition(), kContextUrl, "e1", "e2",
       base::UnguessableToken::Create(), net::NetworkAnonymizationKey());
 
-  reporter.QueueCorpViolationReport(GURL("https://u:p@www1.example.com/x"),
-                                    RequestDestination::kImage,
-                                    /*report_only=*/false);
-  reporter.QueueCorpViolationReport(GURL("https://u:p@www2.example.com/y"),
-                                    RequestDestination::kScript,
-                                    /*report_only=*/true);
+  reporter.QueueCorpViolationReport(
+      GURL("https://u:p@www1.example.com/x#fragment"),
+      RequestDestination::kImage,
+      /*report_only=*/false);
+  reporter.QueueCorpViolationReport(
+      GURL("https://u:p@www2.example.com/y#fragment"),
+      RequestDestination::kScript,
+      /*report_only=*/true);
 
   ASSERT_EQ(2u, network_context().reports().size());
   const Report& r1 = network_context().reports()[0];
diff --git a/services/network/public/cpp/url_util_unittest.cc b/services/network/public/cpp/url_util_unittest.cc
new file mode 100644
index 0000000..6514e0c
--- /dev/null
+++ b/services/network/public/cpp/url_util_unittest.cc
@@ -0,0 +1,41 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/network/public/cpp/url_util.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace network {
+
+TEST(URLUtilTest, SerializeResponseUrlForReporting) {
+  struct {
+    const char* input;
+    const char* expected;
+  } kTestCases[] = {
+      {"https://example.com/", "https://example.com/"},
+      {"https://example.com/#fragment", "https://example.com/"},
+      {"https://user:pass@example.com/", "https://example.com/"},
+      {"https://user:pass@example.com/#fragment", "https://example.com/"},
+      {"https://example.com/?query", "https://example.com/?query"},
+      {"https://example.com/?query#fragment", "https://example.com/?query"},
+      {"http://example.com/", "http://example.com/"},
+      {"http://example.com/#fragment", "http://example.com/"},
+      {"http://user:pass@example.com/", "http://example.com/"},
+      {"http://user:pass@example.com/#fragment", "http://example.com/"},
+      {"data:text/html,<html></html>", "data:text/html,<html></html>"},
+      {"data:text/html,<html></html>#fragment", "data:text/html,<html></html>"},
+      {"blob:https://example.com/uuid", "blob:https://example.com/uuid"},
+      {"blob:https://example.com/uuid#fragment",
+       "blob:https://example.com/uuid"},
+  };
+
+  for (const auto& test_case : kTestCases) {
+    EXPECT_EQ(SerializeResponseUrlForReporting(GURL(test_case.input)).spec(),
+              test_case.expected)
+        << "For input: " << test_case.input;
+  }
+}
+
+}  // namespace network
diff --git a/third_party/blink/web_tests/external/wpt/html/cross-origin-embedder-policy/reporting-subresource-corp.https.html b/third_party/blink/web_tests/external/wpt/html/cross-origin-embedder-policy/reporting-subresource-corp.https.html
index e56124a..da73f83 100644
--- a/third_party/blink/web_tests/external/wpt/html/cross-origin-embedder-policy/reporting-subresource-corp.https.html
+++ b/third_party/blink/web_tests/external/wpt/html/cross-origin-embedder-policy/reporting-subresource-corp.https.html
@@ -162,6 +162,15 @@
   check: (reports, contextUrl, url) => {
     checkReport(reports[0], contextUrl, url, 'reporting', '');
     checkReport(reports[1], contextUrl, url, 'enforce', '');
+  },
+}, {
+  name: 'blocked due to COEP (with fragment)',
+  url: `${REMOTE_ORIGIN}/common/text-plain.txt#fragment`,
+  expected_count: 2,
+  check: (reports, contextUrl, url) => {
+    const strippedUrl = url.split('#')[0];
+    checkReport(reports[0], contextUrl, strippedUrl, 'reporting', '');
+    checkReport(reports[1], contextUrl, strippedUrl, 'enforce', '');
   }
 }, {
   name: 'blocked during redirect',
diff --git a/third_party/blink/web_tests/external/wpt/html/document-isolation-policy/reporting-subresource-corp.tentative.https.html b/third_party/blink/web_tests/external/wpt/html/document-isolation-policy/reporting-subresource-corp.tentative.https.html
index 013eb34..5ced0aa 100644
--- a/third_party/blink/web_tests/external/wpt/html/document-isolation-policy/reporting-subresource-corp.tentative.https.html
+++ b/third_party/blink/web_tests/external/wpt/html/document-isolation-policy/reporting-subresource-corp.tentative.https.html
@@ -133,6 +133,16 @@
     }
   },
   {
+    name: 'blocked due to DIP (with fragment)',
+    url: `${REMOTE_ORIGIN}/common/square.png#fragment`,
+    expected_count: 2,
+    check: (reports, contextUrl, url) => {
+      const strippedUrl = url.split('#')[0];
+      checkReport(reports[0], contextUrl, strippedUrl, 'reporting', '');
+      checkReport(reports[1], contextUrl, strippedUrl, 'enforce', '');
+    }
+  },
+  {
     name: 'blocked during redirect',
     url: `${ORIGIN}/common/redirect.py?location=` +
       encodeURIComponent(`${REMOTE_ORIGIN}/common/square.png`),
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential information leak of URL fragments in COEP and DIP violation reports

Flapjack, 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 Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The CrossOriginEmbedderPolicyReporter and DocumentIsolationPolicyReporter fail to strip URL fragments when generating violation reports. This violates the Fetch/HTML specifications and can potentially leak sensitive data, such as OAuth tokens, embedded in the fragment of a blocked URL to an attacker’s reporting endpoint.

Affected files:

  • content/browser/network/cross_origin_embedder_policy_reporter.cc
  • content/browser/security/dip/document_isolation_policy_reporter.cc

Estimated timestamp from git blame: 2025-01-16

Vulnerability Details

The CrossOriginEmbedderPolicyReporter (in content/browser/network/cross_origin_embedder_policy_reporter.cc) and the DocumentIsolationPolicyReporter (in content/browser/security/dip/document_isolation_policy_reporter.cc) are responsible for generating violation reports when documents or subresources violate Cross-Origin Embedder Policy (COEP) or Document Isolation Policy (DIP).

When a violation occurs, these classes include the blockedURL in the report. To sanitize the URL, they rely on a helper function called StripUsernameAndPassword:

// content/browser/network/cross_origin_embedder_policy_reporter.cc
GURL StripUsernameAndPassword(const GURL& url) {
  GURL::Replacements replacements;
  replacements.ClearUsername();
  replacements.ClearPassword();
  return url.ReplaceComponents(replacements);
}

This implementation clears the username and password but explicitly fails to clear the URL fragment (the portion after the #). According to the HTML specification for queuing a COEP violation report, the blockedURL must be the result of “stripping url for use as a referrer”, which mandates setting the URL’s fragment to null. By failing to strip the fragment, Chromium inadvertently exposes it in the generated report.

Potential Security Impact

This flaw allows sensitive information contained in fragments, such as OAuth access_tokens or internal session identifiers, to be leaked cross-origin.

Suggested Attack Scenario (Client-Side Navigation): Note: These steps represent a potential attack path derived from source code analysis.

  1. An attacker hosts a malicious site and serves the header Cross-Origin-Embedder-Policy-Report-Only: require-corp along with a valid Reporting API endpoint.
  2. The attacker registers a ReportingObserver via JavaScript for "coep" reports.
  3. The attacker embeds a vulnerable, frameable cross-origin victim application (<iframe src="https://victim.com/auth">).
  4. The victim application successfully authenticates the user and executes a client-side navigation to pass a sensitive token via the fragment: window.location.assign("https://victim.com/callback#access_token=SECRET");.
  5. The browser intercepts the navigation. Because it is a cross-document navigation, NavigationRequest::CheckResponseAdherenceToCoep evaluates the new child document against the attacker’s require-corp policy.
  6. Because the victim’s callback lacks a CORP policy, the browser queues a COEP navigation violation report.
  7. The URL passed to the reporter is redirect_chain_[0], which is the exact client-side URL including the fragment.
  8. The flawed StripUsernameAndPassword function preserves #access_token=SECRET.
  9. The browser sends the report containing the sensitive fragment to the attacker’s JavaScript ReportingObserver and to their server-side reporting endpoint.

Furthermore, this same flaw applies to subresource requests blocked by CORP. A victim iframe performing fetch("https://api.victim.com/data#secret") will have the fragment preserved by Blink over Mojo IPC, and the resulting COEP/DIP report will leak the fragment to the embedder.

The StripUsernameAndPassword function in both reporters should be updated to strip fragments. This can be achieved by utilizing GURL::GetAsReferrer() (which correctly strips username, password, and fragment) instead of a custom replacement logic. For example, Chrome’s COOP reporter (CrossOriginOpenerPolicyReporter) already correctly implements this via its SanitizedURL() helper.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


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