Commit 4d113f83 authored by Yutaka Hirano's avatar Yutaka Hirano Committed by Commit Bot

Implement COEP reporting for navigation

Implements https://github.com/mikewest/corpp/pull/10

Bug: 1052764
Change-Id: I76464069d3a0a4ea1463a2e626f5e0b355f175ee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2091326
Commit-Queue: Yutaka Hirano <yhirano@chromium.org>
Reviewed-by: default avatarMatt Falkenhagen <falken@chromium.org>
Cr-Commit-Position: refs/heads/master@{#749129}
parent b59742e7
...@@ -1821,20 +1821,36 @@ void NavigationRequest::OnResponseStarted( ...@@ -1821,20 +1821,36 @@ void NavigationRequest::OnResponseStarted(
auto cross_origin_embedder_policy = auto cross_origin_embedder_policy =
response_head_->cross_origin_embedder_policy; response_head_->cross_origin_embedder_policy;
if (base::FeatureList::IsEnabled(network::features::kCrossOriginIsolation)) { if (base::FeatureList::IsEnabled(network::features::kCrossOriginIsolation)) {
// https://mikewest.github.io/corpp/#integration-html // https://mikewest.github.io/corpp/#process-navigation-response
if (auto* const parent_frame = GetParentFrame()) { if (auto* const parent_frame = GetParentFrame()) {
const auto& parent_coep = parent_frame->cross_origin_embedder_policy(); const auto& parent_coep = parent_frame->cross_origin_embedder_policy();
const auto& url = common_params_->url; const auto& url = common_params_->url;
if (parent_coep.value == constexpr auto kRequireCorp =
network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp) { network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp;
if (url.SchemeIsBlob() || url.SchemeIs(url::kDataScheme)) { constexpr auto kNone =
network::mojom::CrossOriginEmbedderPolicyValue::kNone;
// Some special URLs not loaded using the network are inheriting the // Some special URLs not loaded using the network are inheriting the
// Cross-Origin-Embedder-Policy header from their parent. // Cross-Origin-Embedder-Policy header from their parent.
cross_origin_embedder_policy.value = const bool has_allowed_scheme =
network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp; url.SchemeIsBlob() || url.SchemeIs(url::kDataScheme);
if (parent_coep.value == kRequireCorp && has_allowed_scheme) {
cross_origin_embedder_policy.value = kRequireCorp;
}
auto* const coep_reporter = parent_frame->coep_reporter();
if (parent_coep.report_only_value == kRequireCorp &&
!has_allowed_scheme && cross_origin_embedder_policy.value == kNone &&
coep_reporter) {
coep_reporter->QueueNavigationReport(redirect_chain_[0],
/*report_only=*/true);
}
if (parent_coep.value == kRequireCorp &&
cross_origin_embedder_policy.value == kNone) {
if (coep_reporter) {
coep_reporter->QueueNavigationReport(redirect_chain_[0],
/*report_only=*/false);
} }
if (cross_origin_embedder_policy.value ==
network::mojom::CrossOriginEmbedderPolicyValue::kNone) {
OnRequestFailedInternal(network::URLLoaderCompletionStatus( OnRequestFailedInternal(network::URLLoaderCompletionStatus(
network::BlockedByResponseReason:: network::BlockedByResponseReason::
kCoepFrameResourceNeedsCoepHeader), kCoepFrameResourceNeedsCoepHeader),
...@@ -1846,7 +1862,6 @@ void NavigationRequest::OnResponseStarted( ...@@ -1846,7 +1862,6 @@ void NavigationRequest::OnResponseStarted(
return; return;
} }
} }
}
// The Cross-Origin-Opener-Policy header should be ignored if delivered in // The Cross-Origin-Opener-Policy header should be ignored if delivered in
// insecure contexts. // insecure contexts.
......
...@@ -45,6 +45,26 @@ void CrossOriginEmbedderPolicyReporter::QueueCorpViolationReport( ...@@ -45,6 +45,26 @@ void CrossOriginEmbedderPolicyReporter::QueueCorpViolationReport(
std::move(body)); std::move(body));
} }
void CrossOriginEmbedderPolicyReporter::QueueNavigationReport(
const GURL& blocked_url,
bool report_only) {
const base::Optional<std::string>& endpoint =
report_only ? report_only_endpoint_ : endpoint_;
if (!endpoint) {
return;
}
url::Replacements<char> replacements;
replacements.ClearUsername();
replacements.ClearPassword();
base::DictionaryValue body;
body.SetString("type", "navigation");
body.SetString("blocked-url",
blocked_url.ReplaceComponents(replacements).spec());
storage_partition_->GetNetworkContext()->QueueReport(
"coep", *endpoint, context_url_, /*user_agent=*/base::nullopt,
std::move(body));
}
void CrossOriginEmbedderPolicyReporter::Clone( void CrossOriginEmbedderPolicyReporter::Clone(
mojo::PendingReceiver<network::mojom::CrossOriginEmbedderPolicyReporter> mojo::PendingReceiver<network::mojom::CrossOriginEmbedderPolicyReporter>
receiver) { receiver) {
......
...@@ -48,6 +48,10 @@ class CONTENT_EXPORT CrossOriginEmbedderPolicyReporter final ...@@ -48,6 +48,10 @@ class CONTENT_EXPORT CrossOriginEmbedderPolicyReporter final
mojo::PendingReceiver<network::mojom::CrossOriginEmbedderPolicyReporter> mojo::PendingReceiver<network::mojom::CrossOriginEmbedderPolicyReporter>
receiver) override; receiver) override;
// https://mikewest.github.io/corpp/#abstract-opdef-queue-coep-navigation-violation
// Queue a violation report for COEP mismatch for nested frame navigation.
void QueueNavigationReport(const GURL& blocked_url, bool report_only);
private: private:
// See the class comment. // See the class comment.
StoragePartition* const storage_partition_; StoragePartition* const storage_partition_;
......
...@@ -10,12 +10,15 @@ ...@@ -10,12 +10,15 @@
#include "base/values.h" #include "base/values.h"
#include "content/public/test/test_storage_partition.h" #include "content/public/test/test_storage_partition.h"
#include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/cpp/cross_origin_embedder_policy.h"
#include "services/network/test/test_network_context.h" #include "services/network/test/test_network_context.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
namespace content { namespace content {
namespace { namespace {
using network::CrossOriginEmbedderPolicy;
class TestNetworkContext : public network::TestNetworkContext { class TestNetworkContext : public network::TestNetworkContext {
public: public:
struct Report { struct Report {
...@@ -55,20 +58,27 @@ class CrossOriginEmbedderPolicyReporterTest : public testing::Test { ...@@ -55,20 +58,27 @@ class CrossOriginEmbedderPolicyReporterTest : public testing::Test {
StoragePartition* storage_partition() { return &storage_partition_; } StoragePartition* storage_partition() { return &storage_partition_; }
const TestNetworkContext& network_context() const { return network_context_; } const TestNetworkContext& network_context() const { return network_context_; }
base::Value CreateBody(base::StringPiece s) { base::Value CreateBodyForCorp(base::StringPiece s) {
base::Value dict(base::Value::Type::DICTIONARY); base::Value dict(base::Value::Type::DICTIONARY);
dict.SetKey("type", base::Value("corp")); dict.SetKey("type", base::Value("corp"));
dict.SetKey("blocked-url", base::Value(s)); dict.SetKey("blocked-url", base::Value(s));
return dict; return dict;
} }
base::Value CreateBodyForNavigation(base::StringPiece s) {
base::Value dict(base::Value::Type::DICTIONARY);
dict.SetKey("type", base::Value("navigation"));
dict.SetKey("blocked-url", base::Value(s));
return dict;
}
private: private:
base::test::TaskEnvironment task_environment_; base::test::TaskEnvironment task_environment_;
TestNetworkContext network_context_; TestNetworkContext network_context_;
TestStoragePartition storage_partition_; TestStoragePartition storage_partition_;
}; };
TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpoints) { TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpointsForCorp) {
const GURL kContextUrl("https://example.com/path"); const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl, CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
base::nullopt, base::nullopt); base::nullopt, base::nullopt);
...@@ -81,7 +91,7 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpoints) { ...@@ -81,7 +91,7 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpoints) {
EXPECT_TRUE(network_context().reports().empty()); EXPECT_TRUE(network_context().reports().empty());
} }
TEST_F(CrossOriginEmbedderPolicyReporterTest, Basic) { TEST_F(CrossOriginEmbedderPolicyReporterTest, BasicCorp) {
const GURL kContextUrl("https://example.com/path"); const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl, CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
"e1", "e2"); "e1", "e2");
...@@ -99,14 +109,15 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, Basic) { ...@@ -99,14 +109,15 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, Basic) {
EXPECT_EQ(r1.type, "coep"); EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1"); EXPECT_EQ(r1.group, "e1");
EXPECT_EQ(r1.url, kContextUrl); EXPECT_EQ(r1.url, kContextUrl);
EXPECT_EQ(r1.body, CreateBody("https://www1.example.com/x#foo?bar=baz")); EXPECT_EQ(r1.body,
CreateBodyForCorp("https://www1.example.com/x#foo?bar=baz"));
EXPECT_EQ(r2.type, "coep"); EXPECT_EQ(r2.type, "coep");
EXPECT_EQ(r2.group, "e2"); EXPECT_EQ(r2.group, "e2");
EXPECT_EQ(r2.url, kContextUrl); EXPECT_EQ(r2.url, kContextUrl);
EXPECT_EQ(r2.body, CreateBody("http://www2.example.com:41/y")); EXPECT_EQ(r2.body, CreateBodyForCorp("http://www2.example.com:41/y"));
} }
TEST_F(CrossOriginEmbedderPolicyReporterTest, UserAndPass) { TEST_F(CrossOriginEmbedderPolicyReporterTest, UserAndPassForCorp) {
const GURL kContextUrl("https://example.com/path"); const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl, CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
"e1", "e2"); "e1", "e2");
...@@ -123,11 +134,11 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, UserAndPass) { ...@@ -123,11 +134,11 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, UserAndPass) {
EXPECT_EQ(r1.type, "coep"); EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1"); EXPECT_EQ(r1.group, "e1");
EXPECT_EQ(r1.url, kContextUrl); EXPECT_EQ(r1.url, kContextUrl);
EXPECT_EQ(r1.body, CreateBody("https://www1.example.com/x")); EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x"));
EXPECT_EQ(r2.type, "coep"); EXPECT_EQ(r2.type, "coep");
EXPECT_EQ(r2.group, "e2"); EXPECT_EQ(r2.group, "e2");
EXPECT_EQ(r2.url, kContextUrl); EXPECT_EQ(r2.url, kContextUrl);
EXPECT_EQ(r2.body, CreateBody("https://www2.example.com/y")); EXPECT_EQ(r2.body, CreateBodyForCorp("https://www2.example.com/y"));
} }
TEST_F(CrossOriginEmbedderPolicyReporterTest, Clone) { TEST_F(CrossOriginEmbedderPolicyReporterTest, Clone) {
...@@ -152,11 +163,75 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, Clone) { ...@@ -152,11 +163,75 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, Clone) {
EXPECT_EQ(r1.type, "coep"); EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1"); EXPECT_EQ(r1.group, "e1");
EXPECT_EQ(r1.url, kContextUrl); EXPECT_EQ(r1.url, kContextUrl);
EXPECT_EQ(r1.body, CreateBody("https://www1.example.com/x")); EXPECT_EQ(r1.body, CreateBodyForCorp("https://www1.example.com/x"));
EXPECT_EQ(r2.type, "coep");
EXPECT_EQ(r2.group, "e2");
EXPECT_EQ(r2.url, kContextUrl);
EXPECT_EQ(r2.body, CreateBodyForCorp("https://www2.example.com/y"));
}
TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpointsForNavigation) {
const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
base::nullopt, base::nullopt);
reporter.QueueNavigationReport(GURL("https://www1.example.com/y"),
/*report_only=*/false);
reporter.QueueNavigationReport(GURL("https://www2.example.com/x"),
/*report_only=*/true);
EXPECT_TRUE(network_context().reports().empty());
}
TEST_F(CrossOriginEmbedderPolicyReporterTest, BasicNavigation) {
const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
"e1", "e2");
CrossOriginEmbedderPolicy child_coep;
child_coep.report_only_value =
network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp;
reporter.QueueNavigationReport(GURL("https://www1.example.com/x#foo?bar=baz"),
/*report_only=*/false);
reporter.QueueNavigationReport(GURL("http://www2.example.com:41/y"),
/*report_only=*/true);
ASSERT_EQ(2u, network_context().reports().size());
const Report& r1 = network_context().reports()[0];
const Report& r2 = network_context().reports()[1];
EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1");
EXPECT_EQ(r1.url, kContextUrl);
EXPECT_EQ(r1.body,
CreateBodyForNavigation("https://www1.example.com/x#foo?bar=baz"));
EXPECT_EQ(r2.type, "coep");
EXPECT_EQ(r2.group, "e2");
EXPECT_EQ(r2.url, kContextUrl);
EXPECT_EQ(r2.body, CreateBodyForNavigation("http://www2.example.com:41/y"));
}
TEST_F(CrossOriginEmbedderPolicyReporterTest, UserAndPassForNavigation) {
const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
"e1", "e2");
reporter.QueueNavigationReport(GURL("https://u:p@www1.example.com/x"),
/*report_only=*/false);
reporter.QueueNavigationReport(GURL("https://u:p@www2.example.com/y"),
/*report_only=*/true);
ASSERT_EQ(2u, network_context().reports().size());
const Report& r1 = network_context().reports()[0];
const Report& r2 = network_context().reports()[1];
EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1");
EXPECT_EQ(r1.url, kContextUrl);
EXPECT_EQ(r1.body, CreateBodyForNavigation("https://www1.example.com/x"));
EXPECT_EQ(r2.type, "coep"); EXPECT_EQ(r2.type, "coep");
EXPECT_EQ(r2.group, "e2"); EXPECT_EQ(r2.group, "e2");
EXPECT_EQ(r2.url, kContextUrl); EXPECT_EQ(r2.url, kContextUrl);
EXPECT_EQ(r2.body, CreateBody("https://www2.example.com/y")); EXPECT_EQ(r2.body, CreateBodyForNavigation("https://www2.example.com/y"));
} }
} // namespace } // namespace
......
...@@ -28,8 +28,8 @@ async function pollReports(endpoint, reports) { ...@@ -28,8 +28,8 @@ async function pollReports(endpoint, reports) {
} }
} }
let reports = [] const reports = [];
let reportsForReportOnly = [] const reportsForReportOnly = [];
pollReports('endpoint', reports); pollReports('endpoint', reports);
pollReports('report-only-endpoint', reportsForReportOnly); pollReports('report-only-endpoint', reportsForReportOnly);
...@@ -48,6 +48,21 @@ function checkCorpReportExistence(reports, blockedUrl, contextUrl) { ...@@ -48,6 +48,21 @@ function checkCorpReportExistence(reports, blockedUrl, contextUrl) {
assert_unreached(`A report whose blocked-url is ${blockedUrl} and url is ${contextUrl} is not found.`); assert_unreached(`A report whose blocked-url is ${blockedUrl} and url is ${contextUrl} is not found.`);
} }
function checkNavigationReportExistence(reports, blockedUrl, contextUrl) {
blockedUrl = new URL(blockedUrl, location).href;
contextUrl = new URL(contextUrl, location).href;
for (const report of reports) {
if (report.type !== 'coep' || report.url !== contextUrl ||
report.body.type !== 'navigation') {
continue;
}
if (report.body['blocked-url'] === blockedUrl) {
return;
}
}
assert_unreached(`A report whose blocked-url is ${blockedUrl} and url is ${contextUrl} is not found.`);
}
function checkReportNonExistence(reports, blockedUrl, contextUrl) { function checkReportNonExistence(reports, blockedUrl, contextUrl) {
blockedUrl = new URL(blockedUrl, location).href; blockedUrl = new URL(blockedUrl, location).href;
contextUrl = new URL(contextUrl, location).href; contextUrl = new URL(contextUrl, location).href;
...@@ -87,7 +102,8 @@ async_test(async (t) => { ...@@ -87,7 +102,8 @@ async_test(async (t) => {
fetchInIframe(blockedDueToCoep); fetchInIframe(blockedDueToCoep);
fetchInIframe(redirect); fetchInIframe(redirect);
await new Promise(resolve => t.step_timeout(resolve, 3000)); // Wait 3 seconds for reports to settle.
await wait(3000);
checkReportNonExistence(reports, sameOriginUrl, iframe.src); checkReportNonExistence(reports, sameOriginUrl, iframe.src);
checkReportNonExistence(reports, blockedByPureCorp, iframe.src); checkReportNonExistence(reports, blockedByPureCorp, iframe.src);
...@@ -121,16 +137,18 @@ async_test(async (t) => { ...@@ -121,16 +137,18 @@ async_test(async (t) => {
} }
const suffix = 'navigation-corp'; const suffix = 'navigation-corp';
const sameOrigin = `/common/blank.html?${suffix}`; const coep = `pipe=header(cross-origin-embedder-policy,require-corp)`;
const blockedDueToCoep = `${REMOTE_ORIGIN}/common/blank.html?abc&${suffix}`; const sameOrigin = `/common/blank.html?${coep}&${suffix}`;
const dest = `${REMOTE_ORIGIN}/common/blank.html?xyz&${suffix}`; const blockedDueToCoep = `${REMOTE_ORIGIN}/common/blank.html?${coep}&${suffix}-a`;
const dest = `${REMOTE_ORIGIN}/common/blank.html?${coep}&${suffix}-b`;
const redirect = `/common/redirect.py?location=${encodeURIComponent(dest)}&${suffix}`; const redirect = `/common/redirect.py?location=${encodeURIComponent(dest)}&${suffix}`;
attachFrame(sameOrigin); attachFrame(sameOrigin);
attachFrame(blockedDueToCoep); attachFrame(blockedDueToCoep);
attachFrame(redirect); attachFrame(redirect);
await new Promise(resolve => t.step_timeout(resolve, 3000)); // Wait 3 seconds for reports to settle.
await wait(3000);
checkReportNonExistence(reports, sameOrigin, iframe.src); checkReportNonExistence(reports, sameOrigin, iframe.src);
checkCorpReportExistence(reports, blockedDueToCoep, iframe.src); checkCorpReportExistence(reports, blockedDueToCoep, iframe.src);
...@@ -143,4 +161,98 @@ async_test(async (t) => { ...@@ -143,4 +161,98 @@ async_test(async (t) => {
} }
}, 'navigation CORP'); }, 'navigation CORP');
</script> async_test(async (t) => {
try {
const iframe = document.createElement('iframe');
t.add_cleanup(() => iframe.remove());
const suffix = '&navigation-coep';
const corp = 'header(cross-origin-resource-policy,cross-origin)';
const noCoep = `pipe=${corp}`;
const coep =
`pipe=header(cross-origin-embedder-policy,require-corp%3breport-to=%22endpoint%22)|${corp}`;
const coepReportOnly =
`pipe=header(cross-origin-embedder-policy-report-only,require-corp%3breport-to=%22report-only-endpoint%22)|${corp}`;
const path = `/common/blank.html`;
const pipes = [noCoep, coep, coepReportOnly];
const settings = new Map();
settings.set(noCoep, {
pipe: noCoep,
value: 'unsafe-none',
reportOnlyValue: 'unsafe-none',
});
settings.set(coep, {
pipe: coep,
value: 'require-corp',
reportOnlyValue: 'unsafe-none',
});
settings.set(coepReportOnly, {
pipe: coepReportOnly,
value: 'unsafe-none',
reportOnlyValue: 'require-corp',
});
function genUrl(pipe) {
return `${path}?${pipe}${suffix}`;
}
for (const outer of settings.keys()) {
for (const inner of settings.keys()) {
const iframe = document.createElement('iframe');
t.add_cleanup(() => iframe.remove());
iframe.src = genUrl(outer);
iframe.addEventListener('load', () => {
const w = iframe.contentWindow;
const d = iframe.contentDocument;
const nested = d.createElement('iframe');
nested.src = genUrl(inner) + '-nested';
d.body.appendChild(nested);
}, {once: true});
document.body.appendChild(iframe);
}
}
// Wait 3 seconds for reports to settle.
await wait(3000);
function check(rs, inner, outer) {
checkNavigationReportExistence(
rs, genUrl(inner) + '-nested', genUrl(outer));
}
function checkNoReport(reports, inner, outer) {
checkReportNonExistence(
reports, genUrl(inner) + '-nested', genUrl(outer));
}
// outer === noCoep
checkNoReport(reports, noCoep, noCoep);
checkNoReport(reports, coep, noCoep);
checkNoReport(reports, coepReportOnly, noCoep);
checkNoReport(reportsForReportOnly, noCoep, noCoep);
checkNoReport(reportsForReportOnly, coep, noCoep);
checkNoReport(reportsForReportOnly, coepReportOnly, noCoep);
// outer === coep
check(reports, noCoep, coep);
checkNoReport(reports, coep, coep);
check(reports, coepReportOnly, coep);
checkNoReport(reportsForReportOnly, noCoep, coep);
checkNoReport(reportsForReportOnly, coep, coep);
checkNoReport(reportsForReportOnly, coepReportOnly, coep);
// outer === coepReportOnly
checkNoReport(reports, noCoep, coepReportOnly);
checkNoReport(reports, coep, coepReportOnly);
checkNoReport(reports, coepReportOnly, coepReportOnly);
check(reportsForReportOnly, noCoep, coepReportOnly);
checkNoReport(reportsForReportOnly, coep, coepReportOnly);
check(reportsForReportOnly, coepReportOnly, coepReportOnly);
t.done();
} catch (e) {
t.step(() => { throw e });
}
}, 'COEP violation on nested frame navigation');
</script>$
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment