Commit 613235c2 authored by Antonio Sartori's avatar Antonio Sartori Committed by Commit Bot

Implement CSP source subsumption algorithm in services/network

This CL implements the part "Does source expression A subsume source
expression B?" of the Content Security Policy: Embedded Enforcement
subsumption algorithm following
https://w3c.github.io/webappsec-cspee/#subsume-source-expressions
in the services/network Content Security Policy module.

This is part of a series of CL implementing the whole subsumption
algorithm according to that spec.

Bug: 1094909
Change-Id: I6e60608aa7734b2415cfec9d4b68a1d8dbf54b6b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2315889
Commit-Queue: Antonio Sartori <antoniosartori@chromium.org>
Reviewed-by: default avatarArthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#796407}
parent e0fbe41d
......@@ -48,6 +48,17 @@ enum class PortMatchingResult {
};
enum class SchemeMatchingResult { NotMatching, MatchingUpgrade, MatchingExact };
SchemeMatchingResult MatchScheme(const std::string& scheme_a,
const std::string& scheme_b) {
if (scheme_a == scheme_b)
return SchemeMatchingResult::MatchingExact;
if ((scheme_a == url::kHttpScheme && scheme_b == url::kHttpsScheme) ||
(scheme_a == url::kWsScheme && scheme_b == url::kWssScheme)) {
return SchemeMatchingResult::MatchingUpgrade;
}
return SchemeMatchingResult::NotMatching;
}
SchemeMatchingResult SourceAllowScheme(const mojom::CSPSourcePtr& source,
const GURL& url,
CSPContext* context) {
......@@ -60,19 +71,11 @@ SchemeMatchingResult SourceAllowScheme(const mojom::CSPSourcePtr& source,
const std::string& allowed_scheme =
source->scheme.empty() ? context->self_source()->scheme : source->scheme;
if (url.SchemeIs(allowed_scheme))
return SchemeMatchingResult::MatchingExact;
// Implicitly allow using a more secure version of a protocol when the
// non-secure one is allowed.
if ((allowed_scheme == url::kHttpScheme && url.SchemeIs(url::kHttpsScheme)) ||
(allowed_scheme == url::kWsScheme && url.SchemeIs(url::kWssScheme))) {
return SchemeMatchingResult::MatchingUpgrade;
}
return SchemeMatchingResult::NotMatching;
return MatchScheme(allowed_scheme, url.scheme());
}
bool SourceAllowHost(const mojom::CSPSourcePtr& source, const GURL& url) {
bool SourceAllowHost(const mojom::CSPSourcePtr& source,
const std::string& host) {
if (source->is_host_wildcard) {
if (source->host.empty())
return true;
......@@ -80,66 +83,86 @@ bool SourceAllowHost(const mojom::CSPSourcePtr& source, const GURL& url) {
// The renderer version of this function counts how many times it happens.
// It might be useful to do it outside of blink too.
// See third_party/blink/renderer/core/frame/csp/csp_source.cc
return base::EndsWith(url.host(), '.' + source->host,
return base::EndsWith(host, '.' + source->host,
base::CompareCase::INSENSITIVE_ASCII);
} else {
return base::EqualsCaseInsensitiveASCII(url.host(), source->host);
return base::EqualsCaseInsensitiveASCII(host, source->host);
}
}
PortMatchingResult SourceAllowPort(const mojom::CSPSourcePtr& source,
const GURL& url) {
int url_port = url.EffectiveIntPort();
bool SourceAllowHost(const mojom::CSPSourcePtr& source, const GURL& url) {
return SourceAllowHost(source, url.host());
}
PortMatchingResult SourceAllowPort(const mojom::CSPSourcePtr& source,
int port,
const std::string& scheme) {
if (source->is_port_wildcard)
return PortMatchingResult::MatchingWildcard;
if (source->port == url_port) {
if (source->port == port) {
if (source->port == url::PORT_UNSPECIFIED)
return PortMatchingResult::MatchingWildcard;
return PortMatchingResult::MatchingExact;
}
if (source->port == url::PORT_UNSPECIFIED) {
if (DefaultPortForScheme(url.scheme()) == url_port) {
if (DefaultPortForScheme(scheme) == port)
return PortMatchingResult::MatchingWildcard;
}
if (port == url::PORT_UNSPECIFIED) {
if (source->port == DefaultPortForScheme(scheme))
return PortMatchingResult::MatchingWildcard;
}
return PortMatchingResult::NotMatching;
}
int source_port = source->port;
if (source_port == url::PORT_UNSPECIFIED)
source_port = DefaultPortForScheme(source->scheme);
if (source_port == 80 && url_port == 443)
if (port == url::PORT_UNSPECIFIED)
port = DefaultPortForScheme(scheme);
if (source_port == 80 && port == 443)
return PortMatchingResult::MatchingUpgrade;
return PortMatchingResult::NotMatching;
}
bool SourceAllowPath(const mojom::CSPSourcePtr& source,
const GURL& url,
bool has_followed_redirect) {
if (has_followed_redirect)
return true;
if (source->path.empty())
return true;
PortMatchingResult SourceAllowPort(const mojom::CSPSourcePtr& source,
const GURL& url) {
return SourceAllowPort(source, url.EffectiveIntPort(), url.scheme());
}
std::string url_path;
if (!DecodePath(url.path(), &url_path)) {
bool SourceAllowPath(const mojom::CSPSourcePtr& source,
const std::string& path) {
std::string path_decoded;
if (!DecodePath(path, &path_decoded)) {
// TODO(arthursonzogni): try to figure out if that could happen and how to
// handle it.
return false;
}
if (source->path.empty() || (source->path == "/" && path_decoded.empty()))
return true;
// If the path represents a directory.
if (base::EndsWith(source->path, "/", base::CompareCase::SENSITIVE))
return base::StartsWith(url_path, source->path,
if (base::EndsWith(source->path, "/", base::CompareCase::SENSITIVE)) {
return base::StartsWith(path_decoded, source->path,
base::CompareCase::SENSITIVE);
}
// The path represents a file.
return source->path == url_path;
return source->path == path_decoded;
}
bool SourceAllowPath(const mojom::CSPSourcePtr& source,
const GURL& url,
bool has_followed_redirect) {
if (has_followed_redirect)
return true;
return SourceAllowPath(source, url.path());
}
bool requiresUpgrade(const PortMatchingResult result) {
......@@ -181,6 +204,42 @@ bool CheckCSPSource(const mojom::CSPSourcePtr& source,
SourceAllowPath(source, url, has_followed_redirect);
}
// Check whether |source_a| subsumes |source_b|.
bool CSPSourceSubsumes(const mojom::CSPSourcePtr& source_a,
const mojom::CSPSourcePtr& source_b) {
// If the original source expressions didn't have a scheme, we should have
// filled that already with origin's scheme.
DCHECK(!source_a->scheme.empty());
DCHECK(!source_b->scheme.empty());
if (MatchScheme(source_a->scheme, source_b->scheme) ==
SchemeMatchingResult::NotMatching) {
return false;
}
if (IsSchemeOnly(source_a))
return true;
if (IsSchemeOnly(source_b))
return false;
if (!SourceAllowHost(source_a, (source_b->is_host_wildcard ? "*." : "") +
source_b->host)) {
return false;
}
if (source_b->is_port_wildcard && !source_a->is_port_wildcard)
return false;
PortMatchingResult port_matching =
SourceAllowPort(source_a, source_b->port, source_b->scheme);
if (port_matching == PortMatchingResult::NotMatching)
return false;
if (!SourceAllowPath(source_a, source_b->path))
return false;
return true;
}
std::string ToString(const mojom::CSPSourcePtr& source) {
// scheme
if (IsSchemeOnly(source))
......
......@@ -22,6 +22,12 @@ bool CheckCSPSource(const mojom::CSPSourcePtr& source,
CSPContext* context,
bool has_followed_redirect = false);
// Check if |source_a| subsumes |source_b| according to
// https://w3c.github.io/webappsec-cspee/#subsume-source-expressions
COMPONENT_EXPORT(NETWORK_CPP)
bool CSPSourceSubsumes(const mojom::CSPSourcePtr& source_a,
const mojom::CSPSourcePtr& source_b);
// Serialize the CSPSource |source| as a string. This is used for reporting
// violations.
COMPONENT_EXPORT(NETWORK_CPP)
......
......@@ -3,6 +3,8 @@
// found in the LICENSE file.
#include "services/network/public/cpp/content_security_policy/csp_source.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_context.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/origin.h"
......@@ -20,6 +22,17 @@ bool Allow(const network::mojom::CSPSourcePtr& source,
return CheckCSPSource(source, url, context, is_redirect);
}
network::mojom::CSPSourcePtr CSPSource(const std::string& raw) {
scoped_refptr<net::HttpResponseHeaders> headers(
new net::HttpResponseHeaders("HTTP/1.1 200 OK"));
headers->SetHeader("Content-Security-Policy", "script-src " + raw);
std::vector<mojom::ContentSecurityPolicyPtr> policies;
AddContentSecurityPolicyFromHeaders(*headers, GURL("https://example.com/"),
&policies);
return std::move(
policies[0]->directives[mojom::CSPDirectiveName::ScriptSrc]->sources[0]);
}
} // namespace
TEST(CSPSourceTest, BasicMatching) {
......@@ -310,6 +323,184 @@ TEST(CSPSourceTest, RedirectMatching) {
EXPECT_FALSE(Allow(source, GURL("http://a.com:9000/foo/"), &context, false));
}
TEST(CSPSourceTest, DoesNotSubsume) {
struct TestCase {
const char* a;
const char* b;
} cases[] = {
// In the following test cases, neither |a| subsumes |b| nor |b| subsumes
// |a|.
// Different hosts.
{"http://example.com", "http://another.com"},
// Different schemes (wss -> http).
{"wss://example.com", "http://example.com"},
// Different schemes (wss -> about).
{"wss://example.com/", "about://example.com/"},
// Different schemes (wss -> about).
{"http://example.com/", "about://example.com/"},
// Different paths.
{"http://example.com/1.html", "http://example.com/2.html"},
// Different ports.
{"http://example.com:443/", "http://example.com:800/"},
};
for (const auto& test : cases) {
auto a = CSPSource(test.a);
auto b = CSPSource(test.b);
EXPECT_FALSE(CSPSourceSubsumes(a, b))
<< test.a << " should not subsume " << test.b;
EXPECT_FALSE(CSPSourceSubsumes(b, a))
<< test.b << " should not subsume " << test.a;
}
}
TEST(CSPSourceTest, Subsumes) {
struct TestCase {
const char* a;
const char* b;
bool expected_a_subsumes_b;
bool expected_b_subsumes_a;
} cases[] = {
// Equal signals.
{"http://a.org/", "http://a.org/", true, true},
{"https://a.org/", "https://a.org/", true, true},
{"https://a.org/page.html", "https://a.org/page.html", true, true},
{"http://a.org:70", "http://a.org:70", true, true},
{"https://a.org:70", "https://a.org:70", true, true},
{"https://a.org/page.html", "https://a.org/page.html", true, true},
{"http://a.org:70/page.html", "http://a.org:70/page.html", true, true},
{"https://a.org:70/page.html", "https://a.org:70/page.html", true, true},
{"http://a.org/", "http://a.org", true, true},
{"http://a.org:80", "http://a.org:80", true, true},
{"http://a.org:80", "https://a.org:443", true, false},
{"http://a.org", "https://a.org:443", true, false},
{"http://a.org:80", "https://a.org", true, false},
// One stronger signal in the first CSPSource.
{"http://a.org/", "https://a.org/", true, false},
{"http://a.org/page.html", "http://a.org/", false, true},
{"http://a.org:80/page.html", "http://a.org:80/", false, true},
{"http://a.org:80", "http://a.org/", true, true},
{"http://a.org:700", "http://a.org/", false, false},
// Two stronger signals in the first CSPSource.
{"https://a.org/page.html", "http://a.org/", false, true},
{"https://a.org:80", "http://a.org/", false, false},
{"http://a.org:80/page.html", "http://a.org/", false, true},
// Three stronger signals in the first CSPSource.
{"https://a.org:70/page.html", "http://a.org/", false, false},
// Mixed signals.
{"https://a.org/", "http://a.org/page.html", false, false},
{"https://a.org", "http://a.org:70/", false, false},
{"http://a.org/page.html", "http://a.org:70/", false, false},
};
for (const auto& test : cases) {
auto a = CSPSource(test.a);
auto b = CSPSource(test.b);
EXPECT_EQ(CSPSourceSubsumes(a, b), test.expected_a_subsumes_b)
<< test.a << " subsumes " << test.b << " should return "
<< test.expected_a_subsumes_b;
EXPECT_EQ(CSPSourceSubsumes(b, a), test.expected_b_subsumes_a)
<< test.b << " subsumes " << test.a << " should return "
<< test.expected_b_subsumes_a;
a->is_host_wildcard = true;
EXPECT_FALSE(CSPSourceSubsumes(b, a))
<< test.b << " should not subsume " << ToString(a);
// If also |b| has a wildcard host, then the result should be the expected
// one.
b->is_host_wildcard = true;
EXPECT_EQ(CSPSourceSubsumes(b, a), test.expected_b_subsumes_a)
<< ToString(b) << " subsumes " << ToString(a) << " should return "
<< test.expected_b_subsumes_a;
}
}
TEST(CSPSourceTest, HostWildcardSubsumes) {
const char* a = "http://*.example.org";
const char* b = "http://www.example.org";
const char* c = "http://example.org";
const char* d = "https://*.example.org";
auto source_a = CSPSource(a);
auto source_b = CSPSource(b);
auto source_c = CSPSource(c);
auto source_d = CSPSource(d);
// *.example.com subsumes www.example.com.
EXPECT_TRUE(CSPSourceSubsumes(source_a, source_b))
<< a << " should subsume " << b;
EXPECT_FALSE(CSPSourceSubsumes(source_b, source_a))
<< b << " should not subsume " << a;
// *.example.com and example.com have no relations.
EXPECT_FALSE(CSPSourceSubsumes(source_a, source_c))
<< a << " should not subsume " << c;
EXPECT_FALSE(CSPSourceSubsumes(source_c, source_a))
<< c << " should not subsume " << a;
// https://*.example.com and http://www.example.com have no relations.
EXPECT_FALSE(CSPSourceSubsumes(source_d, source_b))
<< d << " should not subsume " << b;
EXPECT_FALSE(CSPSourceSubsumes(source_b, source_d))
<< b << " should not subsume " << d;
}
TEST(CSPSourceTest, PortWildcardSubsumes) {
const char* a = "http://example.org:*";
const char* b = "http://example.org";
const char* c = "https://example.org:*";
auto source_a = CSPSource(a);
auto source_b = CSPSource(b);
auto source_c = CSPSource(c);
EXPECT_TRUE(CSPSourceSubsumes(source_a, source_b))
<< a << " should subsume " << b;
EXPECT_FALSE(CSPSourceSubsumes(source_b, source_a))
<< b << " should not subsume " << a;
// https://example.com:* and http://example.com have no relations.
EXPECT_FALSE(CSPSourceSubsumes(source_b, source_c))
<< b << " should not subsume " << c;
EXPECT_FALSE(CSPSourceSubsumes(source_c, source_b))
<< c << " should not subsume " << b;
}
TEST(CSPSourceTest, SchemesOnlySubsumes) {
struct TestCase {
const char* a;
const char* b;
bool expected;
} cases[] = {
// HTTP.
{"http:", "http:", true},
{"http:", "https:", true},
{"https:", "http:", false},
{"https:", "https:", true},
// WSS.
{"ws:", "ws:", true},
{"ws:", "wss:", true},
{"wss:", "ws:", false},
{"wss:", "wss:", true},
// Unequal.
{"ws:", "http:", false},
{"http:", "ws:", false},
{"http:", "about:", false},
{"wss:", "https:", false},
{"https:", "wss:", false},
};
for (const auto& test : cases) {
auto source_a = CSPSource(test.a);
auto source_b = CSPSource(test.b);
EXPECT_EQ(CSPSourceSubsumes(source_a, source_b), test.expected)
<< test.a << " subsumes " << test.b << " should return "
<< test.expected;
}
}
TEST(CSPSourceTest, ToString) {
{
auto source = network::mojom::CSPSource::New(
......
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