Commit 03505df6 authored by Daniel Vogelheim's avatar Daniel Vogelheim Committed by Commit Bot

[Origin Policy] Implement error reporting via Reporting API

Bug: 751996
Change-Id: I44dfc36dcec55efd2c68d02c06824fcbb94c879d
Reviewed-on: https://chromium-review.googlesource.com/c/1464301Reviewed-by: default avatarMike West <mkwst@chromium.org>
Reviewed-by: default avatarMatt Menke <mmenke@chromium.org>
Reviewed-by: default avatarAlex Moshchuk <alexmos@chromium.org>
Commit-Queue: Daniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/master@{#635961}
parent daadd52a
HTTP/1.0 200 OK
Content-Type: text/html
Sec-Origin-Policy: policy-with-301redirect
Sec-Origin-Policy: policy=policy-with-301redirect
HTTP/1.0 200 OK
Content-Type: text/html
Sec-Origin-Policy: policy-with-302redirect
Sec-Origin-Policy: policy=policy-with-302redirect
HTTP/1.0 200 OK
Content-Type: text/html
Sec-Origin-Policy: policy-with-307redirect
Sec-Origin-Policy: policy=policy-with-307redirect
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Sec-Origin-Policy: this-policy-is-not-there
Sec-Origin-Policy: policy=this-policy-is-not-there
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Sec-Origin-Policy: example-policy
Sec-Origin-Policy: policy=example-policy
......@@ -5,10 +5,13 @@
#include "content/browser/frame_host/origin_policy_throttle.h"
#include "content/public/browser/origin_policy_commands.h"
#include <utility>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/no_destructor.h"
#include "build/buildflag.h"
#include "content/browser/frame_host/navigation_handle_impl.h"
#include "content/browser/frame_host/navigation_request.h"
#include "content/public/browser/browser_context.h"
......@@ -20,6 +23,8 @@
#include "content/public/common/content_switches.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_util.h"
#include "services/network/network_context.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/origin.h"
......@@ -28,6 +33,8 @@ namespace {
static const char* kDefaultPolicy = "0";
static const char* kDeletePolicy = "0";
static const char* kWellKnown = "/.well-known/origin-policy/";
static const char* kReportTo = "report-to";
static const char* kPolicy = "policy";
// Marker for (temporarily) exempted origins.
// TODO(vogelheim): Make sure this is outside the value space for policy
......@@ -132,10 +139,9 @@ OriginPolicyThrottle::WillProcessResponse() {
// - no header received, last-known version exists: Use last-known version
// - no header, no last-known version: No policy applies.
std::string response_version;
bool header_found =
navigation_handle()->GetResponseHeaders()->GetNormalizedHeader(
net::HttpRequestHeaders::kSecOriginPolicy, &response_version);
std::string response_version =
GetRequestedPolicyAndReportGroupFromHeader().policy_version;
bool header_found = !response_version.empty();
url::Origin origin = GetRequestOrigin();
DCHECK(!origin.Serialize().empty());
......@@ -167,13 +173,13 @@ OriginPolicyThrottle::WillProcessResponse() {
else
iter->second = response_version;
GURL policy = GURL(origin.Serialize() + kWellKnown + response_version);
FetchCallback done =
base::BindOnce(&OriginPolicyThrottle::OnTheGloriousPolicyHasArrived,
base::Unretained(this));
RedirectCallback redirect = base::BindRepeating(
&OriginPolicyThrottle::OnRedirect, base::Unretained(this));
FetchPolicy(policy, std::move(done), std::move(redirect));
FetchPolicy(GetPolicyURL(response_version), std::move(done),
std::move(redirect));
return NavigationThrottle::DEFER;
}
......@@ -196,10 +202,60 @@ OriginPolicyThrottle::GetKnownVersions() {
return *map_instance;
}
const url::Origin OriginPolicyThrottle::GetRequestOrigin() {
OriginPolicyThrottle::PolicyVersionAndReportTo OriginPolicyThrottle::
GetRequestedPolicyAndReportGroupFromHeaderStringForTesting(
const std::string& header) {
return GetRequestedPolicyAndReportGroupFromHeaderString(header);
}
OriginPolicyThrottle::PolicyVersionAndReportTo
OriginPolicyThrottle::GetRequestedPolicyAndReportGroupFromHeader() const {
std::string header;
navigation_handle()->GetResponseHeaders()->GetNormalizedHeader(
net::HttpRequestHeaders::kSecOriginPolicy, &header);
return GetRequestedPolicyAndReportGroupFromHeaderString(header);
}
OriginPolicyThrottle::PolicyVersionAndReportTo
OriginPolicyThrottle::GetRequestedPolicyAndReportGroupFromHeaderString(
const std::string& header) {
// Compatibility with early spec drafts, for safety reasons:
// A lonely "0" will be recognized, so that deletion of a policy always works.
if (net::HttpUtil::TrimLWS(header) == kDeletePolicy)
return PolicyVersionAndReportTo({kDeletePolicy, ""});
std::string policy;
std::string report_to;
bool valid = true;
net::HttpUtil::NameValuePairsIterator iter(header.cbegin(), header.cend(),
',');
while (iter.GetNext()) {
std::string token_value = net::HttpUtil::TrimLWS(iter.value()).as_string();
bool is_token = net::HttpUtil::IsToken(token_value);
if (iter.name() == kPolicy) {
valid &= is_token && policy.empty();
policy = token_value;
} else if (iter.name() == kReportTo) {
valid &= is_token && report_to.empty();
report_to = token_value;
}
}
valid &= iter.valid();
if (!valid)
return PolicyVersionAndReportTo();
return PolicyVersionAndReportTo({policy, report_to});
}
const url::Origin OriginPolicyThrottle::GetRequestOrigin() const {
return url::Origin::Create(navigation_handle()->GetURL());
}
const GURL OriginPolicyThrottle::GetPolicyURL(
const std::string& version) const {
return GURL(GetRequestOrigin().Serialize() + kWellKnown + version);
}
void OriginPolicyThrottle::FetchPolicy(const GURL& url,
FetchCallback done,
RedirectCallback redirect) {
......@@ -290,6 +346,53 @@ void OriginPolicyThrottle::CancelNavigation(OriginPolicyErrorReason reason) {
reason, navigation_handle());
CancelDeferredNavigation(NavigationThrottle::ThrottleCheckResult(
NavigationThrottle::CANCEL, net::ERR_BLOCKED_BY_CLIENT, error_page));
Report(reason);
}
#if BUILDFLAG(ENABLE_REPORTING)
void OriginPolicyThrottle::Report(OriginPolicyErrorReason reason) {
const PolicyVersionAndReportTo header_values =
GetRequestedPolicyAndReportGroupFromHeader();
if (header_values.report_to.empty())
return;
std::string user_agent;
navigation_handle()->GetRequestHeaders().GetHeader(
net::HttpRequestHeaders::kUserAgent, &user_agent);
std::string origin_policy_header;
navigation_handle()->GetResponseHeaders()->GetNormalizedHeader(
net::HttpRequestHeaders::kSecOriginPolicy, &origin_policy_header);
const char* reason_str = nullptr;
switch (reason) {
case OriginPolicyErrorReason::kCannotLoadPolicy:
reason_str = "CANNOT_LOAD";
break;
case OriginPolicyErrorReason::kPolicyShouldNotRedirect:
reason_str = "REDIRECT";
break;
case OriginPolicyErrorReason::kOther:
reason_str = "OTHER";
break;
}
base::DictionaryValue report_body;
report_body.SetKey(
"origin_policy_url",
base::Value(GetPolicyURL(header_values.policy_version).spec()));
report_body.SetKey("policy", base::Value(origin_policy_header));
report_body.SetKey("policy_error_reason", base::Value(reason_str));
SiteInstance* site_instance = navigation_handle()->GetStartingSiteInstance();
BrowserContext::GetStoragePartition(site_instance->GetBrowserContext(),
site_instance)
->GetNetworkContext()
->QueueReport("origin-policy", header_values.report_to,
navigation_handle()->GetURL(), user_agent,
std::move(report_body));
}
#else
void OriginPolicyThrottle::Report(OriginPolicyErrorReason reason) {}
#endif // BUILDFLAG(ENABLE_REPORTING)
} // namespace content
......@@ -43,6 +43,11 @@ enum class OriginPolicyErrorReason;
// throttle or not.
class CONTENT_EXPORT OriginPolicyThrottle : public NavigationThrottle {
public:
struct PolicyVersionAndReportTo {
std::string policy_version;
std::string report_to;
};
// Determine whether to request a policy (or advertise origin policy
// support) and which version.
// Returns whether the policy header should be sent. It it returns true,
......@@ -71,6 +76,9 @@ class CONTENT_EXPORT OriginPolicyThrottle : public NavigationThrottle {
static KnownVersionMap& GetKnownVersionsForTesting();
void InjectPolicyForTesting(const std::string& policy_content);
static PolicyVersionAndReportTo
GetRequestedPolicyAndReportGroupFromHeaderStringForTesting(
const std::string& header);
private:
using FetchCallback = base::OnceCallback<void(std::unique_ptr<std::string>)>;
......@@ -83,7 +91,13 @@ class CONTENT_EXPORT OriginPolicyThrottle : public NavigationThrottle {
static KnownVersionMap& GetKnownVersions();
const url::Origin GetRequestOrigin();
// Get the policy name and the reporting group from the header string.
PolicyVersionAndReportTo GetRequestedPolicyAndReportGroupFromHeader() const;
static PolicyVersionAndReportTo
GetRequestedPolicyAndReportGroupFromHeaderString(const std::string& header);
const url::Origin GetRequestOrigin() const;
const GURL GetPolicyURL(const std::string& version) const;
void FetchPolicy(const GURL& url,
FetchCallback done,
RedirectCallback redirect);
......@@ -94,6 +108,8 @@ class CONTENT_EXPORT OriginPolicyThrottle : public NavigationThrottle {
std::vector<std::string>* to_be_removed_headers);
void CancelNavigation(OriginPolicyErrorReason reason);
void Report(OriginPolicyErrorReason reason);
// We may need the SimpleURLLoader to download the policy. The loader must
// be kept alive while the load is ongoing.
std::unique_ptr<network::SimpleURLLoader> url_loader_;
......
......@@ -120,7 +120,8 @@ TEST_P(OriginPolicyThrottleTest, RunRequestEndToEnd) {
// Fake a response with a policy header. Check whether the navigation
// is deferred.
const char* raw_headers = "HTTP/1.1 200 OK\nSec-Origin-Policy: policy-1\n\n";
const char* raw_headers =
"HTTP/1.1 200 OK\nSec-Origin-Policy: policy=policy-1\n\n";
scoped_refptr<net::HttpResponseHeaders> headers =
new net::HttpResponseHeaders(
net::HttpUtil::AssembleRawHeaders(raw_headers, strlen(raw_headers)));
......@@ -176,7 +177,8 @@ TEST_P(OriginPolicyThrottleTest, AddExceptionEndToEnd) {
navigation->GetLastThrottleCheckResult().action());
// Fake a response with a policy header.
const char* raw_headers = "HTTP/1.1 200 OK\nSec-Origin-Policy: policy-1\n\n";
const char* raw_headers =
"HTTP/1.1 200 OK\nSec-Origin-Policy: policy=policy-1\n\n";
scoped_refptr<net::HttpResponseHeaders> headers =
new net::HttpResponseHeaders(
net::HttpUtil::AssembleRawHeaders(raw_headers, strlen(raw_headers)));
......@@ -195,4 +197,70 @@ TEST_P(OriginPolicyThrottleTest, AddExceptionEndToEnd) {
EXPECT_EQ(version, "0");
}
TEST(OriginPolicyThrottleTest, ParseHeaders) {
const struct {
const char* header;
const char* policy_version;
const char* report_to;
} testcases[] = {
// The common cases: We expect >99% of headers to look like these:
{"policy=policy", "policy", ""},
{"policy=policy, report-to=endpoint", "policy", "endpoint"},
// Delete a policy. This better work.
{"0", "0", ""},
{"policy=0", "0", ""},
{"policy=\"0\"", "0", ""},
{"policy=0, report-to=endpoint", "0", "endpoint"},
// Order, please!
{"policy=policy, report-to=endpoint", "policy", "endpoint"},
{"report-to=endpoint, policy=policy", "policy", "endpoint"},
// Quoting:
{"policy=\"policy\"", "policy", ""},
{"policy=\"policy\", report-to=endpoint", "policy", "endpoint"},
{"policy=\"policy\", report-to=\"endpoint\"", "policy", "endpoint"},
{"policy=policy, report-to=\"endpoint\"", "policy", "endpoint"},
// Whitespace, and funky but valid syntax:
{" policy = policy ", "policy", ""},
{" policy = \t policy ", "policy", ""},
{" policy \t= \t \"policy\" ", "policy", ""},
{" policy = \" policy \" ", "policy", ""},
{" , policy = policy , report-to=endpoint , ", "policy", "endpoint"},
// Valid policy, invalid report-to:
{"policy=policy, report-to endpoint", "", ""},
{"policy=policy, report-to=here, report-to=there", "", ""},
{"policy=policy, \"report-to\"=endpoint", "", ""},
// Invalid policy, valid report-to:
{"policy=policy1, policy=policy2", "", ""},
{"policy, report-to=r", "", ""},
{"report-to=endpoint", "", "endpoint"},
// Invalid everything:
{"one two three", "", ""},
{"one, two, three", "", ""},
{"policy report-to=endpoint", "", ""},
{"policy=policy report-to=endpoint", "", ""},
// Forward compatibility, ignore unknown keywords:
{"policy=pol, report-to=endpoint, unknown=keyword", "pol", "endpoint"},
{"unknown=keyword, policy=pol, report-to=endpoint", "pol", "endpoint"},
{"policy=pol, unknown=keyword", "pol", ""},
{"policy=policy, report_to=endpoint", "policy", ""},
{"policy=policy, reportto=endpoint", "policy", ""},
};
for (const auto& testcase : testcases) {
SCOPED_TRACE(testcase.header);
const auto result = OriginPolicyThrottle::
GetRequestedPolicyAndReportGroupFromHeaderStringForTesting(
testcase.header);
EXPECT_EQ(result.policy_version, testcase.policy_version);
EXPECT_EQ(result.report_to, testcase.report_to);
}
}
} // namespace content
<!DOCTYPE HTML>
<html>
<head>
<title>Test that Origin Policy report-to are deliverd to the declared reporting group</title>
<script src='/resources/testharness.js'></script>
<script src='/resources/testharnessreport.js'></script>
</head>
<body>
<iframe id="frame" src="about:blank"></iframe>
<script>
// Navigate the frame to a URL that declares an (invalid) origin policy with
// a report-to directive.
document.getElementById("frame").src =
"https://{{hosts[alt][]}}:{{ports[https][0]}}/origin-policy/sec-origin-policy-subframe.html";
</script>
<script async defer src='/content-security-policy/support/checkReport.sub.js?reportField=policy&reportValue=nonexistingpolicy'>
// This re-uses CSP reporting test infrastructure, and contains the actual
// test. In addition to sanity checks, it will check whether the report body
// contains a key/value pair as indicated by reportField and reportValue.
</script>
</body>
</html>
Set-Cookie: origin-policy-report-to=5b4d35b6-0771-46fe-8700-ed2bb59ed4be; Path=/origin-policy/
......@@ -11,7 +11,7 @@ def main(request, response):
response_policy = request.GET.first("policy", default="")
if request_policy and response_policy:
response.headers.set(origin_policy_header, response_policy)
response.headers.set(origin_policy_header, "policy=%s" % response_policy)
response.headers.set("Vary", "sec-origin-policy")
response.headers.set("Content-Type", "text/html");
......
The forbidden frame.
Content shouldn't matter, because this frame shouldn't be loaded.
So there.
Report-To: { "group": "report-to-group", "max_age": 1000, "endpoints": [{ "url": "https://{{hosts[alt][]}}:{{ports[https][0]}}/content-security-policy/support/report.py?op=put&reportID=5b4d35b6-0771-46fe-8700-ed2bb59ed4be" }] }
Sec-Origin-Policy: policy=nonexistingpolicy, report-to=report-to-group
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