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,30 +1821,45 @@ void NavigationRequest::OnResponseStarted(
auto cross_origin_embedder_policy =
response_head_->cross_origin_embedder_policy;
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()) {
const auto& parent_coep = parent_frame->cross_origin_embedder_policy();
const auto& url = common_params_->url;
if (parent_coep.value ==
network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp) {
if (url.SchemeIsBlob() || url.SchemeIs(url::kDataScheme)) {
// Some special URLs not loaded using the network are inheriting the
// Cross-Origin-Embedder-Policy header from their parent.
cross_origin_embedder_policy.value =
network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp;
}
if (cross_origin_embedder_policy.value ==
network::mojom::CrossOriginEmbedderPolicyValue::kNone) {
OnRequestFailedInternal(network::URLLoaderCompletionStatus(
network::BlockedByResponseReason::
kCoepFrameResourceNeedsCoepHeader),
false /* skip_throttles */,
base::nullopt /* error_page_content */,
false /* collapse_frame */);
// DO NOT ADD CODE after this. The previous call to
// OnRequestFailedInternal has destroyed the NavigationRequest.
return;
constexpr auto kRequireCorp =
network::mojom::CrossOriginEmbedderPolicyValue::kRequireCorp;
constexpr auto kNone =
network::mojom::CrossOriginEmbedderPolicyValue::kNone;
// Some special URLs not loaded using the network are inheriting the
// Cross-Origin-Embedder-Policy header from their parent.
const bool has_allowed_scheme =
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);
}
OnRequestFailedInternal(network::URLLoaderCompletionStatus(
network::BlockedByResponseReason::
kCoepFrameResourceNeedsCoepHeader),
false /* skip_throttles */,
base::nullopt /* error_page_content */,
false /* collapse_frame */);
// DO NOT ADD CODE after this. The previous call to
// OnRequestFailedInternal has destroyed the NavigationRequest.
return;
}
}
......
......@@ -45,6 +45,26 @@ void CrossOriginEmbedderPolicyReporter::QueueCorpViolationReport(
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(
mojo::PendingReceiver<network::mojom::CrossOriginEmbedderPolicyReporter>
receiver) {
......
......@@ -48,6 +48,10 @@ class CONTENT_EXPORT CrossOriginEmbedderPolicyReporter final
mojo::PendingReceiver<network::mojom::CrossOriginEmbedderPolicyReporter>
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:
// See the class comment.
StoragePartition* const storage_partition_;
......
......@@ -10,12 +10,15 @@
#include "base/values.h"
#include "content/public/test/test_storage_partition.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 "testing/gtest/include/gtest/gtest.h"
namespace content {
namespace {
using network::CrossOriginEmbedderPolicy;
class TestNetworkContext : public network::TestNetworkContext {
public:
struct Report {
......@@ -55,20 +58,27 @@ class CrossOriginEmbedderPolicyReporterTest : public testing::Test {
StoragePartition* storage_partition() { return &storage_partition_; }
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);
dict.SetKey("type", base::Value("corp"));
dict.SetKey("blocked-url", base::Value(s));
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:
base::test::TaskEnvironment task_environment_;
TestNetworkContext network_context_;
TestStoragePartition storage_partition_;
};
TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpoints) {
TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpointsForCorp) {
const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
base::nullopt, base::nullopt);
......@@ -81,7 +91,7 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, NullEndpoints) {
EXPECT_TRUE(network_context().reports().empty());
}
TEST_F(CrossOriginEmbedderPolicyReporterTest, Basic) {
TEST_F(CrossOriginEmbedderPolicyReporterTest, BasicCorp) {
const GURL kContextUrl("https://example.com/path");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
"e1", "e2");
......@@ -99,14 +109,15 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, Basic) {
EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1");
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.group, "e2");
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");
CrossOriginEmbedderPolicyReporter reporter(storage_partition(), kContextUrl,
"e1", "e2");
......@@ -123,11 +134,11 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, UserAndPass) {
EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1");
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, CreateBody("https://www2.example.com/y"));
EXPECT_EQ(r2.body, CreateBodyForCorp("https://www2.example.com/y"));
}
TEST_F(CrossOriginEmbedderPolicyReporterTest, Clone) {
......@@ -152,11 +163,75 @@ TEST_F(CrossOriginEmbedderPolicyReporterTest, Clone) {
EXPECT_EQ(r1.type, "coep");
EXPECT_EQ(r1.group, "e1");
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.group, "e2");
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
......
......@@ -28,8 +28,8 @@ async function pollReports(endpoint, reports) {
}
}
let reports = []
let reportsForReportOnly = []
const reports = [];
const reportsForReportOnly = [];
pollReports('endpoint', reports);
pollReports('report-only-endpoint', reportsForReportOnly);
......@@ -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.`);
}
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) {
blockedUrl = new URL(blockedUrl, location).href;
contextUrl = new URL(contextUrl, location).href;
......@@ -87,7 +102,8 @@ async_test(async (t) => {
fetchInIframe(blockedDueToCoep);
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, blockedByPureCorp, iframe.src);
......@@ -121,16 +137,18 @@ async_test(async (t) => {
}
const suffix = 'navigation-corp';
const sameOrigin = `/common/blank.html?${suffix}`;
const blockedDueToCoep = `${REMOTE_ORIGIN}/common/blank.html?abc&${suffix}`;
const dest = `${REMOTE_ORIGIN}/common/blank.html?xyz&${suffix}`;
const coep = `pipe=header(cross-origin-embedder-policy,require-corp)`;
const sameOrigin = `/common/blank.html?${coep}&${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}`;
attachFrame(sameOrigin);
attachFrame(blockedDueToCoep);
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);
checkCorpReportExistence(reports, blockedDueToCoep, iframe.src);
......@@ -143,4 +161,98 @@ async_test(async (t) => {
}
}, '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