Commit 1b96d2a6 authored by Antonio Sartori's avatar Antonio Sartori Committed by Commit Bot

Implement CSPEE subsumption algorithm in services/network

This CL completes the implementation of the Content Security Policy:
Embedded Enforcement subsumption algorithm following
https://w3c.github.io/webappsec-cspee/#subsume-policy
in the services/network Content Security Policy module.

Bug: 1094909
Change-Id: I880d4b31044a15dffe054ed83c2080e302f6906c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2315348
Commit-Queue: Antonio Sartori <antoniosartori@chromium.org>
Reviewed-by: default avatarMike West <mkwst@chromium.org>
Reviewed-by: default avatarArthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#803849}
parent b26a6063
......@@ -165,8 +165,9 @@ NavigationThrottle::ThrottleCheckResult AncestorThrottle::WillStartRequest() {
request->frame_tree_node()->parent()->required_csp();
std::string error_message;
if (!network::IsValidRequiredCSPAttr(frame_csp, parent_required_csp,
error_message)) {
if (!network::IsValidRequiredCSPAttr(
frame_csp, parent_required_csp,
url::Origin::Create(navigation_handle()->GetURL()), error_message)) {
if (frame_csp[0]) {
navigation_handle()->GetParentFrame()->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
......
......@@ -802,6 +802,19 @@ void AddContentSecurityPolicyFromHeader(base::StringPiece header,
}
}
std::pair<CSPDirectiveName, const mojom::CSPSourceList*> GetSourceList(
CSPDirectiveName directive,
const mojom::ContentSecurityPolicy& policy) {
for (CSPDirectiveName effective_directive = directive;
effective_directive != CSPDirectiveName::Unknown;
effective_directive = CSPFallback(effective_directive, directive)) {
auto value = policy.directives.find(effective_directive);
if (value != policy.directives.end())
return std::make_pair(effective_directive, value->second.get());
}
return std::make_pair(CSPDirectiveName::Unknown, nullptr);
}
} // namespace
void AddContentSecurityPolicyFromHeaders(
......@@ -947,6 +960,7 @@ void UpgradeInsecureRequest(GURL* url) {
bool IsValidRequiredCSPAttr(
const std::vector<mojom::ContentSecurityPolicyPtr>& policy,
const mojom::ContentSecurityPolicy* context,
const url::Origin& origin,
std::string& error_message) {
DCHECK(policy.size() == 1);
if (!policy[0])
......@@ -967,7 +981,7 @@ bool IsValidRequiredCSPAttr(
return false;
}
if (context && !Subsumes(*context, policy)) {
if (context && !Subsumes(*context, policy, origin)) {
error_message =
"The csp attribute Content-Security-Policy is not subsumed by the "
"frame's parent csp attribute Content-Security-Policy.";
......@@ -978,28 +992,55 @@ bool IsValidRequiredCSPAttr(
}
bool Subsumes(const mojom::ContentSecurityPolicy& policy_a,
const std::vector<mojom::ContentSecurityPolicyPtr>& policies_b) {
const std::vector<mojom::ContentSecurityPolicyPtr>& policies_b,
const url::Origin& origin_b) {
if (policy_a.directives.empty())
return true;
if (policy_a.header->type == mojom::ContentSecurityPolicyType::kReport)
return true;
// TODO(antoniosartori): Complete the implementation of this function
return util::ranges::all_of(
policy_a.directives, [&policies_b](const auto& directive_a) {
return util::ranges::any_of(
policies_b, [&directive_a](const auto& policy_b) {
if (policy_b->header->type ==
mojom::ContentSecurityPolicyType::kReport) {
return false;
}
auto value_b = policy_b->directives.find(directive_a.first);
return value_b != policy_b->directives.end() &&
value_b->second == directive_a.second;
});
});
if (policies_b.empty())
return false;
// A list of directives that we consider for subsumption.
// See more about source lists here:
// https://w3c.github.io/webappsec-csp/#framework-directive-source-list
static const CSPDirectiveName directives[] = {
CSPDirectiveName::ChildSrc, CSPDirectiveName::ConnectSrc,
CSPDirectiveName::FontSrc, CSPDirectiveName::FrameSrc,
CSPDirectiveName::ImgSrc, CSPDirectiveName::ManifestSrc,
CSPDirectiveName::MediaSrc, CSPDirectiveName::ObjectSrc,
CSPDirectiveName::ScriptSrc, CSPDirectiveName::ScriptSrcAttr,
CSPDirectiveName::ScriptSrcElem, CSPDirectiveName::StyleSrc,
CSPDirectiveName::StyleSrcAttr, CSPDirectiveName::StyleSrcElem,
CSPDirectiveName::WorkerSrc, CSPDirectiveName::BaseURI,
CSPDirectiveName::FrameAncestors, CSPDirectiveName::FormAction,
CSPDirectiveName::NavigateTo};
return util::ranges::all_of(directives, [&](CSPDirectiveName directive) {
auto required = GetSourceList(directive, policy_a);
if (!required.second)
return true;
// Aggregate all serialized source lists of the returned CSP into a vector
// based on a directive type, defaulting accordingly (for example, to
// `default-src`).
std::vector<const mojom::CSPSourceList*> returned;
for (const auto& policy_b : policies_b) {
// Ignore report-only returned policies.
if (policy_b->header->type == mojom::ContentSecurityPolicyType::kReport)
continue;
auto source_list = GetSourceList(directive, *policy_b);
if (source_list.second)
returned.push_back(source_list.second);
}
// TODO(amalika): Add checks for plugin-types, sandbox, disown-opener,
// navigation-to, worker-src.
return CSPSourceListSubsumes(*required.second, returned, required.first,
origin_b);
});
}
CSPDirectiveName ToCSPDirectiveName(const std::string& name) {
......
......@@ -11,6 +11,10 @@
class GURL;
namespace url {
class Origin;
}
namespace net {
class HttpResponseHeaders;
}
......@@ -79,13 +83,16 @@ COMPONENT_EXPORT(NETWORK_CPP)
bool IsValidRequiredCSPAttr(
const std::vector<mojom::ContentSecurityPolicyPtr>& policy,
const mojom::ContentSecurityPolicy* context,
const url::Origin& url,
std::string& error_message);
// Checks whether |policy_a| subsumes the policy list |policies_b| according to
// the algorithm https://w3c.github.io/webappsec-cspee/#subsume-policy-list.
// Checks whether |policy_a| subsumes the policy list
// |policies_b| with origin |origin_b| according to the algorithm
// https://w3c.github.io/webappsec-cspee/#subsume-policy-list.
COMPONENT_EXPORT(NETWORK_CPP)
bool Subsumes(const mojom::ContentSecurityPolicy& policy_a,
const std::vector<mojom::ContentSecurityPolicyPtr>& policies_b);
const std::vector<mojom::ContentSecurityPolicyPtr>& policies_b,
const url::Origin& origin_b);
COMPONENT_EXPORT(NETWORK_CPP)
mojom::CSPDirectiveName ToCSPDirectiveName(const std::string& name);
......
......@@ -11,6 +11,7 @@
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/third_party/mozilla/url_parse.h"
namespace network {
......@@ -1157,7 +1158,10 @@ TEST(ContentSecurityPolicy, IsValidRequiredCSPAttr) {
AddContentSecurityPolicyFromHeaders(*required_csp_headers,
GURL("https://example.com/"), &csp);
std::string out;
EXPECT_EQ(test.expected, IsValidRequiredCSPAttr(csp, nullptr, out));
EXPECT_EQ(
test.expected,
IsValidRequiredCSPAttr(
csp, nullptr, url::Origin::Create(GURL("https://a.com")), out));
EXPECT_EQ(test.expected_error, out);
}
}
......@@ -1165,46 +1169,56 @@ TEST(ContentSecurityPolicy, IsValidRequiredCSPAttr) {
TEST(ContentSecurityPolicy, Subsumes) {
struct TestCase {
std::string name;
std::string required_csp;
std::string returned_csp;
std::string required;
std::string returned;
bool returned_is_report_only;
bool expected;
} cases[] = {
{
"No required csp",
"Required CSP but no returned CSP should return false.",
"script-src 'none'",
"",
false,
false,
},
{
"Same CSP should return true.",
"script-src 'none'",
"script-src 'none'",
false,
true,
},
{
"Same CSPs",
"Same CSP returned in report-only mode should not be subsumed.",
"script-src 'none'",
"script-src 'none'",
true,
false,
},
};
for (auto& test : cases) {
SCOPED_TRACE(test.name);
std::vector<mojom::ContentSecurityPolicyPtr> required_csp;
if (test.required_csp.empty()) {
required_csp.push_back(mojom::ContentSecurityPolicy::New());
} else {
auto required_csp_headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
required_csp_headers->SetHeader("Content-Security-Policy",
test.required_csp);
AddContentSecurityPolicyFromHeaders(
*required_csp_headers, GURL("https://example.com/"), &required_csp);
}
auto required_csp_headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
required_csp_headers->SetHeader("Content-Security-Policy", test.required);
AddContentSecurityPolicyFromHeaders(
*required_csp_headers, GURL("https://example.com/"), &required_csp);
auto returned_csp_headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
returned_csp_headers->AddHeader("Content-Security-Policy",
test.returned_csp);
if (test.returned_is_report_only)
returned_csp_headers->SetHeader("Content-Security-Policy-Report-Only",
test.returned);
else
returned_csp_headers->SetHeader("Content-Security-Policy", test.returned);
std::vector<mojom::ContentSecurityPolicyPtr> returned_csp;
AddContentSecurityPolicyFromHeaders(
*returned_csp_headers, GURL("https://example.com/"), &returned_csp);
EXPECT_EQ(test.expected, Subsumes(*required_csp[0], returned_csp));
EXPECT_EQ(test.expected,
Subsumes(*required_csp[0], returned_csp,
url::Origin::Create(GURL("https://a.com"))))
<< test.name;
}
}
......
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