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 <[email protected]> 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 <[email protected]> Commit-Queue: Shunya Shishido <[email protected]> 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:[email protected]/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:[email protected]/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:[email protected]/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:[email protected]/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:[email protected]/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:[email protected]/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