Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Frames
DescriptionInformation leak in Frames
ComponentFrames
Bug ClassLogic Error
Tracker540082621
Fix commitce89826b8bce (chromium/src) +119/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js
modified
if
third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js
modified

Files Changed

  • third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc
  • third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js
  • third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js.sub.headers
  • third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js
  • third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js.headers
From ce89826b8bce94f16d6fddd15d51cb2232f0cac6 Mon Sep 17 00:00:00 2001
From: Daniel Cheng <dcheng@chromium.org>
Date: Thu, 30 Jul 2026 14:13:40 -0700
Subject: [PATCH] Sanitize URLs in Connection-Allowlist violation reports

Bug: 540082621
Change-Id: I880471de03518e0f631892fc86d92ee09d4f9150
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8176442
Reviewed-by: Shivani Sharma <shivanisha@chromium.org>
Commit-Queue: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671414}
---

diff --git a/third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc b/third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc
index 26055c8..a7ed23e 100644
--- a/third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc
+++ b/third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.h"
 
 #include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
+#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
 #include "third_party/blink/renderer/core/frame/policy_container.h"
 #include "third_party/blink/renderer/core/frame/report.h"
 #include "third_party/blink/renderer/core/frame/reporting_context.h"
@@ -24,7 +25,10 @@
     const KURL& connection,
     V8ConnectionAllowlistDisposition::Enum disposition,
     const ExecutionContext& execution_context) {
-  QueueReport(connection.GetString(), disposition, execution_context);
+  const String safe_connection = ContentSecurityPolicy::StripURLForUseInReport(
+      execution_context.GetSecurityOrigin(), connection,
+      CSPDirectiveName::Unknown);
+  QueueReport(safe_connection, disposition, execution_context);
 }
 
 void ConnectionAllowlistViolationReportBody::BuildJSONValue(
@@ -82,14 +86,17 @@
           : V8ConnectionAllowlistDisposition(
                 V8ConnectionAllowlistDisposition::Enum::kReport);
 
+  const String safe_document_url =
+      ContentSecurityPolicy::StripURLForUseInReport(
+          execution_context.GetSecurityOrigin(), execution_context.Url(),
+          CSPDirectiveName::Unknown);
+
   ConnectionAllowlistViolationReportBody* body =
       MakeGarbageCollected<ConnectionAllowlistViolationReportBody>(
-          execution_context.Url().GetString(), connection, blink_allowlist,
-          disposition_obj);
+          safe_document_url, connection, blink_allowlist, disposition_obj);
 
-  Report* report =
-      MakeGarbageCollected<Report>(ReportType::kConnectionAllowlistViolation,
-                                   execution_context.Url().GetString(), body);
+  Report* report = MakeGarbageCollected<Report>(
+      ReportType::kConnectionAllowlistViolation, safe_document_url, body);
   const std::optional<std::string> endpoint = allowlist->reporting_endpoint;
 
   if (endpoint.has_value()) {
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js
new file mode 100644
index 0000000..1edd366
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js
@@ -0,0 +1,47 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/reporting/resources/report-helper.js
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+
+const port = get_host_info().HTTPS_PORT_ELIDED;
+const reportID = 'a1b2c3d4-5678-90ef-1234-567890abcdef';
+const endpoint = '/reporting/resources/report.py';
+
+promise_test(async t => {
+  const scope = '/';
+  const script = 'resources/service-worker-fetch-script-allow-all.js';
+
+  const registration = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(async () => {
+    await registration.unregister();
+  });
+
+  const worker = get_newest_worker(registration);
+  await wait_for_state(t, worker, 'activated');
+
+  await new Promise((resolve) => {
+    if (navigator.serviceWorker.controller) {
+      return resolve();
+    }
+    navigator.serviceWorker.addEventListener('controllerchange', () => resolve());
+  });
+
+  const unstrippedUrl = `https://{{hosts[alt][www]}}${port}/common/blank-with-cors.html?param=val#test-fragment`;
+  const expectedSanitizedUrl = `https://{{hosts[alt][www]}}${port}/common/blank-with-cors.html?param=val`;
+
+  let fetchFailed = false;
+  try {
+    await fetch(unstrippedUrl, { mode: 'cors', credentials: 'omit' });
+  } catch (err) {
+    fetchFailed = true;
+  }
+  assert_true(fetchFailed, 'Fetch violating Connection-Allowlist should be blocked.');
+
+  await wait(5000);
+  const reports = await pollReports(endpoint, reportID);
+
+  const violationReport = reports.find(r => r.type === 'connection-allowlist');
+  assert_not_equals(violationReport, undefined, 'Connection-Allowlist violation report should be generated.');
+  assert_equals(violationReport.body.connection, expectedSanitizedUrl, 'Fragment (#hash) must be stripped from report.body.connection.');
+  assert_false(violationReport.body.connection.includes('#test-fragment'), 'Report connection URL must not contain the fragment.');
+
+}, 'Service Worker intercepted fetch reports sanitize target connection URLs.');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js.sub.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js.sub.headers
new file mode 100644
index 0000000..1594925
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js.sub.headers
@@ -0,0 +1,2 @@
+Reporting-Endpoints: endpoint="/reporting/resources/report.py?reportID=a1b2c3d4-5678-90ef-1234-567890abcdef"
+Connection-Allowlist: (response-origin "https://{{hosts[alt][]}}:{{ports[https][0]}}"); report-to=endpoint
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js
new file mode 100644
index 0000000..1cc0db5
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js
@@ -0,0 +1,55 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/reporting/resources/report-helper.js
+// META: script=/content-security-policy/webrtc/webrtc.js
+//
+// Test that WebRTC Connection-Allowlist violation reports sanitize document URL
+// fragments while preserving report.body.connection as "webrtc".
+
+promise_test(async (t) => {
+  history.pushState(null, '', '#secret_token');
+  const expected_sanitized_url = location.href.split('#')[0];
+
+  let local_reports = [];
+  let report_promise = new Promise((resolve) => {
+    let observer = new ReportingObserver((reports) => {
+      local_reports = local_reports.concat(reports);
+      if (local_reports.length >= 2) {
+        observer.disconnect();
+        resolve();
+      }
+    });
+    observer.observe();
+  });
+
+  assert_equals(await tryConnect(), 'blocked');
+  await report_promise;
+
+  assert_equals(local_reports.length, 2);
+  assert_equals(local_reports[0]['type'], 'connection-allowlist');
+  assert_equals(local_reports[0]['url'], expected_sanitized_url);
+
+  const local_body = local_reports[0]['body'];
+  assert_equals(local_body['url'], expected_sanitized_url);
+  assert_equals(local_body['connection'], 'webrtc');
+  assert_array_equals(
+      local_body['allowlist'],
+      [get_host_info().HTTPS_ORIGIN]);
+  assert_equals(local_body['disposition'], 'enforce');
+
+  const endpoint = '/reporting/resources/report.py';
+  const id = '6b1a2380-4965-4f4f-9e66-9b6e5e89d123';
+  await wait(5000);
+  const remote_reports = await pollReports(endpoint, id);
+  assert_equals(remote_reports.length, 2);
+
+  assert_equals(remote_reports[0]['type'], 'connection-allowlist');
+  assert_equals(remote_reports[0]['url'], expected_sanitized_url);
+
+  const remote_body = remote_reports[0]['body'];
+  assert_equals(remote_body['url'], expected_sanitized_url);
+  assert_equals(remote_body['connection'], 'webrtc');
+  assert_array_equals(
+      remote_body['allowlist'],
+      [get_host_info().HTTPS_ORIGIN]);
+  assert_equals(remote_body['disposition'], 'enforce');
+}, 'Test that WebRTC Connection-Allowlist violation reports sanitize document URL fragments while keeping connection="webrtc".');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js.headers
new file mode 100644
index 0000000..274f8a9d9
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js.headers
@@ -0,0 +1,2 @@
+Reporting-Endpoints: endpoint="/reporting/resources/report.py?reportID=6b1a2380-4965-4f4f-9e66-9b6e5e89d123"
+Connection-Allowlist: (response-origin); webrtc=block; report-to=endpoint
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js
new file mode 100644
index 0000000..1edd366
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js
@@ -0,0 +1,47 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/reporting/resources/report-helper.js
+// META: script=/service-workers/service-worker/resources/test-helpers.sub.js
+
+const port = get_host_info().HTTPS_PORT_ELIDED;
+const reportID = 'a1b2c3d4-5678-90ef-1234-567890abcdef';
+const endpoint = '/reporting/resources/report.py';
+
+promise_test(async t => {
+  const scope = '/';
+  const script = 'resources/service-worker-fetch-script-allow-all.js';
+
+  const registration = await service_worker_unregister_and_register(t, script, scope);
+  t.add_cleanup(async () => {
+    await registration.unregister();
+  });
+
+  const worker = get_newest_worker(registration);
+  await wait_for_state(t, worker, 'activated');
+
+  await new Promise((resolve) => {
+    if (navigator.serviceWorker.controller) {
+      return resolve();
+    }
+    navigator.serviceWorker.addEventListener('controllerchange', () => resolve());
+  });
+
+  const unstrippedUrl = `https://{{hosts[alt][www]}}${port}/common/blank-with-cors.html?param=val#test-fragment`;
+  const expectedSanitizedUrl = `https://{{hosts[alt][www]}}${port}/common/blank-with-cors.html?param=val`;
+
+  let fetchFailed = false;
+  try {
+    await fetch(unstrippedUrl, { mode: 'cors', credentials: 'omit' });
+  } catch (err) {
+    fetchFailed = true;
+  }
+  assert_true(fetchFailed, 'Fetch violating Connection-Allowlist should be blocked.');
+
+  await wait(5000);
+  const reports = await pollReports(endpoint, reportID);
+
+  const violationReport = reports.find(r => r.type === 'connection-allowlist');
+  assert_not_equals(violationReport, undefined, 'Connection-Allowlist violation report should be generated.');
+  assert_equals(violationReport.body.connection, expectedSanitizedUrl, 'Fragment (#hash) must be stripped from report.body.connection.');
+  assert_false(violationReport.body.connection.includes('#test-fragment'), 'Report connection URL must not contain the fragment.');
+
+}, 'Service Worker intercepted fetch reports sanitize target connection URLs.');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js.sub.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js.sub.headers
new file mode 100644
index 0000000..1594925
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/service-worker-reporting-url-sanitization.https.sub.window.js.sub.headers
@@ -0,0 +1,2 @@
+Reporting-Endpoints: endpoint="/reporting/resources/report.py?reportID=a1b2c3d4-5678-90ef-1234-567890abcdef"
+Connection-Allowlist: (response-origin "https://{{hosts[alt][]}}:{{ports[https][0]}}"); report-to=endpoint
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js
new file mode 100644
index 0000000..1cc0db5
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js
@@ -0,0 +1,55 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/reporting/resources/report-helper.js
+// META: script=/content-security-policy/webrtc/webrtc.js
+//
+// Test that WebRTC Connection-Allowlist violation reports sanitize document URL
+// fragments while preserving report.body.connection as "webrtc".
+
+promise_test(async (t) => {
+  history.pushState(null, '', '#secret_token');
+  const expected_sanitized_url = location.href.split('#')[0];
+
+  let local_reports = [];
+  let report_promise = new Promise((resolve) => {
+    let observer = new ReportingObserver((reports) => {
+      local_reports = local_reports.concat(reports);
+      if (local_reports.length >= 2) {
+        observer.disconnect();
+        resolve();
+      }
+    });
+    observer.observe();
+  });
+
+  assert_equals(await tryConnect(), 'blocked');
+  await report_promise;
+
+  assert_equals(local_reports.length, 2);
+  assert_equals(local_reports[0]['type'], 'connection-allowlist');
+  assert_equals(local_reports[0]['url'], expected_sanitized_url);
+
+  const local_body = local_reports[0]['body'];
+  assert_equals(local_body['url'], expected_sanitized_url);
+  assert_equals(local_body['connection'], 'webrtc');
+  assert_array_equals(
+      local_body['allowlist'],
+      [get_host_info().HTTPS_ORIGIN]);
+  assert_equals(local_body['disposition'], 'enforce');
+
+  const endpoint = '/reporting/resources/report.py';
+  const id = '6b1a2380-4965-4f4f-9e66-9b6e5e89d123';
+  await wait(5000);
+  const remote_reports = await pollReports(endpoint, id);
+  assert_equals(remote_reports.length, 2);
+
+  assert_equals(remote_reports[0]['type'], 'connection-allowlist');
+  assert_equals(remote_reports[0]['url'], expected_sanitized_url);
+
+  const remote_body = remote_reports[0]['body'];
+  assert_equals(remote_body['url'], expected_sanitized_url);
+  assert_equals(remote_body['connection'], 'webrtc');
+  assert_array_equals(
+      remote_body['allowlist'],
+      [get_host_info().HTTPS_ORIGIN]);
+  assert_equals(remote_body['disposition'], 'enforce');
+}, 'Test that WebRTC Connection-Allowlist violation reports sanitize document URL fragments while keeping connection="webrtc".');
diff --git a/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js.headers b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js.headers
new file mode 100644
index 0000000..274f8a9d9
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/connection-allowlist/tentative/webrtc-queuereport-sanitization.https.sub.window.js.headers
@@ -0,0 +1,2 @@
+Reporting-Endpoints: endpoint="/reporting/resources/report.py?reportID=6b1a2380-4965-4f4f-9e66-9b6e5e89d123"
+Connection-Allowlist: (response-origin); webrtc=block; report-to=endpoint
Loading diff…

Original Bug Report

reported by vm...@google.com

URL fragment leak in Connection-Allowlist violation reports

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Blink’s Connection-Allowlist violation reporting pipeline potentially leaks raw document and blocked resource URLs, including their sensitive URL fragments, to reporting collectors. Although the top-level reporting pipeline sanitizes top-level report URLs, the inner JSON report body preserves fragments such as OAuth tokens. This occurs because the report body creation fails to strip fragments before sending them over Mojo and the reporting API.

Affected files:

  • third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc
  • third_party/blink/renderer/core/frame/reporting_context.cc
  • content/browser/network/reporting_service_proxy.cc

Estimated timestamp from git blame: 2026-06-02

Root Cause

In third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc inside ConnectionAllowlistViolationReportBody::QueueReport, the report body is constructed using raw, unstripped URL strings:

// third_party/blink/renderer/core/frame/connection_allowlist_violation_report_body.cc:85-88
ConnectionAllowlistViolationReportBody* body =
    MakeGarbageCollected<ConnectionAllowlistViolationReportBody>(
        execution_context.Url().GetString(), connection, blink_allowlist,
        disposition_obj);

ExecutionContext::Url().GetString() (referencing Document::url_ under the hood) returns the raw document URL, which includes any #fragment identifier (e.g., #access_token=SECRET_TOKEN). Similarly, the connection string argument represents the raw blocked-resource URL (e.g., https://disallowed.example/x#icon), which also retains its fragment identifier.

These unstripped strings are passed via Mojo to the browser process and handled in ReportingServiceProxyImpl::QueueConnectionAllowlistViolationReport (content/browser/network/reporting_service_proxy.cc:245-264). The browser proxy writes them verbatim into the base::DictValue body dictionary:

body.Set("url", url_string);        // Document URL with fragment intact
body.Set("connection", connection); // Blocked resource URL with fragment intact

While the core network reporting service (net/reporting/reporting_service.cc:113) sanitizes the top-level report url using url.GetAsReferrer(), the inner JSON body is treated as opaque and cloned verbatim (net/reporting/reporting_delivery_agent.cc:83). Consequently, the configured reporting collector receives the full, unstripped URLs inside the body field of the JSON payload, potentially leaking sensitive information such as OAuth implicit-flow tokens, credentials, or session state.

Comparison with Siblings

Other similar reporting structures explicitly strip both the document and blocked URLs prior to report body construction. For instance, in IntegrityPolicy (third_party/blink/renderer/core/frame/integrity_policy.cc:41-48), both URLs are sanitized using ContentSecurityPolicy::StripURLForUseInReport before IntegrityViolationReportBody instantiation.

Potential Trigger Steps

Note: These are suggested/potential steps to demonstrate the leak, as our tooling agent does not currently have the capability to run code to confirm a working proof-of-concept.

  1. A victim navigates to a webpage hosted at https://site.example/app#access_token=SECRET_TOKEN.
  2. The server responds with headers enabling Connection-Allowlist and configuring a reporting endpoint:
    • Connection-Allowlist: ("https://api.site.example/*"); report-to=collector; webrtc=block
    • Reporting-Endpoints: collector="https://third-party-collector.example/r"
  3. The page initiates a connection blocked by the allowlist (for example, constructing an RTCPeerConnection when webrtc=block is specified, or triggering a fetch through a Service Worker to a blocked domain).
  4. A violation report is queued and sent as an HTTP POST request to https://third-party-collector.example/r.
  5. The JSON payload sent to the collector contains the raw URL containing the #access_token=SECRET_TOKEN fragment inside body.url.

Suggested Fix

To resolve this issue, the URLs should be stripped of username, password, and fragment identifiers before ConnectionAllowlistViolationReportBody is constructed. This can be achieved by utilizing ContentSecurityPolicy::StripURLForUseInReport:

const SecurityOrigin* origin = execution_context.GetSecurityOrigin();
String safe_document_url = ContentSecurityPolicy::StripURLForUseInReport(
    origin, execution_context.Url(), CSPDirectiveName::Unknown);

KURL connection_url(connection);
String safe_connection_url = ContentSecurityPolicy::StripURLForUseInReport(
    origin, connection_url, CSPDirectiveName::Unknown);

And then passing safe_document_url and safe_connection_url when instantiating the report and its body.

Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker