Add support for path absolute url to reporting endpoint
Add wpt tests to cover reports sent out-of-band to
path-absolute-url endpoints.
Copying some report infra code from wpt/content-security-policy
Github discussions here https://github.com/w3c/reporting/issues/147
Bug: 1060306
Change-Id: I293cb686fd60edd15b28b7ccf7a5a9904b7ea588
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2096846
Commit-Queue: Rodney Ding <[email protected]>
Reviewed-by: Lily Chen <[email protected]>
Reviewed-by: Ian Clelland <[email protected]>
Cr-Commit-Position: refs/heads/master@{#749897}
diff --git a/net/reporting/reporting_header_parser.cc b/net/reporting/reporting_header_parser.cc
index 4f802fe..12f0064a 100644
--- a/net/reporting/reporting_header_parser.cc
+++ b/net/reporting/reporting_header_parser.cc
@@ -79,7 +79,13 @@
if (!dict->GetString(kUrlKey, &endpoint_url_string))
return HeaderEndpointOutcome::DISCARDED_URL_NOT_STRING;
- GURL endpoint_url(endpoint_url_string);
+ GURL endpoint_url;
+ // Support path-absolute-URL string
+ if (std::strspn(endpoint_url_string.c_str(), "/") == 1) {
+ endpoint_url = group_key.origin.GetURL().Resolve(endpoint_url_string);
+ } else {
+ endpoint_url = GURL(endpoint_url_string);
+ }
if (!endpoint_url.is_valid())
return HeaderEndpointOutcome::DISCARDED_URL_INVALID;
if (!endpoint_url.SchemeIsCryptographic())
diff --git a/net/reporting/reporting_header_parser_unittest.cc b/net/reporting/reporting_header_parser_unittest.cc
index e0e51ca1..cedaa99 100644
--- a/net/reporting/reporting_header_parser_unittest.cc
+++ b/net/reporting/reporting_header_parser_unittest.cc
@@ -145,6 +145,8 @@
const GURL kEndpoint_ = GURL("https://endpoint.test/");
const GURL kEndpoint2_ = GURL("https://endpoint2.test/");
const GURL kEndpoint3_ = GURL("https://endpoint3.test/");
+ const GURL kEndpointPathAbsolute_ =
+ GURL("https://origin.test/path-absolute-url");
const std::string kGroup_ = "group";
const std::string kGroup2_ = "group2";
const std::string kType_ = "type";
@@ -168,9 +170,12 @@
} kInvalidHeaderTestCases[] = {
{"{\"max_age\":1, \"endpoints\": [{}]}", "missing url"},
{"{\"max_age\":1, \"endpoints\": [{\"url\":0}]}", "non-string url"},
+ {"{\"max_age\":1, \"endpoints\": [{\"url\":\"//scheme/relative\"}]}",
+ "scheme-relative url"},
+ {"{\"max_age\":1, \"endpoints\": [{\"url\":\"relative/path\"}]}",
+ "path relative url"},
{"{\"max_age\":1, \"endpoints\": [{\"url\":\"http://insecure/\"}]}",
"insecure url"},
-
{"{\"endpoints\": [{\"url\":\"https://endpoint/\"}]}", "missing max_age"},
{"{\"max_age\":\"\", \"endpoints\": [{\"url\":\"https://endpoint/\"}]}",
"non-integer max_age"},
@@ -255,6 +260,47 @@
}
}
+TEST_P(ReportingHeaderParserTest, PathAbsoluteURLEndpoint) {
+ std::string header =
+ "{\"group\": \"group\", \"max_age\":1, \"endpoints\": "
+ "[{\"url\":\"/path-absolute-url\"}]}";
+
+ ParseHeader(kUrl_, header);
+ EXPECT_EQ(1u, cache()->GetEndpointGroupCountForTesting());
+ EXPECT_TRUE(
+ EndpointGroupExistsInCache(kGroupKey_, OriginSubdomains::DEFAULT));
+ EXPECT_TRUE(ClientExistsInCacheForOrigin(kOrigin_));
+ EXPECT_EQ(1u, cache()->GetEndpointCount());
+ ReportingEndpoint endpoint =
+ FindEndpointInCache(kGroupKey_, kEndpointPathAbsolute_);
+ ASSERT_TRUE(endpoint);
+ EXPECT_EQ(kOrigin_, endpoint.group_key.origin);
+ EXPECT_EQ(kGroup_, endpoint.group_key.group_name);
+ EXPECT_EQ(kEndpointPathAbsolute_, endpoint.info.url);
+ EXPECT_EQ(ReportingEndpoint::EndpointInfo::kDefaultPriority,
+ endpoint.info.priority);
+ EXPECT_EQ(ReportingEndpoint::EndpointInfo::kDefaultWeight,
+ endpoint.info.weight);
+
+ if (mock_store()) {
+ mock_store()->Flush();
+ EXPECT_EQ(1, mock_store()->StoredEndpointsCount());
+ EXPECT_EQ(1, mock_store()->StoredEndpointGroupsCount());
+ MockPersistentReportingStore::CommandList expected_commands;
+ expected_commands.emplace_back(
+ CommandType::ADD_REPORTING_ENDPOINT,
+ ReportingEndpoint(kGroupKey_, ReportingEndpoint::EndpointInfo{
+ kEndpointPathAbsolute_}));
+ expected_commands.emplace_back(
+ CommandType::ADD_REPORTING_ENDPOINT_GROUP,
+ CachedReportingEndpointGroup(
+ kGroupKey_, OriginSubdomains::DEFAULT /* irrelevant */,
+ base::Time() /* irrelevant */, base::Time() /* irrelevant */));
+ EXPECT_THAT(mock_store()->GetAllCommands(),
+ testing::IsSupersetOf(expected_commands));
+ }
+}
+
TEST_P(ReportingHeaderParserTest, OmittedGroupName) {
ReportingEndpointGroupKey kGroupKey(NetworkIsolationKey(), kOrigin_,
"default");
diff --git a/net/reporting/reporting_service_unittest.cc b/net/reporting/reporting_service_unittest.cc
index 4fe6e600..96365b1a 100644
--- a/net/reporting/reporting_service_unittest.cc
+++ b/net/reporting/reporting_service_unittest.cc
@@ -138,6 +138,18 @@
EXPECT_EQ(1u, context()->cache()->GetEndpointCount());
}
+TEST_P(ReportingServiceTest, ProcessHeaderPathAbsolute) {
+ service()->ProcessHeader(kUrl_,
+ "{\"endpoints\":[{\"url\":\"/path-absolute\"}],"
+ "\"group\":\"" +
+ kGroup_ +
+ "\","
+ "\"max_age\":86400}");
+ FinishLoading(true /* load_success */);
+
+ EXPECT_EQ(1u, context()->cache()->GetEndpointCount());
+}
+
TEST_P(ReportingServiceTest, ProcessHeader_TooLong) {
const std::string header_too_long =
"{\"endpoints\":[{\"url\":\"" + kEndpoint_.spec() +
diff --git a/third_party/blink/web_tests/external/wpt/reporting/path-absolute-endpoint.https.sub.html b/third_party/blink/web_tests/external/wpt/reporting/path-absolute-endpoint.https.sub.html
new file mode 100644
index 0000000..ec06a368
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/reporting/path-absolute-endpoint.https.sub.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that reports are sent when report-endpoint points to path-absolute-url</title>
+ <script src='/https/chromium.googlesource.com/resources/testharness.js'></script>
+ <script src='/https/chromium.googlesource.com/resources/testharnessreport.js'></script>
+ <script src='resources/report-helper.js'></script>
+</head>
+<body>
+ <script>
+ var t = async_test("Test that image does not load");
+ const base_url = `${location.protocol}//${location.host}`;
+ async_test(function(t) {
+ window.addEventListener("securitypolicyviolation", t.step_func(function(e) {
+ assert_equals(e.blockedURI, `${base_url}/reporting/resources/fail.png`);
+ assert_equals(e.violatedDirective, "img-src");
+ t.done();
+ }));
+ }, "Event is fired");
+
+ async_test(function(t) {
+ var observer = new ReportingObserver(function(reports, observer) {
+ t.step(function() {
+ assert_equals(reports.length, 1);
+
+ // Ensure that the contents of the report are valid.
+
+ assert_equals(reports[0].type, "csp-violation");
+ assert_equals(reports[0].url, location.href);
+ assert_equals(reports[0].body.documentURL, location.href);
+ assert_equals(reports[0].body.referrer, null);
+ assert_equals(reports[0].body.blockedURL,
+ `${base_url}/reporting/resources/fail.png`);
+ assert_equals(reports[0].body.effectiveDirective, "img-src");
+ assert_equals(reports[0].body.originalPolicy,
+ "script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-group");
+ assert_equals(reports[0].body.sourceFile, location.href);
+ assert_equals(reports[0].body.sample, null);
+ assert_equals(reports[0].body.disposition, "enforce");
+ assert_equals(reports[0].body.statusCode, 0);
+ assert_equals(reports[0].body.lineNumber, 66);
+ assert_equals(reports[0].body.columnNumber, 0);
+ });
+
+ t.done();
+ });
+ observer.observe();
+ }, "Report is observable to ReportingObserver");
+ </script>
+ <img src='/https/chromium.googlesource.com/reporting/resources/fail.png'
+ onload='t.unreached_func("The image should not have loaded");'
+ onerror='t.done();'>
+ <script>
+ async_test(async (t) => {
+ try {
+ const endpoint = `${base_url}/reporting/resources/report.py`;
+ const id = 'd0d517bf-891b-457a-b970-8b2b2c81a0bf';
+ await wait(3000);
+ const reports = await pollReports(endpoint, id);
+ checkReportExists(reports, 'csp-violation', location.href);
+ t.done();
+ } catch (e) {
+ t.step(() => { throw e; });
+ }
+ }, "Reporting endpoints received reports.");
+ </script>
+
+</body>
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/reporting/path-absolute-endpoint.https.sub.html.sub.headers b/third_party/blink/web_tests/external/wpt/reporting/path-absolute-endpoint.https.sub.html.sub.headers
new file mode 100644
index 0000000..ec25b28
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/reporting/path-absolute-endpoint.https.sub.html.sub.headers
@@ -0,0 +1,2 @@
+Report-To: { "group": "csp-group", "max_age": 10886400, "endpoints": [{ "url": "/reporting/resources/report.py?id=d0d517bf-891b-457a-b970-8b2b2c81a0bf" }] }
+Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-group
diff --git a/third_party/blink/web_tests/external/wpt/reporting/resources/fail.png b/third_party/blink/web_tests/external/wpt/reporting/resources/fail.png
new file mode 100644
index 0000000..b5933803
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/reporting/resources/fail.png
Binary files differ
diff --git a/third_party/blink/web_tests/external/wpt/reporting/resources/report-helper.js b/third_party/blink/web_tests/external/wpt/reporting/resources/report-helper.js
new file mode 100644
index 0000000..a20a9cd
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/reporting/resources/report-helper.js
@@ -0,0 +1,22 @@
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+async function pollReports(endpoint, id) {
+ const res = await fetch(`${endpoint}?id=${id}`, {cache: 'no-store'});
+ const reports = [];
+ if (res.status === 200) {
+ for (const report of await res.json()) {
+ reports.push(report);
+ }
+ }
+ return reports;
+}
+
+function checkReportExists(reports, type, url) {
+ for (const report of reports) {
+ if (report.type !== type) continue;
+ if (report.body.sourceFile === url) return true;
+ }
+ assert_unreached(`A report of ${type} from ${url} is not found.`);
+}
diff --git a/third_party/blink/web_tests/external/wpt/reporting/resources/report.py b/third_party/blink/web_tests/external/wpt/reporting/resources/report.py
new file mode 100644
index 0000000..d31b474
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/reporting/resources/report.py
@@ -0,0 +1,17 @@
+import json
+
+def main(request, response):
+ key = request.GET.first('id')
+
+ # No CORS support for cross-origin reporting endpoints
+ if request.method == 'POST':
+ reports = request.server.stash.take(key) or []
+ for report in json.loads(request.body):
+ reports.append(report)
+ request.server.stash.put(key, reports)
+ return 'done'
+ if request.method == 'GET':
+ return json.dumps(request.server.stash.take(key) or [])
+
+ response.status = 400
+ return 'invalid method'
\ No newline at end of file