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() { ...@@ -165,8 +165,9 @@ NavigationThrottle::ThrottleCheckResult AncestorThrottle::WillStartRequest() {
request->frame_tree_node()->parent()->required_csp(); request->frame_tree_node()->parent()->required_csp();
std::string error_message; std::string error_message;
if (!network::IsValidRequiredCSPAttr(frame_csp, parent_required_csp, if (!network::IsValidRequiredCSPAttr(
error_message)) { frame_csp, parent_required_csp,
url::Origin::Create(navigation_handle()->GetURL()), error_message)) {
if (frame_csp[0]) { if (frame_csp[0]) {
navigation_handle()->GetParentFrame()->AddMessageToConsole( navigation_handle()->GetParentFrame()->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError, blink::mojom::ConsoleMessageLevel::kError,
......
...@@ -802,6 +802,19 @@ void AddContentSecurityPolicyFromHeader(base::StringPiece header, ...@@ -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 } // namespace
void AddContentSecurityPolicyFromHeaders( void AddContentSecurityPolicyFromHeaders(
...@@ -947,6 +960,7 @@ void UpgradeInsecureRequest(GURL* url) { ...@@ -947,6 +960,7 @@ void UpgradeInsecureRequest(GURL* url) {
bool IsValidRequiredCSPAttr( bool IsValidRequiredCSPAttr(
const std::vector<mojom::ContentSecurityPolicyPtr>& policy, const std::vector<mojom::ContentSecurityPolicyPtr>& policy,
const mojom::ContentSecurityPolicy* context, const mojom::ContentSecurityPolicy* context,
const url::Origin& origin,
std::string& error_message) { std::string& error_message) {
DCHECK(policy.size() == 1); DCHECK(policy.size() == 1);
if (!policy[0]) if (!policy[0])
...@@ -967,7 +981,7 @@ bool IsValidRequiredCSPAttr( ...@@ -967,7 +981,7 @@ bool IsValidRequiredCSPAttr(
return false; return false;
} }
if (context && !Subsumes(*context, policy)) { if (context && !Subsumes(*context, policy, origin)) {
error_message = error_message =
"The csp attribute Content-Security-Policy is not subsumed by the " "The csp attribute Content-Security-Policy is not subsumed by the "
"frame's parent csp attribute Content-Security-Policy."; "frame's parent csp attribute Content-Security-Policy.";
...@@ -978,28 +992,55 @@ bool IsValidRequiredCSPAttr( ...@@ -978,28 +992,55 @@ bool IsValidRequiredCSPAttr(
} }
bool Subsumes(const mojom::ContentSecurityPolicy& policy_a, 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()) if (policy_a.directives.empty())
return true; return true;
if (policy_a.header->type == mojom::ContentSecurityPolicyType::kReport) if (policy_a.header->type == mojom::ContentSecurityPolicyType::kReport)
return true; return true;
// TODO(antoniosartori): Complete the implementation of this function if (policies_b.empty())
return util::ranges::all_of( return false;
policy_a.directives, [&policies_b](const auto& directive_a) {
return util::ranges::any_of( // A list of directives that we consider for subsumption.
policies_b, [&directive_a](const auto& policy_b) { // See more about source lists here:
if (policy_b->header->type == // https://w3c.github.io/webappsec-csp/#framework-directive-source-list
mojom::ContentSecurityPolicyType::kReport) { static const CSPDirectiveName directives[] = {
return false; CSPDirectiveName::ChildSrc, CSPDirectiveName::ConnectSrc,
} CSPDirectiveName::FontSrc, CSPDirectiveName::FrameSrc,
CSPDirectiveName::ImgSrc, CSPDirectiveName::ManifestSrc,
auto value_b = policy_b->directives.find(directive_a.first); CSPDirectiveName::MediaSrc, CSPDirectiveName::ObjectSrc,
return value_b != policy_b->directives.end() && CSPDirectiveName::ScriptSrc, CSPDirectiveName::ScriptSrcAttr,
value_b->second == directive_a.second; 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) { CSPDirectiveName ToCSPDirectiveName(const std::string& name) {
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
class GURL; class GURL;
namespace url {
class Origin;
}
namespace net { namespace net {
class HttpResponseHeaders; class HttpResponseHeaders;
} }
...@@ -79,13 +83,16 @@ COMPONENT_EXPORT(NETWORK_CPP) ...@@ -79,13 +83,16 @@ COMPONENT_EXPORT(NETWORK_CPP)
bool IsValidRequiredCSPAttr( bool IsValidRequiredCSPAttr(
const std::vector<mojom::ContentSecurityPolicyPtr>& policy, const std::vector<mojom::ContentSecurityPolicyPtr>& policy,
const mojom::ContentSecurityPolicy* context, const mojom::ContentSecurityPolicy* context,
const url::Origin& url,
std::string& error_message); std::string& error_message);
// Checks whether |policy_a| subsumes the policy list |policies_b| according to // Checks whether |policy_a| subsumes the policy list
// the algorithm https://w3c.github.io/webappsec-cspee/#subsume-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) COMPONENT_EXPORT(NETWORK_CPP)
bool Subsumes(const mojom::ContentSecurityPolicy& policy_a, 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) COMPONENT_EXPORT(NETWORK_CPP)
mojom::CSPDirectiveName ToCSPDirectiveName(const std::string& name); mojom::CSPDirectiveName ToCSPDirectiveName(const std::string& name);
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include "services/network/public/mojom/content_security_policy.mojom.h" #include "services/network/public/mojom/content_security_policy.mojom.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h" #include "url/gurl.h"
#include "url/origin.h"
#include "url/third_party/mozilla/url_parse.h" #include "url/third_party/mozilla/url_parse.h"
namespace network { namespace network {
...@@ -1157,7 +1158,10 @@ TEST(ContentSecurityPolicy, IsValidRequiredCSPAttr) { ...@@ -1157,7 +1158,10 @@ TEST(ContentSecurityPolicy, IsValidRequiredCSPAttr) {
AddContentSecurityPolicyFromHeaders(*required_csp_headers, AddContentSecurityPolicyFromHeaders(*required_csp_headers,
GURL("https://example.com/"), &csp); GURL("https://example.com/"), &csp);
std::string out; 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); EXPECT_EQ(test.expected_error, out);
} }
} }
...@@ -1165,46 +1169,56 @@ TEST(ContentSecurityPolicy, IsValidRequiredCSPAttr) { ...@@ -1165,46 +1169,56 @@ TEST(ContentSecurityPolicy, IsValidRequiredCSPAttr) {
TEST(ContentSecurityPolicy, Subsumes) { TEST(ContentSecurityPolicy, Subsumes) {
struct TestCase { struct TestCase {
std::string name; std::string name;
std::string required_csp; std::string required;
std::string returned_csp; std::string returned;
bool returned_is_report_only;
bool expected; bool expected;
} cases[] = { } 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'", "script-src 'none'",
false,
true, true,
}, },
{ {
"Same CSPs", "Same CSP returned in report-only mode should not be subsumed.",
"script-src 'none'", "script-src 'none'",
"script-src 'none'", "script-src 'none'",
true, true,
false,
}, },
}; };
for (auto& test : cases) { for (auto& test : cases) {
SCOPED_TRACE(test.name);
std::vector<mojom::ContentSecurityPolicyPtr> required_csp; std::vector<mojom::ContentSecurityPolicyPtr> required_csp;
if (test.required_csp.empty()) { auto required_csp_headers =
required_csp.push_back(mojom::ContentSecurityPolicy::New()); base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
} else { required_csp_headers->SetHeader("Content-Security-Policy", test.required);
auto required_csp_headers = AddContentSecurityPolicyFromHeaders(
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK"); *required_csp_headers, GURL("https://example.com/"), &required_csp);
required_csp_headers->SetHeader("Content-Security-Policy",
test.required_csp);
AddContentSecurityPolicyFromHeaders(
*required_csp_headers, GURL("https://example.com/"), &required_csp);
}
auto returned_csp_headers = auto returned_csp_headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK"); base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
returned_csp_headers->AddHeader("Content-Security-Policy", if (test.returned_is_report_only)
test.returned_csp); 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; std::vector<mojom::ContentSecurityPolicyPtr> returned_csp;
AddContentSecurityPolicyFromHeaders( AddContentSecurityPolicyFromHeaders(
*returned_csp_headers, GURL("https://example.com/"), &returned_csp); *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