Chrome · Core
CVE-2026-87477
Logic Error in Core
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc |
modified |
Files Changed
content/browser/declarative_performance_observer/declarative_performance_observer.cccontent/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc
Patch
From f5383966e92c1b7718d11931f3e471c56b7b9c5d Mon Sep 17 00:00:00 2001
From: Shunya Shishido <sisidovski@chromium.org>
Date: Mon, 03 Aug 2026 02:59:38 -0700
Subject: [PATCH] Sanitize URLs in DeclarativePerformanceObserver report bodies
In DeclarativePerformanceObserver, navigation URLs and Largest
Contentful Paint (LCP) resource URLs were previously serialized verbatim
into performance entry dictionaries within Reporting-API report bodies.
While ReportingServiceImpl::QueueReport() sanitizes top-level report
URLs by stripping fragments and user credentials, report bodies were
transmitted unmodified.
This change wraps URL assignments in
network::SerializeResponseUrlForReporting() when initializing and
updating committed_url_ from navigation handles (during initial
navigation and BackForwardCache restoration), as well as when formatting
LCP resource URLs in DidObservePerformanceEntries().
TAG=agy
CONV=0d216dbe-f50c-4f85-a83b-851e23cebd72
Bug: 540059211
Change-Id: I24f0e23291232f1aab23023db36fab0e47253a19
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8177145
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Commit-Queue: Shunya Shishido <sisidovski@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672507}
---
diff --git a/content/browser/declarative_performance_observer/declarative_performance_observer.cc b/content/browser/declarative_performance_observer/declarative_performance_observer.cc
index 59c9da2..d958fe1 100644
--- a/content/browser/declarative_performance_observer/declarative_performance_observer.cc
+++ b/content/browser/declarative_performance_observer/declarative_performance_observer.cc
@@ -18,6 +18,7 @@
#include "mojo/public/cpp/bindings/message.h"
#include "net/base/load_timing_info.h"
#include "net/base/net_errors.h"
+#include "services/network/public/cpp/url_util.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
@@ -51,7 +52,8 @@
}
navigation_start_ = navigation_handle->NavigationStart();
- committed_url_ = navigation_handle->GetURL();
+ committed_url_ =
+ network::SerializeResponseUrlForReporting(navigation_handle->GetURL());
network_anonymization_key_ =
rfh->GetIsolationInfoForSubresources().network_anonymization_key();
@@ -151,6 +153,8 @@
DCHECK(navigation_handle->IsServedFromBackForwardCache());
navigation_start_ = navigation_handle->NavigationStart();
+ committed_url_ =
+ network::SerializeResponseUrlForReporting(navigation_handle->GetURL());
buffered_entries_.clear();
current_buffer_bytes_ = 0;
@@ -369,7 +373,16 @@
dict.Set("renderTime", lcp->render_time.InMillisecondsF());
dict.Set("loadTime", lcp->load_time.InMillisecondsF());
dict.Set("id", lcp->id.value_or(""));
- dict.Set("url", lcp->url.value_or(""));
+ std::string lcp_url = "";
+ if (lcp->url.has_value() && !lcp->url->empty()) {
+ GURL url(lcp->url.value());
+ if (url.is_valid()) {
+ lcp_url = network::SerializeResponseUrlForReporting(url).spec();
+ } else {
+ lcp_url = lcp->url.value();
+ }
+ }
+ dict.Set("url", lcp_url);
dict.Set("element", lcp->element.value_or(""));
AddEntryToBuffer(std::move(dict));
diff --git a/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc b/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc
index b2f5001..37cc7f61 100644
--- a/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc
+++ b/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc
@@ -1353,5 +1353,130 @@
SetBrowserClientForTesting(old_client);
}
+TEST_F(DeclarativePerformanceObserverTest,
+ SanitizesReportUrlsInNavigationEntry) {
+ const GURL kPageURL("https://user:password@example.com/index.html#section1");
+ const GURL kExpectedSanitizedURL("https://example.com/index.html");
+ const std::string kEndpoint("telemetry");
+
+ auto policy = network::mojom::DeclarativePerformanceObserverPolicy::New();
+ policy->reporting_endpoint = kEndpoint;
+ policy->entry_types.push_back(
+ network::mojom::PerformanceEntryType::kNavigation);
+
+ MockNavigationHandle navigation_handle(kPageURL, main_rfh());
+ navigation_handle.set_has_committed(true);
+ navigation_handle.set_is_in_primary_main_frame(true);
+ navigation_handle.set_is_error_page(false);
+
+ NavigationHandleTiming timing;
+ ON_CALL(navigation_handle, GetNavigationHandleTiming())
+ .WillByDefault(testing::ReturnRef(timing));
+
+ ON_CALL(navigation_handle, GetDeclarativePerformanceObserverPolicy())
+ .WillByDefault(testing::Return(policy.get()));
+
+ CreateObserver(&navigation_handle);
+ auto* observer =
+ DeclarativePerformanceObserver::GetForCurrentDocument(main_rfh());
+ ASSERT_TRUE(observer);
+
+ observer->OnEnterBFCache();
+
+ ASSERT_EQ(network_context_.reports().size(), 1u);
+ const auto& report1 = network_context_.reports()[0];
+ EXPECT_EQ(report1.type, "performance-observer");
+
+ const base::ListValue* entries1 = report1.body.FindList("entries");
+ ASSERT_TRUE(entries1);
+ ASSERT_GE(entries1->size(), 1u);
+
+ const base::DictValue* nav_entry1 = (*entries1)[0].GetIfDict();
+ ASSERT_TRUE(nav_entry1);
+ EXPECT_EQ(*(nav_entry1->FindString("entryType")), "navigation");
+ EXPECT_EQ(*(nav_entry1->FindString("name")), kExpectedSanitizedURL.spec());
+
+ network_context_.ClearReports();
+
+ const GURL kBFCacheURL(
+ "https://user:password@example.com/index.html#section2");
+ MockNavigationHandle restore_handle(kBFCacheURL, main_rfh());
+ restore_handle.set_has_committed(true);
+ restore_handle.set_is_in_primary_main_frame(true);
+ restore_handle.set_is_error_page(false);
+ restore_handle.set_is_served_from_bfcache(true);
+
+ observer->OnDidFinishNavigation(&restore_handle);
+
+ DeclarativePerformanceObserver::DeleteForCurrentDocument(main_rfh());
+
+ ASSERT_EQ(network_context_.reports().size(), 1u);
+ const auto& report2 = network_context_.reports()[0];
+ const base::ListValue* entries2 = report2.body.FindList("entries");
+ ASSERT_TRUE(entries2);
+ ASSERT_GE(entries2->size(), 1u);
+
+ const base::DictValue* nav_entry2 = (*entries2)[0].GetIfDict();
+ ASSERT_TRUE(nav_entry2);
+ EXPECT_EQ(*(nav_entry2->FindString("entryType")), "navigation");
+ EXPECT_EQ(*(nav_entry2->FindString("type")), "back_forward");
+ EXPECT_EQ(*(nav_entry2->FindString("name")), kExpectedSanitizedURL.spec());
+}
+
+TEST_F(DeclarativePerformanceObserverTest, SanitizesReportUrlsInLcpEntry) {
+ const GURL kPageURL("https://example.com/index.html");
+ const std::string kEndpoint("telemetry");
+
+ auto policy = network::mojom::DeclarativePerformanceObserverPolicy::New();
+ policy->reporting_endpoint = kEndpoint;
+ policy->entry_types.push_back(
+ network::mojom::PerformanceEntryType::kLargestContentfulPaint);
+
+ MockNavigationHandle navigation_handle(kPageURL, main_rfh());
+ navigation_handle.set_has_committed(true);
+ navigation_handle.set_is_in_primary_main_frame(true);
+ navigation_handle.set_is_error_page(false);
+
+ NavigationHandleTiming timing;
+ ON_CALL(navigation_handle, GetNavigationHandleTiming())
+ .WillByDefault(testing::ReturnRef(timing));
+
+ ON_CALL(navigation_handle, GetDeclarativePerformanceObserverPolicy())
+ .WillByDefault(testing::Return(policy.get()));
+
+ CreateObserver(&navigation_handle);
+
+ mojo::Remote<blink::mojom::DeclarativePerformanceObserverHost>
+ observer_remote;
+ DeclarativePerformanceObserver::Bind(
+ main_rfh(), observer_remote.BindNewPipeAndPassReceiver());
+
+ std::vector<blink::mojom::DeclarativePerformanceEntryPtr> entries;
+ entries.push_back(blink::mojom::DeclarativePerformanceEntry::NewLcp(
+ blink::mojom::DeclarativeLargestContentfulPaint::New(
+ base::Milliseconds(150), 450, base::Milliseconds(150),
+ base::Milliseconds(120), "hero-img",
+ "https://user:password@example.com/hero.png#section2", "IMG")));
+ observer_remote->DidObservePerformanceEntries(std::move(entries));
+ observer_remote.FlushForTesting();
+
+ DeclarativePerformanceObserver::DeleteForCurrentDocument(main_rfh());
+
+ ASSERT_EQ(network_context_.reports().size(), 1u);
+ const auto& report = network_context_.reports()[0];
+
+ const base::ListValue* entries_list = report.body.FindList("entries");
+ ASSERT_TRUE(entries_list);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc b/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc
index b2f5001..37cc7f61 100644
--- a/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc
+++ b/content/browser/declarative_performance_observer/declarative_performance_observer_unittest.cc
@@ -1353,5 +1353,130 @@
SetBrowserClientForTesting(old_client);
}
+TEST_F(DeclarativePerformanceObserverTest,
+ SanitizesReportUrlsInNavigationEntry) {
+ const GURL kPageURL("https://user:password@example.com/index.html#section1");
+ const GURL kExpectedSanitizedURL("https://example.com/index.html");
+ const std::string kEndpoint("telemetry");
+
+ auto policy = network::mojom::DeclarativePerformanceObserverPolicy::New();
+ policy->reporting_endpoint = kEndpoint;
+ policy->entry_types.push_back(
+ network::mojom::PerformanceEntryType::kNavigation);
+
+ MockNavigationHandle navigation_handle(kPageURL, main_rfh());
+ navigation_handle.set_has_committed(true);
+ navigation_handle.set_is_in_primary_main_frame(true);
+ navigation_handle.set_is_error_page(false);
+
+ NavigationHandleTiming timing;
+ ON_CALL(navigation_handle, GetNavigationHandleTiming())
+ .WillByDefault(testing::ReturnRef(timing));
+
+ ON_CALL(navigation_handle, GetDeclarativePerformanceObserverPolicy())
+ .WillByDefault(testing::Return(policy.get()));
+
+ CreateObserver(&navigation_handle);
+ auto* observer =
+ DeclarativePerformanceObserver::GetForCurrentDocument(main_rfh());
+ ASSERT_TRUE(observer);
+
+ observer->OnEnterBFCache();
+
+ ASSERT_EQ(network_context_.reports().size(), 1u);
+ const auto& report1 = network_context_.reports()[0];
+ EXPECT_EQ(report1.type, "performance-observer");
+
+ const base::ListValue* entries1 = report1.body.FindList("entries");
+ ASSERT_TRUE(entries1);
+ ASSERT_GE(entries1->size(), 1u);
+
+ const base::DictValue* nav_entry1 = (*entries1)[0].GetIfDict();
+ ASSERT_TRUE(nav_entry1);
+ EXPECT_EQ(*(nav_entry1->FindString("entryType")), "navigation");
+ EXPECT_EQ(*(nav_entry1->FindString("name")), kExpectedSanitizedURL.spec());
+
+ network_context_.ClearReports();
+
+ const GURL kBFCacheURL(
+ "https://user:password@example.com/index.html#section2");
+ MockNavigationHandle restore_handle(kBFCacheURL, main_rfh());
+ restore_handle.set_has_committed(true);
+ restore_handle.set_is_in_primary_main_frame(true);
+ restore_handle.set_is_error_page(false);
+ restore_handle.set_is_served_from_bfcache(true);
+
+ observer->OnDidFinishNavigation(&restore_handle);
+
+ DeclarativePerformanceObserver::DeleteForCurrentDocument(main_rfh());
+
+ ASSERT_EQ(network_context_.reports().size(), 1u);
+ const auto& report2 = network_context_.reports()[0];
+ const base::ListValue* entries2 = report2.body.FindList("entries");
+ ASSERT_TRUE(entries2);
+ ASSERT_GE(entries2->size(), 1u);
+
+ const base::DictValue* nav_entry2 = (*entries2)[0].GetIfDict();
+ ASSERT_TRUE(nav_entry2);
+ EXPECT_EQ(*(nav_entry2->FindString("entryType")), "navigation");
+ EXPECT_EQ(*(nav_entry2->FindString("type")), "back_forward");
+ EXPECT_EQ(*(nav_entry2->FindString("name")), kExpectedSanitizedURL.spec());
+}
+
+TEST_F(DeclarativePerformanceObserverTest, SanitizesReportUrlsInLcpEntry) {
+ const GURL kPageURL("https://example.com/index.html");
+ const std::string kEndpoint("telemetry");
+
+ auto policy = network::mojom::DeclarativePerformanceObserverPolicy::New();
+ policy->reporting_endpoint = kEndpoint;
+ policy->entry_types.push_back(
+ network::mojom::PerformanceEntryType::kLargestContentfulPaint);
+
+ MockNavigationHandle navigation_handle(kPageURL, main_rfh());
+ navigation_handle.set_has_committed(true);
+ navigation_handle.set_is_in_primary_main_frame(true);
+ navigation_handle.set_is_error_page(false);
+
+ NavigationHandleTiming timing;
+ ON_CALL(navigation_handle, GetNavigationHandleTiming())
+ .WillByDefault(testing::ReturnRef(timing));
+
+ ON_CALL(navigation_handle, GetDeclarativePerformanceObserverPolicy())
+ .WillByDefault(testing::Return(policy.get()));
+
+ CreateObserver(&navigation_handle);
+
+ mojo::Remote<blink::mojom::DeclarativePerformanceObserverHost>
+ observer_remote;
+ DeclarativePerformanceObserver::Bind(
+ main_rfh(), observer_remote.BindNewPipeAndPassReceiver());
+
+ std::vector<blink::mojom::DeclarativePerformanceEntryPtr> entries;
+ entries.push_back(blink::mojom::DeclarativePerformanceEntry::NewLcp(
+ blink::mojom::DeclarativeLargestContentfulPaint::New(
+ base::Milliseconds(150), 450, base::Milliseconds(150),
+ base::Milliseconds(120), "hero-img",
+ "https://user:password@example.com/hero.png#section2", "IMG")));
+ observer_remote->DidObservePerformanceEntries(std::move(entries));
+ observer_remote.FlushForTesting();
+
+ DeclarativePerformanceObserver::DeleteForCurrentDocument(main_rfh());
+
+ ASSERT_EQ(network_context_.reports().size(), 1u);
+ const auto& report = network_context_.reports()[0];
+
+ const base::ListValue* entries_list = report.body.FindList("entries");
+ ASSERT_TRUE(entries_list);
+ ASSERT_GE(entries_list->size(), 1u);
+
+ const base::Value& entry_val0 = (*entries_list)[0];
+ const base::DictValue* lcp_entry = entry_val0.GetIfDict();
+ ASSERT_TRUE(lcp_entry);
+
+ const std::string* url = lcp_entry->FindString("url");
+ ASSERT_TRUE(url);
+ EXPECT_EQ(*url, "https://example.com/hero.png");
+}
+
} // namespace
} // namespace content
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.
References
On This Page