Commit 951ba7a0 authored by Antonio Sartori's avatar Antonio Sartori Committed by Commit Bot

Implement CSP source list subsumption algorithm in services/network

This CL implements the part "Does source list A subsume source
list B?" of the Content Security Policy: Embedded Enforcement
subsumption algorithm following
https://w3c.github.io/webappsec-cspee/#subsume-source-list
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: Id8cecc37c12a1db79cd0a97e616e0f46c98bdc97
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2315145
Commit-Queue: Antonio Sartori <antoniosartori@chromium.org>
Reviewed-by: default avatarArthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#803833}
parent 1fbc53fc
...@@ -22,10 +22,6 @@ bool HasHost(const mojom::CSPSourcePtr& source) { ...@@ -22,10 +22,6 @@ bool HasHost(const mojom::CSPSourcePtr& source) {
return !source->host.empty() || source->is_host_wildcard; return !source->host.empty() || source->is_host_wildcard;
} }
bool IsSchemeOnly(const mojom::CSPSourcePtr& source) {
return !HasHost(source);
}
bool DecodePath(const base::StringPiece& path, std::string* output) { bool DecodePath(const base::StringPiece& path, std::string* output) {
url::RawCanonOutputT<base::char16> unescaped; url::RawCanonOutputT<base::char16> unescaped;
url::DecodeURLEscapeSequences(path.data(), path.size(), url::DecodeURLEscapeSequences(path.data(), path.size(),
...@@ -184,11 +180,15 @@ bool canUpgrade(const SchemeMatchingResult result) { ...@@ -184,11 +180,15 @@ bool canUpgrade(const SchemeMatchingResult result) {
} // namespace } // namespace
bool CSPSourceIsSchemeOnly(const mojom::CSPSourcePtr& source) {
return !HasHost(source);
}
bool CheckCSPSource(const mojom::CSPSourcePtr& source, bool CheckCSPSource(const mojom::CSPSourcePtr& source,
const GURL& url, const GURL& url,
CSPContext* context, CSPContext* context,
bool has_followed_redirect) { bool has_followed_redirect) {
if (IsSchemeOnly(source)) { if (CSPSourceIsSchemeOnly(source)) {
return SourceAllowScheme(source, url, context) != return SourceAllowScheme(source, url, context) !=
SchemeMatchingResult::NotMatching; SchemeMatchingResult::NotMatching;
} }
...@@ -222,11 +222,11 @@ mojom::CSPSourcePtr CSPSourcesIntersect(const mojom::CSPSourcePtr& source_a, ...@@ -222,11 +222,11 @@ mojom::CSPSourcePtr CSPSourcesIntersect(const mojom::CSPSourcePtr& source_a,
return nullptr; return nullptr;
} }
if (IsSchemeOnly(source_a)) { if (CSPSourceIsSchemeOnly(source_a)) {
auto new_result = source_b->Clone(); auto new_result = source_b->Clone();
new_result->scheme = result->scheme; new_result->scheme = result->scheme;
return new_result; return new_result;
} else if (IsSchemeOnly(source_b)) { } else if (CSPSourceIsSchemeOnly(source_b)) {
auto new_result = source_a->Clone(); auto new_result = source_a->Clone();
new_result->scheme = result->scheme; new_result->scheme = result->scheme;
return new_result; return new_result;
...@@ -284,9 +284,9 @@ bool CSPSourceSubsumes(const mojom::CSPSourcePtr& source_a, ...@@ -284,9 +284,9 @@ bool CSPSourceSubsumes(const mojom::CSPSourcePtr& source_a,
return false; return false;
} }
if (IsSchemeOnly(source_a)) if (CSPSourceIsSchemeOnly(source_a))
return true; return true;
if (IsSchemeOnly(source_b)) if (CSPSourceIsSchemeOnly(source_b))
return false; return false;
if (!SourceAllowHost(source_a, (source_b->is_host_wildcard ? "*." : "") + if (!SourceAllowHost(source_a, (source_b->is_host_wildcard ? "*." : "") +
...@@ -309,7 +309,7 @@ bool CSPSourceSubsumes(const mojom::CSPSourcePtr& source_a, ...@@ -309,7 +309,7 @@ bool CSPSourceSubsumes(const mojom::CSPSourcePtr& source_a,
std::string ToString(const mojom::CSPSourcePtr& source) { std::string ToString(const mojom::CSPSourcePtr& source) {
// scheme // scheme
if (IsSchemeOnly(source)) if (CSPSourceIsSchemeOnly(source))
return source->scheme + ":"; return source->scheme + ":";
std::stringstream text; std::stringstream text;
......
...@@ -15,6 +15,9 @@ namespace network { ...@@ -15,6 +15,9 @@ namespace network {
class CSPContext; class CSPContext;
// Check if a CSP |source| matches the scheme-source grammar.
bool CSPSourceIsSchemeOnly(const mojom::CSPSourcePtr& source);
// Check if a |url| matches with a CSP |source| matches. // Check if a |url| matches with a CSP |source| matches.
COMPONENT_EXPORT(NETWORK_CPP) COMPONENT_EXPORT(NETWORK_CPP)
bool CheckCSPSource(const mojom::CSPSourcePtr& source, bool CheckCSPSource(const mojom::CSPSourcePtr& source,
......
...@@ -3,12 +3,17 @@ ...@@ -3,12 +3,17 @@
// found in the LICENSE file. // found in the LICENSE file.
#include "services/network/public/cpp/content_security_policy/csp_source_list.h" #include "services/network/public/cpp/content_security_policy/csp_source_list.h"
#include "base/containers/flat_set.h"
#include "base/util/ranges/algorithm.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.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 "services/network/public/cpp/content_security_policy/csp_context.h"
#include "services/network/public/cpp/content_security_policy/csp_source.h" #include "services/network/public/cpp/content_security_policy/csp_source.h"
namespace network { namespace network {
using CSPDirectiveName = mojom::CSPDirectiveName;
namespace { namespace {
bool AllowFromSources(const GURL& url, bool AllowFromSources(const GURL& url,
...@@ -22,6 +27,163 @@ bool AllowFromSources(const GURL& url, ...@@ -22,6 +27,163 @@ bool AllowFromSources(const GURL& url,
return false; return false;
} }
// Removes from |a| elements not contained in |b|.
void IntersectNonces(base::flat_set<std::string>& a,
const base::flat_set<std::string>& b) {
base::EraseIf(a, [&b](const std::string& s) { return !b.contains(s); });
}
// Removes from |a| elements not contained in |b|.
void IntersectHashes(base::flat_set<mojom::CSPHashSourcePtr>& a,
const base::flat_set<mojom::CSPHashSourcePtr>& b) {
base::EraseIf(
a, [&b](const mojom::CSPHashSourcePtr& h) { return !b.contains(h); });
}
bool IsScriptDirective(CSPDirectiveName directive) {
return directive == CSPDirectiveName::ScriptSrc ||
directive == CSPDirectiveName::ScriptSrcAttr ||
directive == CSPDirectiveName::ScriptSrcElem;
}
bool IsStyleDirective(CSPDirectiveName directive) {
return directive == CSPDirectiveName::StyleSrc ||
directive == CSPDirectiveName::StyleSrcAttr ||
directive == CSPDirectiveName::StyleSrcElem;
}
bool AllowAllInline(CSPDirectiveName directive,
const mojom::CSPSourceList& source) {
return source.allow_inline && source.hashes.empty() &&
source.nonces.empty() &&
(!IsScriptDirective(directive) || !source.allow_dynamic);
}
void AddSourceSchemesToSet(base::flat_set<std::string>& set,
const mojom::CSPSource* source) {
set.emplace(source->scheme);
if (source->scheme == url::kHttpScheme)
set.emplace(url::kHttpsScheme);
else if (source->scheme == url::kWsScheme)
set.emplace(url::kWssScheme);
}
base::flat_set<std::string> IntersectSchemesOnly(
const std::vector<mojom::CSPSourcePtr>& list_a,
const std::vector<mojom::CSPSourcePtr>& list_b) {
base::flat_set<std::string> schemes_a;
for (const auto& source_a : list_a) {
if (CSPSourceIsSchemeOnly(source_a)) {
AddSourceSchemesToSet(schemes_a, source_a.get());
}
}
base::flat_set<std::string> intersection;
for (const auto& source_b : list_b) {
if (CSPSourceIsSchemeOnly(source_b)) {
if (schemes_a.contains(source_b->scheme))
AddSourceSchemesToSet(intersection, source_b.get());
else if (source_b->scheme == url::kHttpScheme &&
schemes_a.contains(url::kHttpsScheme)) {
intersection.emplace(url::kHttpsScheme);
} else if (source_b->scheme == url::kWsScheme &&
schemes_a.contains(url::kWssScheme)) {
intersection.emplace(url::kWssScheme);
}
}
}
return intersection;
}
std::vector<mojom::CSPSourcePtr> ExpandSchemeStarAndSelf(
const mojom::CSPSourceList& source_list,
const mojom::CSPSource& self) {
std::vector<mojom::CSPSourcePtr> result;
for (const mojom::CSPSourcePtr& item : source_list.sources) {
mojom::CSPSourcePtr new_item = item->Clone();
if (new_item->scheme.empty()) {
if (self.scheme.empty())
continue;
new_item->scheme = self.scheme;
}
result.push_back(std::move(new_item));
}
if (source_list.allow_star) {
result.push_back(mojom::CSPSource::New(
url::kFtpScheme, "", url::PORT_UNSPECIFIED, "", false, false));
result.push_back(mojom::CSPSource::New(
url::kWsScheme, "", url::PORT_UNSPECIFIED, "", false, false));
result.push_back(mojom::CSPSource::New(
url::kHttpScheme, "", url::PORT_UNSPECIFIED, "", false, false));
if (!self.scheme.empty()) {
result.push_back(mojom::CSPSource::New(
self.scheme, "", url::PORT_UNSPECIFIED, "", false, false));
}
}
if (source_list.allow_self && !self.scheme.empty() && !self.host.empty()) {
// If |self| is an opaque origin we should ignore it.
result.push_back(self.Clone());
}
return result;
}
std::vector<mojom::CSPSourcePtr> IntersectSources(
const mojom::CSPSourceList& source_list_a,
const std::vector<mojom::CSPSourcePtr>& source_list_b,
const mojom::CSPSource& self) {
auto schemes = IntersectSchemesOnly(source_list_a.sources, source_list_b);
std::vector<mojom::CSPSourcePtr> normalized;
// Add all normalized scheme source expressions.
for (const auto& it : schemes) {
// We do not add secure versions if insecure schemes are present.
if ((it != url::kHttpsScheme || !schemes.count(url::kHttpScheme)) &&
(it != url::kWssScheme || !schemes.count(url::kWsScheme))) {
normalized.emplace_back(mojom::CSPSource::New(
it, "", url::PORT_UNSPECIFIED, "", false, false));
}
}
std::vector<mojom::CSPSourcePtr> sources_a =
ExpandSchemeStarAndSelf(source_list_a, self);
for (auto& source_a : sources_a) {
if (schemes.count(source_a->scheme))
continue;
for (auto& source_b : source_list_b) {
// No need to add a host source expression if it is subsumed by the
// matching scheme source expression.
if (schemes.contains(source_b->scheme))
continue;
if (mojom::CSPSourcePtr local_match =
CSPSourcesIntersect(source_a, source_b)) {
normalized.emplace_back(std::move(local_match));
}
}
}
return normalized;
}
bool UrlSourceListSubsumes(
const std::vector<mojom::CSPSourcePtr>& source_list_a,
const std::vector<mojom::CSPSourcePtr>& source_list_b) {
// Empty vector of CSPSources has an effect of 'none'.
if (!source_list_a.size() || !source_list_b.size())
return !source_list_b.size();
// Every item in |source_list_b| must be subsumed by at least one item in
// |source_list_a|.
return util::ranges::all_of(source_list_b, [&](const auto& source_b) {
return util::ranges::any_of(source_list_a, [&](const auto& source_a) {
return CSPSourceSubsumes(source_a, source_b);
});
});
}
} // namespace } // namespace
bool CheckCSPSourceList(const mojom::CSPSourceListPtr& source_list, bool CheckCSPSourceList(const mojom::CSPSourceListPtr& source_list,
...@@ -58,6 +220,100 @@ bool CheckCSPSourceList(const mojom::CSPSourceListPtr& source_list, ...@@ -58,6 +220,100 @@ bool CheckCSPSourceList(const mojom::CSPSourceListPtr& source_list,
has_followed_redirect); has_followed_redirect);
} }
bool CSPSourceListSubsumes(
const mojom::CSPSourceList& source_list_a,
const std::vector<const mojom::CSPSourceList*>& source_list_b,
CSPDirectiveName directive,
const url::Origin& origin_b) {
if (source_list_b.empty())
return false;
auto it = source_list_b.begin();
bool allow_inline_b = (*it)->allow_inline;
bool allow_eval_b = (*it)->allow_eval;
bool allow_wasm_eval_b = (*it)->allow_wasm_eval;
bool allow_dynamic_b = (*it)->allow_dynamic;
bool allow_unsafe_hashes_b = (*it)->allow_unsafe_hashes;
bool is_hash_or_nonce_present_b =
!(*it)->nonces.empty() || !(*it)->hashes.empty();
base::flat_set<std::string> nonces_b((*it)->nonces);
base::flat_set<mojom::CSPHashSourcePtr> hashes_b(mojo::Clone((*it)->hashes));
auto origin_b_as_csp_source = mojom::CSPSource::New(
origin_b.scheme(), origin_b.host(), origin_b.port(), "", false, false);
std::vector<mojom::CSPSourcePtr> normalized_sources_b =
ExpandSchemeStarAndSelf(**it, *origin_b_as_csp_source);
++it;
for (; it != source_list_b.end(); ++it) {
// 'allow_inline' is ignored if hashes or nonces are present, or if
// 'strict-dynamic' is specified.
allow_inline_b = allow_inline_b && (*it)->allow_inline;
allow_eval_b = allow_eval_b && (*it)->allow_eval;
allow_wasm_eval_b = allow_wasm_eval_b && (*it)->allow_wasm_eval;
allow_dynamic_b = allow_dynamic_b && (*it)->allow_dynamic;
allow_unsafe_hashes_b = allow_unsafe_hashes_b && (*it)->allow_unsafe_hashes;
is_hash_or_nonce_present_b =
is_hash_or_nonce_present_b &&
(!(*it)->nonces.empty() || !(*it)->hashes.empty());
base::flat_set<std::string> item_nonces((*it)->nonces);
IntersectNonces(nonces_b, item_nonces);
base::flat_set<mojom::CSPHashSourcePtr> item_hashes(
mojo::Clone((*it)->hashes));
IntersectHashes(hashes_b, item_hashes);
normalized_sources_b =
IntersectSources(**it, normalized_sources_b, *origin_b_as_csp_source);
}
// If source_list_b enforces some nonce, then source_list_a must contain
// some nonce, but they do not need to match.
if (!nonces_b.empty() && source_list_a.nonces.empty())
return false;
// All hashes enforced by source_list_b must be contained in source_list_a.
if (!hashes_b.empty()) {
base::flat_set<mojom::CSPHashSourcePtr> hashes_a(
mojo::Clone(source_list_a.hashes));
for (const auto& hash : hashes_b) {
if (!hashes_a.count(hash))
return false;
}
}
if (IsScriptDirective(directive) || IsStyleDirective(directive)) {
if (!source_list_a.allow_eval && allow_eval_b)
return false;
if (!source_list_a.allow_wasm_eval && allow_wasm_eval_b)
return false;
if (!source_list_a.allow_unsafe_hashes && allow_unsafe_hashes_b)
return false;
bool allow_all_inline_b =
allow_inline_b && !is_hash_or_nonce_present_b &&
(!IsScriptDirective(directive) || !allow_dynamic_b);
if (!AllowAllInline(directive, source_list_a) && allow_all_inline_b)
return false;
}
if (IsScriptDirective(directive) &&
(source_list_a.allow_dynamic || allow_dynamic_b)) {
// If `this` does not allow `strict-dynamic`, then it must be that `other`
// does allow, so the result is `false`.
if (!source_list_a.allow_dynamic)
return false;
// All keyword source expressions have been considered so only CSPSource
// subsumption is left. However, `strict-dynamic` ignores all CSPSources so
// for subsumption to be true either `other` must allow `strict-dynamic` or
// have no allowed CSPSources.
return allow_dynamic_b || !normalized_sources_b.size();
}
// If embedding CSP specifies `self`, `self` refers to the embedee's origin.
std::vector<mojom::CSPSourcePtr> normalized_sources_a =
ExpandSchemeStarAndSelf(source_list_a, *origin_b_as_csp_source);
return UrlSourceListSubsumes(normalized_sources_a, normalized_sources_b);
}
std::string ToString(const mojom::CSPSourceListPtr& source_list) { std::string ToString(const mojom::CSPSourceListPtr& source_list) {
bool is_none = !source_list->allow_self && !source_list->allow_star && bool is_none = !source_list->allow_self && !source_list->allow_star &&
source_list->sources.empty(); source_list->sources.empty();
......
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
class GURL; class GURL;
namespace url {
class Origin;
}
namespace network { namespace network {
class CSPContext; class CSPContext;
...@@ -28,5 +32,15 @@ bool CheckCSPSourceList(const mojom::CSPSourceListPtr& source_list, ...@@ -28,5 +32,15 @@ bool CheckCSPSourceList(const mojom::CSPSourceListPtr& source_list,
bool has_followed_redirect = false, bool has_followed_redirect = false,
bool is_response_check = false); bool is_response_check = false);
// Check if |source_list_a| subsumes |source_list_b| with origin |origin_b| for
// directive |directive| according to
// https://w3c.github.io/webappsec-cspee/#subsume-source-list
COMPONENT_EXPORT(NETWORK_CPP)
bool CSPSourceListSubsumes(
const mojom::CSPSourceList& source_list_a,
const std::vector<const mojom::CSPSourceList*>& source_list_b,
mojom::CSPDirectiveName directive,
const url::Origin& origin_b);
} // namespace network } // namespace network
#endif // SERVICES_NETWORK_PUBLIC_CPP_CONTENT_SECURITY_POLICY_CSP_SOURCE_LIST_H_ #endif // SERVICES_NETWORK_PUBLIC_CPP_CONTENT_SECURITY_POLICY_CSP_SOURCE_LIST_H_
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
#include "services/network/public/cpp/content_security_policy/csp_source_list.h" #include "services/network/public/cpp/content_security_policy/csp_source_list.h"
#include "base/strings/stringprintf.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/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_context.h" #include "services/network/public/cpp/content_security_policy/csp_context.h"
#include "services/network/public/mojom/content_security_policy.mojom.h" #include "services/network/public/mojom/content_security_policy.mojom.h"
...@@ -24,6 +26,54 @@ bool Allow(const mojom::CSPSourceListPtr& source_list, ...@@ -24,6 +26,54 @@ bool Allow(const mojom::CSPSourceListPtr& source_list,
is_response_check); is_response_check);
} }
std::vector<mojom::ContentSecurityPolicyPtr> Parse(
const std::vector<std::string>& policies) {
auto headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
for (const auto& policy : policies) {
headers->AddHeader("Content-Security-Policy", policy);
}
std::vector<mojom::ContentSecurityPolicyPtr> parsed_policies;
AddContentSecurityPolicyFromHeaders(*headers, GURL("https://example.com/"),
&parsed_policies);
return parsed_policies;
}
mojom::CSPSourceListPtr ParseToSourceList(mojom::CSPDirectiveName directive,
const std::string& value) {
return std::move(
Parse({ToString(directive) + " " + value})[0]->directives[directive]);
}
std::vector<mojom::CSPSourceListPtr> ParseToVectorOfSourceLists(
mojom::CSPDirectiveName directive,
const std::vector<std::string>& values) {
std::vector<std::string> csp_values(values.size());
std::transform(values.begin(), values.end(), csp_values.begin(),
[directive](const std::string& s) -> std::string {
return ToString(directive) + " " + s;
});
std::vector<mojom::ContentSecurityPolicyPtr> policies = Parse(csp_values);
std::vector<mojom::CSPSourceListPtr> sources(policies.size());
std::transform(policies.begin(), policies.end(), sources.begin(),
[directive](mojom::ContentSecurityPolicyPtr& p)
-> mojom::CSPSourceListPtr {
return std::move(p->directives[directive]);
});
return sources;
}
std::vector<const mojom::CSPSourceList*> ToRawPointers(
const std::vector<mojom::CSPSourceListPtr>& list) {
std::vector<const mojom::CSPSourceList*> out(list.size());
std::transform(
list.begin(), list.end(), out.begin(),
[](const mojom::CSPSourceListPtr& item) -> const mojom::CSPSourceList* {
return item.get();
});
return out;
}
} // namespace } // namespace
TEST(CSPSourceList, MultipleSource) { TEST(CSPSourceList, MultipleSource) {
...@@ -124,4 +174,867 @@ TEST(CSPSourceTest, SelfIsUnique) { ...@@ -124,4 +174,867 @@ TEST(CSPSourceTest, SelfIsUnique) {
EXPECT_FALSE(Allow(source_list, GURL("data:text/html,hello"), &context)); EXPECT_FALSE(Allow(source_list, GURL("data:text/html,hello"), &context));
} }
TEST(CSPSourceList, Subsume) {
std::string required =
"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/";
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(mojom::CSPDirectiveName::ScriptSrc, required);
struct TestCase {
std::vector<std::string> response_csp;
bool expected;
} cases[] = {
// Non-intersecting source lists give an effective policy of 'none', which
// is always subsumed.
{{"http://example1.com/bar/", "http://*.example3.com:*/bar/"}, true},
{{"http://example1.com/bar/",
"http://*.example3.com:*/bar/ https://*.example2.com/bar/"},
true},
// Lists that intersect into one of the required sources are subsumed.
{{"http://example1.com/foo/"}, true},
{{"https://*.example2.com/bar/"}, true},
{{"http://*.example3.com:*/bar/"}, true},
{{"https://example1.com/foo/",
"http://*.example1.com/foo/ https://*.example2.com/bar/"},
true},
{{"https://example2.com/bar/",
"http://*.example3.com:*/bar/ https://*.example2.com/bar/"},
true},
{{"http://example3.com:100/bar/",
"http://*.example3.com:*/bar/ https://*.example2.com/bar/"},
true},
// Lists that intersect into two of the required sources are subsumed.
{{"http://example1.com/foo/ https://*.example2.com/bar/"}, true},
{{"http://example1.com/foo/ https://a.example2.com/bar/",
"https://a.example2.com/bar/ http://example1.com/foo/"},
true},
{{"http://example1.com/foo/ https://a.example2.com/bar/",
"http://*.example2.com/bar/ http://example1.com/foo/"},
true},
// Ordering should not matter.
{{"https://example1.com/foo/ https://a.example2.com/bar/",
"http://a.example2.com/bar/ http://example1.com/foo/"},
true},
// Lists that intersect into a policy identical to the required list are
// subsumed.
{{"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ http://example1.com/foo/"},
true},
{{"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/"},
true},
{{"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/",
"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ http://example4.com/foo/"},
true},
{{"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/",
"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ http://example1.com/foo/"},
true},
// Lists that include sources which are not subsumed by the required list
// are not subsumed.
{{"http://example1.com/foo/ https://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ http://*.example4.com:*/bar/"},
false},
{{"http://example1.com/foo/ http://example2.com/foo/"}, false},
{{"http://*.com/bar/", "http://example1.com/bar/"}, false},
{{"http://*.example1.com/bar/"}, false},
{{"http://example1.com/bar/"}, false},
{{"http://*.example1.com/foo/"}, false},
{{"wss://example2.com/bar/"}, false},
{{"http://*.non-example3.com:*/bar/"}, false},
{{"http://example3.com/foo/"}, false},
{{"http://not-example1.com", "http://not-example1.com"}, false},
// Lists that intersect into sources which are not subsumed by the
// required
// list are not subsumed.
{{"http://not-example1.com/foo/", "https:"}, false},
{{"http://not-example1.com/foo/ http://example1.com/foo/", "https:"},
false},
{{"http://*", "http://*.com http://*.example3.com:*/bar/"}, false},
};
for (const auto& test : cases) {
auto response_sources = ParseToVectorOfSourceLists(
mojom::CSPDirectiveName::ScriptSrc, test.response_csp);
EXPECT_EQ(test.expected,
CSPSourceListSubsumes(
*required_sources, ToRawPointers(response_sources),
mojom::CSPDirectiveName::ScriptSrc,
url::Origin::Create(GURL("https://frame.test"))))
<< required << " should " << (test.expected ? "" : "not ") << "subsume "
<< base::JoinString(test.response_csp, ", ");
}
}
TEST(CSPSourceList, SubsumeWithSelf) {
std::string required =
"http://example1.com/foo/ http://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ 'self'";
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(mojom::CSPDirectiveName::ScriptSrc, required);
struct TestCase {
std::vector<std::string> response_csp;
const char* origin;
bool expected;
} cases[] = {
// "https://example.test/" is a secure origin for both `required` and
// `response_csp`.
{{"'self'"}, "https://example.test/", true},
{{"https://example.test"}, "https://example.test/", true},
{{"https://example.test/"}, "https://example.test/", true},
{{"'self' 'self' 'self'"}, "https://example.test/", true},
{{"'self'", "'self'", "'self'"}, "https://example.test/", true},
{{"'self'", "'self'", "https://*.example.test/"},
"https://example.test/",
true},
{{"'self'", "'self'", "https://*.example.test/bar/"},
"https://example.test/",
true},
{{"'self' https://another.test/bar", "'self' http://*.example.test/bar",
"https://*.example.test/bar/"},
"https://example.test/",
true},
{{"http://example1.com/foo/ 'self'"}, "https://example.test/", true},
{{"http://example1.com/foo/ https://example.test/"},
"https://example.test/",
true},
{{"http://example1.com/foo/ http://*.example2.com/bar/"},
"https://example.test/",
true},
{{"http://example1.com/foo/ http://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ https://example.test/"},
"https://example.test/",
true},
{{"http://example1.com/foo/ http://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ 'self'"},
"https://example.test/",
true},
{{"'self'", "'self'", "https://example.test/"},
"https://example.test/",
true},
{{"'self'", "https://example.test/folder/"},
"https://example.test/",
true},
{{"'self'", "http://example.test/folder/"},
"https://example.test/",
true},
{{"'self' https://example.com/", "https://example.com/"},
"https://example.test/",
false},
{{"http://example1.com/foo/ http://*.example2.com/bar/",
"http://example1.com/foo/ http://*.example2.com/bar/ 'self'"},
"https://example.test/",
true},
{{"http://*.example1.com/foo/", "http://*.example1.com/foo/ 'self'"},
"https://example.test/",
false},
{{"https://*.example.test/", "https://*.example.test/ 'self'"},
"https://example.test/",
false},
{{"http://example.test/"}, "https://example.test/", false},
{{"https://example.test/"}, "https://example.test/", true},
// Origins of `required` and `response_csp` do not match.
{{"https://example.test/"}, "https://other-origin.test/", false},
{{"'self'"}, "https://other-origin.test/", true},
{{"http://example1.com/foo/ http://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ 'self'"},
"https://other-origin.test/",
true},
{{"http://example1.com/foo/ http://*.example2.com/bar/ "
"http://*.example3.com:*/bar/ https://other-origin.test/"},
"https://other-origin.test/",
true},
{{"http://example1.com/foo/ 'self'"}, "https://other-origin.test/", true},
{{"'self'", "https://example.test/"}, "https://other-origin.test/", true},
{{"'self' https://example.test/", "https://example.test/"},
"https://other-origin.test/",
false},
{{"https://example.test/", "http://example.test/"},
"https://other-origin.test/",
false},
{{"'self'", "http://other-origin.test/"},
"https://other-origin.test/",
true},
{{"'self'", "https://non-example.test/"},
"https://other-origin.test/",
true},
// `response_csp`'s origin matches one of the sources in the source list
// of `required`.
{{"'self'", "http://*.example1.com/foo/"}, "http://example1.com/", true},
{{"http://*.example2.com/bar/", "'self'"},
"http://example2.com/bar/",
true},
{{"'self' http://*.example1.com/foo/", "http://*.example1.com/foo/"},
"http://example1.com/",
false},
{{"http://*.example2.com/bar/ http://example1.com/",
"'self' http://example1.com/"},
"http://example2.com/bar/",
false},
};
for (const auto& test : cases) {
auto response_sources = ParseToVectorOfSourceLists(
mojom::CSPDirectiveName::ScriptSrc, test.response_csp);
EXPECT_EQ(test.expected,
CSPSourceListSubsumes(*required_sources,
ToRawPointers(response_sources),
mojom::CSPDirectiveName::ScriptSrc,
url::Origin::Create(GURL(test.origin))))
<< required << "from origin " << test.origin << " should "
<< (test.expected ? "" : "not ") << "subsume "
<< base::JoinString(test.response_csp, ", ");
}
}
TEST(CSPSourceList, SubsumeAllowAllInline) {
struct TestCase {
mojom::CSPDirectiveName directive;
std::string required;
std::vector<std::string> response_csp;
bool expected;
} cases[] = {
// `required` allows all inline behavior.
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"'unsafe-inline' http://example1.com/foo/bar.html"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"http://example1.com/foo/ 'unsafe-inline'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"'unsafe-inline' 'nonce-yay'", "'unsafe-inline'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"'unsafe-inline' 'nonce-yay'", "'unsafe-inline'", "'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"'unsafe-inline' 'nonce-yay'", "'unsafe-inline'",
"'strict-dynamic' 'nonce-yay'"},
true},
// `required` does not allow all inline behavior.
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'strict-dynamic'",
{"'unsafe-inline' http://example1.com/foo/bar.html"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self'",
{"'unsafe-inline'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"'unsafe-inline' 'nonce-yay'", "'nonce-abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self'",
{"'unsafe-inline' https://example.test/"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"'unsafe-inline' https://example.test/"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"'unsafe-inline' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'nonce-yay'",
{"'unsafe-inline' 'nonce-yay'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic' "
"'nonce-yay'",
{"'unsafe-inline' 'nonce-yay'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic' "
"'nonce-yay'",
{"http://example1.com/foo/ 'unsafe-inline' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba' "
"'strict-dynamic'",
{"'unsafe-inline' 'sha512-321cba'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic' "
"'sha512-321cba'",
{"http://example1.com/foo/ 'unsafe-inline' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba'",
{"http://example1.com/foo/ 'unsafe-inline'",
"http://example1.com/foo/ 'sha512-321cba'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba'",
{"http://example1.com/foo/ 'unsafe-inline'",
"http://example1.com/foo/ 'unsafe-inline' 'sha512-321cba'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba'",
{"http://example1.com/foo/ 'unsafe-inline' 'nonce-yay'",
"http://example1.com/foo/ 'unsafe-inline' 'sha512-321cba'"},
true},
};
for (const auto& test : cases) {
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(test.directive, test.required);
auto response_sources =
ParseToVectorOfSourceLists(test.directive, test.response_csp);
EXPECT_EQ(
test.expected,
CSPSourceListSubsumes(*required_sources,
ToRawPointers(response_sources), test.directive,
url::Origin::Create(GURL("https://frame.test"))))
<< test.required << " should " << (test.expected ? "" : "not ")
<< "subsume " << base::JoinString(test.response_csp, ", ");
}
}
TEST(CSPSourceList, SubsumeUnsafeAttributes) {
struct TestCase {
mojom::CSPDirectiveName directive;
std::string required;
std::vector<std::string> response_csp;
bool expected;
} cases[] = {
// `required` or `response_csp` contain `unsafe-eval`.
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic' "
"'unsafe-eval'",
{"http://example1.com/foo/bar.html 'unsafe-eval'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-eval'",
{"http://example1.com/foo/ 'unsafe-inline'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-eval'",
{"http://example1.com/foo/ 'unsafe-inline' 'unsafe-eval'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-eval'",
{"http://example1.com/foo/ 'unsafe-eval'",
"http://example1.com/foo/bar 'self' unsafe-eval'",
"http://non-example.com/foo/ 'unsafe-eval' 'self'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self'",
{"http://example1.com/foo/ 'unsafe-eval'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"http://example1.com/foo/ 'unsafe-eval'",
"http://example1.com/foo/bar 'self' 'unsafe-eval'",
"http://non-example.com/foo/ 'unsafe-eval' 'self'"},
false},
// `required` or `response_csp` contain `unsafe-hashes`.
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'unsafe-eval' "
"'strict-dynamic' "
"'unsafe-hashes'",
{"http://example1.com/foo/bar.html 'unsafe-hashes'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-hashes'",
{"http://example1.com/foo/ 'unsafe-inline'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-hashes'",
{"http://example1.com/foo/ 'unsafe-inline' 'unsafe-hashes'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-eval' "
"'unsafe-hashes'",
{"http://example1.com/foo/ 'unsafe-eval' 'unsafe-hashes'",
"http://example1.com/foo/bar 'self' 'unsafe-hashes'",
"http://non-example.com/foo/ 'unsafe-hashes' 'self'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self'",
{"http://example1.com/foo/ 'unsafe-hashes'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"http://example1.com/foo/ 'unsafe-hashes'",
"http://example1.com/foo/bar 'self' 'unsafe-hashes'",
"https://example1.com/foo/bar 'unsafe-hashes' 'self'"},
false},
};
for (const auto& test : cases) {
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(test.directive, test.required);
auto response_sources =
ParseToVectorOfSourceLists(test.directive, test.response_csp);
EXPECT_EQ(
test.expected,
CSPSourceListSubsumes(*required_sources,
ToRawPointers(response_sources), test.directive,
url::Origin::Create(GURL("https://frame.test"))))
<< test.required << " should " << (test.expected ? "" : "not ")
<< "subsume " << base::JoinString(test.response_csp, ", ");
}
}
TEST(CSPSourceList, SubsumeNoncesAndHashes) {
// For |required| to subsume |response_csp|:
// - If |response_csp| enforces some nonce, then |required| must contain some
// nonce, but they do not need to match.
// - On the other side, all hashes enforced by |response_csp| must be
// contained in |required|.
struct TestCase {
mojom::CSPDirectiveName directive;
std::string required;
std::vector<std::string> response_csp;
bool expected;
} cases[] = {
// Check nonces.
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'unsafe-inline' 'nonce-abc'",
{"'unsafe-inline'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'nonce-abc'",
{"'nonce-abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"'unsafe-inline' 'nonce-yay'", "'nonce-yay'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-yay'",
{"'unsafe-inline' 'nonce-yay'", "'nonce-yay'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-abc' 'nonce-yay'",
{"'unsafe-inline' https://example.test/"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-abc' 'nonce-yay'",
{"'nonce-abc' https://example1.com/foo/"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'nonce-yay' "
"'strict-dynamic'",
{"https://example.test/ 'nonce-yay'"},
false},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'nonce-yay' "
"'strict-dynamic'",
{"'nonce-yay' https://example1.com/foo/"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'nonce-abc'",
{"http://example1.com/foo/ 'nonce-xyz'"},
true},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'nonce-abc'",
{"http://example1.com/foo/ 'nonce-xyz'"},
true},
// Check hashes.
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba'",
{"http://example1.com/foo/page.html 'strict-dynamic'",
"https://example1.com/foo/ 'sha512-321cba'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba'",
{"http://some-other.com/ 'strict-dynamic' 'sha512-321cba'",
"http://example1.com/foo/ 'unsafe-inline' 'sha512-321cba'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba'",
{"http://example1.com/foo/ 'sha512-321abc' 'sha512-321cba'",
"http://example1.com/foo/ 'sha512-321abc' 'sha512-321cba'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321cba'",
{"http://example1.com/foo/ 'unsafe-inline'",
"http://example1.com/foo/ 'sha512-321cba'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc'",
{"http://example1.com/foo/ 'unsafe-inline' 'sha512-321abc'",
"http://example1.com/foo/ 'sha512-321abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc'",
{"'unsafe-inline' 'sha512-321abc'",
"http://example1.com/foo/ 'sha512-321abc'"},
true},
// Nonces and hashes together.
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc' "
"'nonce-abc'",
{"'unsafe-inline' 'sha512-321abc' 'self'",
"'unsafe-inline''sha512-321abc' https://example.test/"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc' "
"'nonce-abc'",
{"'unsafe-inline' 'sha512-321abc' 'self' 'nonce-abc'",
"'sha512-321abc' https://example.test/"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc' "
"'nonce-abc'",
{"'unsafe-inline' 'sha512-321abc' 'self'",
" 'sha512-321abc' https://example.test/ 'nonce-abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc' "
"'nonce-abc'",
{"'unsafe-inline' 'sha512-321abc' 'self' 'nonce-xyz'",
"unsafe-inline' 'sha512-321abc' https://example.test/ 'nonce-xyz'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc' "
"'nonce-abc'",
{"'unsafe-inline' 'sha512-321abc' 'self' 'sha512-xyz'",
"unsafe-inline' 'sha512-321abc' https://example.test/ 'sha512-xyz'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'nonce-abc' 'sha512-321abc'",
{"http://example1.com/foo/ 'nonce-xyz' 'sha512-321abc'"},
true},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'nonce-abc' 'sha512-321abc'",
{"http://example1.com/foo/ 'nonce-xyz' 'sha512-321abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'nonce-abc' 'sha512-321abc'",
{"http://example1.com/foo/ 'nonce-xyz' 'sha512-xyz'"},
false},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'nonce-abc' 'sha512-321abc'",
{"http://example1.com/foo/ 'nonce-xyz' 'sha512-xyz'",
"http://example1.com/foo/ 'nonce-zyx' 'nonce-xyz' 'sha512-xyz'"},
false},
};
for (const auto& test : cases) {
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(test.directive, test.required);
auto response_sources =
ParseToVectorOfSourceLists(test.directive, test.response_csp);
EXPECT_EQ(
test.expected,
CSPSourceListSubsumes(*required_sources,
ToRawPointers(response_sources), test.directive,
url::Origin::Create(GURL("https://frame.test"))))
<< test.required << " should " << (test.expected ? "" : "not ")
<< "subsume " << base::JoinString(test.response_csp, ", ");
}
}
TEST(CSPSourceList, SubsumeStrictDynamic) {
struct TestCase {
mojom::CSPDirectiveName directive;
std::string required;
std::vector<std::string> response_csp;
bool expected;
} cases[] = {
// Neither `required` nor effective policy of `response_csp` has
// `strict-dynamic`.
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'nonce-yay' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay'", "'nonce-yay' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'nonce-yay' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay'", "'nonce-abc' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc'",
{"'strict-dynamic' 'nonce-yay' 'sha512-321abc'",
"'sha512-321abc' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc'",
{"'strict-dynamic' 'nonce-yay' 'sha512-321abc'",
"'sha512-321abc' 'strict-dynamic'", "'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::StyleSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc'",
{"'strict-dynamic' 'nonce-yay' http://example1.com/",
"http://example1.com/ 'strict-dynamic'"},
false},
// `required` has `strict-dynamic`, effective policy of `response_csp`
// does not.
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-yay' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay'", "'nonce-yay'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"'strict-dynamic' 'sha512-321abc'", "'unsafe-inline' 'sha512-321abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"'sha512-321abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"http://example1.com/foo/ 'sha512-321abc'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"'self' 'sha512-321abc'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay'", "'nonce-yay'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"http://example1.com/ 'sha512-321abc'",
"http://example1.com/ 'sha512-321abc'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'sha512-321abc' 'strict-dynamic'",
{"https://example1.com/foo/ 'sha512-321abc'",
"http://example1.com/foo/ 'sha512-321abc'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-yay' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay'", "'nonce-yay'", "'sha512-321abc'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-hashes' "
"'strict-dynamic'",
{"'strict-dynamic' 'unsafe-hashes'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-yay' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay' 'unsafe-hashes'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-eval' 'strict-dynamic'",
{"'strict-dynamic' 'unsafe-eval'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-yay' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay' 'unsafe-eval'"},
false},
// `required` does not have `strict-dynamic`, but effective policy of
// `response_csp` does.
// Note that any subsumption in this set-up should be `false`.
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'nonce-yay'",
{"'strict-dynamic' 'nonce-yay'", "'sha512-321abc' 'strict-dynamic'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc'",
{"'strict-dynamic' 'sha512-321abc'", "'strict-dynamic' 'sha512-321abc'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline'",
{"'strict-dynamic'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'sha512-321abc'",
{"'strict-dynamic'"},
false},
// Both `required` and effective policy of `response_csp` has
// `strict-dynamic`.
{mojom::CSPDirectiveName::ScriptSrc,
"'strict-dynamic'",
{"'strict-dynamic'", "'strict-dynamic'", "'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"'strict-dynamic'",
{"'strict-dynamic'", "'strict-dynamic' 'nonce-yay'",
"'strict-dynamic' 'nonce-yay'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"'strict-dynamic' 'nonce-yay'",
{"http://example.com 'strict-dynamic' 'nonce-yay'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"'strict-dynamic' 'nonce-yay'", "'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"'strict-dynamic' http://another.com/",
"http://another.com/ 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'unsafe-inline' 'strict-dynamic'",
{"'self' 'sha512-321abc' 'strict-dynamic'",
"'self' 'strict-dynamic' 'sha512-321abc'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'sha512-321abc' 'strict-dynamic'",
{"'self' 'sha512-321abc' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"'unsafe-inline' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"'unsafe-inline' 'sha512-123xyz' 'strict-dynamic'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"'unsafe-eval' 'self' 'sha512-321abc' 'strict-dynamic'",
{"'unsafe-eval' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"'unsafe-eval' 'strict-dynamic'"},
false},
{mojom::CSPDirectiveName::ScriptSrc,
"'unsafe-hashes' 'self' 'sha512-321abc' 'strict-dynamic'",
{"'unsafe-hashes' 'strict-dynamic'"},
true},
{mojom::CSPDirectiveName::ScriptSrc,
"http://example1.com/foo/ 'self' 'sha512-321abc' 'strict-dynamic'",
{"'unsafe-hashes' 'strict-dynamic'"},
false},
};
for (const auto& test : cases) {
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(test.directive, test.required);
auto response_sources =
ParseToVectorOfSourceLists(test.directive, test.response_csp);
EXPECT_EQ(
test.expected,
CSPSourceListSubsumes(*required_sources,
ToRawPointers(response_sources), test.directive,
url::Origin::Create(GURL("https://frame.test"))))
<< test.required << " should " << (test.expected ? "" : "not ")
<< "subsume " << base::JoinString(test.response_csp, ", ");
}
}
TEST(CSPSourceList, SubsumeListWildcard) {
struct TestCase {
std::string required;
std::vector<std::string> response_csp;
bool expected;
} cases[] = {
// `required` subsumes `response_csp`.
{"*", {""}, true},
{"*", {"'none'"}, true},
{"*", {"*"}, true},
{"*", {"*", "*", "*"}, true},
{"*", {"*", "* https: http: ftp: ws: wss:"}, true},
{"*", {"*", "https: http: ftp: ws: wss:"}, true},
{"https: http: ftp: ws: wss:", {"*", "https: http: ftp: ws: wss:"}, true},
{"http: ftp: ws:", {"*", "https: http: ftp: ws: wss:"}, true},
{"http: ftp: ws:", {"*", "https: 'strict-dynamic'"}, true},
{"http://another.test", {"*", "'self'"}, true},
{"http://another.test/", {"*", "'self'"}, true},
{"http://another.test", {"https:", "'self'"}, true},
{"'self'", {"*", "'self'"}, true},
{"'unsafe-eval' * ", {"'unsafe-eval'"}, true},
{"'unsafe-hashes' * ", {"'unsafe-hashes'"}, true},
{"'unsafe-inline' * ", {"'unsafe-inline'"}, true},
{"*", {"*", "http://a.com ws://b.com ftp://c.com"}, true},
{"*", {"* data: blob:", "http://a.com ws://b.com ftp://c.com"}, true},
{"*", {"data: blob:", "http://a.com ws://b.com ftp://c.com"}, true},
{"*", {"*", "data://a.com ws://b.com ftp://c.com"}, true},
{"* data:",
{"data: blob: *", "data://a.com ws://b.com ftp://c.com"},
true},
{"http://a.com ws://b.com ftp://c.com",
{"*", "http://a.com ws://b.com ftp://c.com"},
true},
// `required` does not subsume `response_csp`.
{"*", std::vector<std::string>(), false},
{"", {"*"}, false},
{"'none'", {"*"}, false},
{"*", {"data:"}, false},
{"*", {"blob:"}, false},
{"http: ftp: ws:",
{"* 'strict-dynamic'", "https: 'strict-dynamic'"},
false},
{"https://another.test", {"*"}, false},
{"*", {"* 'unsafe-eval'"}, false},
{"*", {"* 'unsafe-hashes'"}, false},
{"*", {"* 'unsafe-inline'"}, false},
{"'unsafe-eval'", {"* 'unsafe-eval'"}, false},
{"'unsafe-hashes'", {"* 'unsafe-hashes'"}, false},
{"'unsafe-inline'", {"* 'unsafe-inline'"}, false},
{"*", {"data: blob:", "data://a.com ws://b.com ftp://c.com"}, false},
{"* data:",
{"data: blob:", "blob://a.com ws://b.com ftp://c.com"},
false},
};
for (const auto& test : cases) {
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(mojom::CSPDirectiveName::ScriptSrc, test.required);
auto response_sources = ParseToVectorOfSourceLists(
mojom::CSPDirectiveName::ScriptSrc, test.response_csp);
EXPECT_EQ(test.expected,
CSPSourceListSubsumes(
*required_sources, ToRawPointers(response_sources),
mojom::CSPDirectiveName::ScriptSrc,
url::Origin::Create(GURL("https://another.test/image.png"))))
<< test.required << " should " << (test.expected ? "" : "not ")
<< "subsume " << base::JoinString(test.response_csp, ", ");
}
}
TEST(CSPSourceList, SubsumeListNoScheme) {
struct TestCase {
std::string required;
std::vector<std::string> response_csp;
std::string origin;
bool expected;
} cases[] = {
{"http://a.com", {"a.com"}, "https://example.org", true},
{"https://a.com", {"a.com"}, "https://example.org", true},
{"https://a.com", {"a.com"}, "http://example.org", false},
{"data://a.com", {"a.com"}, "https://example.org", false},
{"a.com", {"a.com"}, "https://example.org", true},
{"a.com", {"a.com"}, "http://example.org", true},
{"a.com", {"https://a.com"}, "http://example.org", true},
{"a.com", {"https://a.com"}, "https://example.org", true},
{"a.com", {"http://a.com"}, "https://example.org", false},
{"https:", {"a.com"}, "http://example.org", false},
{"http:", {"a.com"}, "http://example.org", true},
{"https:", {"a.com", "https:"}, "http://example.org", true},
{"https:", {"a.com"}, "https://example.org", true},
};
for (const auto& test : cases) {
mojom::CSPSourceListPtr required_sources =
ParseToSourceList(mojom::CSPDirectiveName::ScriptSrc, test.required);
auto response_sources = ParseToVectorOfSourceLists(
mojom::CSPDirectiveName::ScriptSrc, test.response_csp);
EXPECT_EQ(test.expected,
CSPSourceListSubsumes(*required_sources,
ToRawPointers(response_sources),
mojom::CSPDirectiveName::ScriptSrc,
url::Origin::Create(GURL(test.origin))))
<< test.required << " on origin " << test.origin << " should "
<< (test.expected ? "" : "not ") << "subsume "
<< base::JoinString(test.response_csp, ", ");
}
}
} // namespace network } // namespace network
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