Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in ReportingAndNEL
DescriptionInappropriate implementation in ReportingAndNEL
ComponentReportingAndNEL
Bug ClassLogic Error
Tracker517461759
Fix commita5ac118a31af (chromium/src) +35/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST_P
net/network_error_logging/network_error_logging_service_unittest.cc
modified

Files Changed

  • net/network_error_logging/network_error_logging_service.cc
  • net/network_error_logging/network_error_logging_service_unittest.cc
From a5ac118a31af1cf7d9f76e3a7b9ea92d42ea7f0c Mon Sep 17 00:00:00 2001
From: Kouhei Ueno <kouhei@chromium.org>
Date: Fri, 26 Jun 2026 04:39:08 -0700
Subject: [PATCH] NEL: Strip fragment/credentials from SXG report body URLs

ReportingService::QueueReport() strips the username, password, and ref
fragment from the top-level URL of every report, but the body is passed
through opaquely. The signed-exchange "sxg" body contains outer_url,
inner_url, and cert_url, which were written with raw spec(). Route them
through GetAsReferrer() so the body URLs are serialized the same way as
the top-level URL, matching the NEL spec's "exclude fragment"
serialization.

Bug: 517461759
Change-Id: Ic1617571758e567cc3e45edf6cbbd38c1a18733c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8009447
Reviewed-by: Adam Rice <ricea@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1653047}
---

diff --git a/net/network_error_logging/network_error_logging_service.cc b/net/network_error_logging/network_error_logging_service.cc
index ea31c947..4916183e 100644
--- a/net/network_error_logging/network_error_logging_service.cc
+++ b/net/network_error_logging/network_error_logging_service.cc
@@ -826,14 +826,18 @@
     body.Set(kElapsedTimeKey,
              static_cast<int>(details.elapsed_time.InMilliseconds()));
 
+    // Strip username, password, and ref fragment from the URLs in the body,
+    // matching what ReportingService::QueueReport() does for the top-level URL.
     base::DictValue sxg_body;
-    sxg_body.Set(kOuterUrlKey, details.outer_url.spec());
-    if (details.inner_url.is_valid())
-      sxg_body.Set(kInnerUrlKey, details.inner_url.spec());
+    sxg_body.Set(kOuterUrlKey, details.outer_url.GetAsReferrer().spec());
+    if (details.inner_url.is_valid()) {
+      sxg_body.Set(kInnerUrlKey, details.inner_url.GetAsReferrer().spec());
+    }
 
     base::ListValue cert_url_list;
-    if (details.cert_url.is_valid())
-      cert_url_list.Append(details.cert_url.spec());
+    if (details.cert_url.is_valid()) {
+      cert_url_list.Append(details.cert_url.GetAsReferrer().spec());
+    }
     sxg_body.Set(kCertUrlKey, std::move(cert_url_list));
     body.Set(kSignedExchangeBodyKey, std::move(sxg_body));
 
diff --git a/net/network_error_logging/network_error_logging_service_unittest.cc b/net/network_error_logging/network_error_logging_service_unittest.cc
index afbcf36..06b65d9 100644
--- a/net/network_error_logging/network_error_logging_service_unittest.cc
+++ b/net/network_error_logging/network_error_logging_service_unittest.cc
@@ -1322,6 +1322,32 @@
                             base::ListValue().Append(kCertUrl_.spec()))))));
 }
 
+TEST_P(NetworkErrorLoggingServiceTest,
+       SignedExchangeFragmentAndCredentialsStrippedFromReportBody) {
+  service()->OnHeader(kNak_, kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
+  const GURL outer_url("https://example.com/path#fragment");
+  const GURL inner_url("https://user:pass@example.net/path#fragment");
+  const GURL cert_url("https://example.com/cert_path#fragment");
+
+  service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
+      kNak_, false, "sxg.failed", outer_url, inner_url, cert_url, kServerIP_));
+  ASSERT_EQ(1u, reports().size());
+
+  EXPECT_THAT(
+      reports()[0].body,
+      Pointee(IsSupersetOfValue(base::DictValue().Set(
+          NetworkErrorLoggingService::kSignedExchangeBodyKey,
+          base::DictValue()
+              .Set(NetworkErrorLoggingService::kOuterUrlKey, kUrl_.spec())
+              .Set(NetworkErrorLoggingService::kInnerUrlKey, kInnerUrl_.spec())
+              .Set(NetworkErrorLoggingService::kCertUrlKey,
+                   base::ListValue().Append(kCertUrl_.spec()))))));
+}
+
 TEST_P(NetworkErrorLoggingServiceTest, MismatchingSubdomain_SignedExchange) {
   service()->OnHeader(kNak_, kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/net/network_error_logging/network_error_logging_service_unittest.cc b/net/network_error_logging/network_error_logging_service_unittest.cc
index afbcf36..06b65d9 100644
--- a/net/network_error_logging/network_error_logging_service_unittest.cc
+++ b/net/network_error_logging/network_error_logging_service_unittest.cc
@@ -1322,6 +1322,32 @@
                             base::ListValue().Append(kCertUrl_.spec()))))));
 }
 
+TEST_P(NetworkErrorLoggingServiceTest,
+       SignedExchangeFragmentAndCredentialsStrippedFromReportBody) {
+  service()->OnHeader(kNak_, kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
+  const GURL outer_url("https://example.com/path#fragment");
+  const GURL inner_url("https://user:pass@example.net/path#fragment");
+  const GURL cert_url("https://example.com/cert_path#fragment");
+
+  service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
+      kNak_, false, "sxg.failed", outer_url, inner_url, cert_url, kServerIP_));
+  ASSERT_EQ(1u, reports().size());
+
+  EXPECT_THAT(
+      reports()[0].body,
+      Pointee(IsSupersetOfValue(base::DictValue().Set(
+          NetworkErrorLoggingService::kSignedExchangeBodyKey,
+          base::DictValue()
+              .Set(NetworkErrorLoggingService::kOuterUrlKey, kUrl_.spec())
+              .Set(NetworkErrorLoggingService::kInnerUrlKey, kInnerUrl_.spec())
+              .Set(NetworkErrorLoggingService::kCertUrlKey,
+                   base::ListValue().Append(kCertUrl_.spec()))))));
+}
+
 TEST_P(NetworkErrorLoggingServiceTest, MismatchingSubdomain_SignedExchange) {
   service()->OnHeader(kNak_, kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
Loading diff…

Original Bug Report

reported by vm...@google.com

URL fragment and credential leak in Signed Exchange reporting body

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: The Network Error Logging (NEL) service does not sanitize URL fragments or credentials from Signed Exchange (SXG) URLs before generating reporting bodies. This can potentially allow sensitive client-side fragments (such as OAuth access tokens, Scroll-to-Text queries, or SPA router state) or credentials to be exfiltrated to the distributor’s configured NEL endpoint. This behavior violates the Fetch and NEL specifications which require response URLs serialized for reporting to exclude fragments and credentials.

Affected files:

  • net/network_error_logging/network_error_logging_service.cc
  • content/browser/web_package/signed_exchange_reporter.cc

Estimated timestamp from git blame: 2019-02-21

Summary

The Network Error Logging (NEL) service fails to sanitize the URL fields written into the Signed Exchange (SXG) report body. Specifically, sxg_body.Set(kOuterUrlKey, details.outer_url.spec()) (and the corresponding inner_url/cert_url fields) are serialized using .spec() directly, preserving URL fragments and credentials. This potentially allows sensitive client-side fragments (such as OAuth access tokens, Scroll-to-Text-Fragments, or internal routing states) to be exfiltrated to the distributor’s configured NEL collector.

Root Cause Analysis

In net/network_error_logging/network_error_logging_service.cc, CreateSignedExchangeReportBody() serializes URL details for SXG reports:

base::DictValue sxg_body;
sxg_body.Set(kOuterUrlKey, details.outer_url.spec());
if (details.inner_url.is_valid())
  sxg_body.Set(kInnerUrlKey, details.inner_url.spec());

base::ListValue cert_url_list;
if (details.cert_url.is_valid())
  cert_url_list.Append(details.cert_url.spec());
sxg_body.Set(kCertUrlKey, std::move(cert_url_list));
body.Set(kSignedExchangeBodyKey, std::move(sxg_body));

While ReportingServiceImpl::QueueReport() sanitizes the top-level URL via GetAsReferrer(), it treats the report body as opaque data and forwards it as-is. Thus, the top-level url field of the NEL report is correctly sanitized, but body.sxg.outer_url retains the original fragment and credentials.

Potential Trigger Path

  1. A page navigates or prefetches a Signed Exchange URL containing a fragment (e.g., https://distributor.example/article.sxg#access_token=SECRET). Alternatively, the fragment can be inherited cross-origin via a redirect (e.g., https://victim.example/page#access_token=SECRET -> 302 -> https://distributor.example/article.sxg).
  2. CreateResourceRequestForNavigation() sets new_request->url directly from common_params->url, keeping the full fragment.
  3. SignedExchangeRequestHandler::MaybeCreateLoader constructs the reporter using the unsanitized URL.
  4. SignedExchangeReporter stores outer_url without sanitization.
  5. On SXG load (success or failure), ReportLoadResultAndFinish forwards the report to the network service, where NetworkContext::QueueSignedExchangeReport copies it straight through.
  6. CreateSignedExchangeReportBody() writes the unsanitized outer_url.spec() into the report body.
  7. The report is uploaded to the distributor’s configured NEL endpoint containing the fragment.

Note: Since our testing environment does not support dynamic execution, these are potential steps and the vulnerability has been identified via static code analysis.

Impact

The distributor’s NEL collector receives URL fragments and credentials, which violates the Fetch and NEL specifications (where response URLs serialized for reporting must have the exclude fragment flag set).

This can potentially lead to information disclosure including:

  • Cross-origin redirect fragment inheritance: Extracting #access_token=XYZ from a cross-origin landing page.
  • Scroll-to-Text-Fragment: Revealing exact search queries or highlighted page sections to the collector.
  • Single Page App (SPA) / Router State: Exfiltrating routing details or tokens stored in URL hash fragments to a third-party reporting service.

Suggested Fix

Use network::SerializeResponseUrlForReporting() on outer_url, inner_url, and cert_url in CreateSignedExchangeReportBody() prior to calling .spec(). This aligns behavior with how other report URLs are serialized in Chrome and ensures compliance with the Fetch and NEL specifications.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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