blob: 7679bfe74d77542b2f1dac616d3a1ac4a823811b [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/frame/csp/csp_directive_list.h"
#include <list>
#include <string>
#include "base/test/scoped_feature_list.h"
#include "services/network/public/cpp/features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
#include "third_party/blink/renderer/platform/loader/subresource_integrity.h"
#include "third_party/blink/renderer/platform/network/content_security_policy_parsers.h"
#include "third_party/blink/renderer/platform/wtf/text/string_operators.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
using network::mojom::ContentSecurityPolicySource;
using network::mojom::ContentSecurityPolicyType;
class CSPDirectiveListTest : public testing::Test {
public:
CSPDirectiveListTest() : csp(MakeGarbageCollected<ContentSecurityPolicy>()) {}
void SetUp() override {
scoped_feature_list_.InitWithFeatures({network::features::kReporting}, {});
}
network::mojom::blink::ContentSecurityPolicyPtr CreateList(
const String& list,
ContentSecurityPolicyType type,
ContentSecurityPolicySource source = ContentSecurityPolicySource::kHTTP) {
Vector<UChar> characters;
list.AppendTo(characters);
const UChar* begin = characters.data();
const UChar* end = begin + characters.size();
scoped_refptr<SecurityOrigin> self_origin =
SecurityOrigin::Create(KURL("https://example.test/index.html"));
return CSPDirectiveListParse(csp.Get(), begin, end, *self_origin, type,
source);
}
protected:
Persistent<ContentSecurityPolicy> csp;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(CSPDirectiveListTest, Header) {
struct TestCase {
const char* list;
const char* expected;
} cases[] = {{"script-src 'self'", "script-src 'self'"},
{" script-src 'self' ", "script-src 'self'"},
{"\t\tscript-src 'self'", "script-src 'self'"},
{"script-src 'self' \t", "script-src 'self'"}};
for (const auto& test : cases) {
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(test.expected, directive_list->header->header_value);
directive_list = CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.expected, directive_list->header->header_value);
}
}
TEST_F(CSPDirectiveListTest, IsMatchingNoncePresent) {
struct TestCase {
const char* list;
const char* nonce;
bool expected;
} cases[] = {
{"script-src 'self'", "yay", false},
{"script-src 'self'", "boo", false},
{"script-src 'nonce-yay'", "yay", true},
{"script-src 'nonce-yay'", "boo", false},
{"script-src 'nonce-yay' 'nonce-boo'", "yay", true},
{"script-src 'nonce-yay' 'nonce-boo'", "boo", true},
// Falls back to 'default-src'
{"default-src 'nonce-yay'", "yay", true},
{"default-src 'nonce-yay'", "boo", false},
{"default-src 'nonce-boo'; script-src 'nonce-yay'", "yay", true},
{"default-src 'nonce-boo'; script-src 'nonce-yay'", "boo", false},
// Unrelated directives do not affect result
{"style-src 'nonce-yay'; default-src 'none'", "yay", false},
{"style-src 'nonce-yay'; default-src 'none'", "boo", false},
{"script-src-attr 'nonce-yay'; default-src 'none'", "yay", false},
// Script-src-elem falls back on script-src and then default-src.
{"script-src 'nonce-yay'", "yay", true},
{"script-src 'nonce-yay'; default-src 'nonce-boo'", "yay", true},
{"script-src 'nonce-boo'; default-src 'nonce-yay'", "yay", false},
{"script-src-elem 'nonce-yay'; script-src 'nonce-boo'; default-src "
"'nonce-boo'",
"yay", true},
{"default-src 'nonce-yay'", "yay", true},
{"script-src-attr 'nonce-yay'; script-src 'nonce-boo'; default-src "
"'nonce-foo'",
"yay", false},
{"script-src-attr 'nonce-yay'; script-src 'nonce-boo'; default-src "
"'nonce-foo'",
"boo", true},
{"script-src-attr 'nonce-yay'; script-src 'nonce-boo'; default-src "
"'nonce-foo'",
"foo", false},
};
KURL blocked_url = KURL("https://blocked.com");
for (const auto& test : cases) {
// Report-only
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
blocked_url, blocked_url,
ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, test.nonce));
// Enforce
directive_list = CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
blocked_url, blocked_url,
ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, test.nonce));
}
}
TEST_F(CSPDirectiveListTest, AllowScriptFromSourceNoNonce) {
struct TestCase {
const char* list;
const char* url;
bool expected;
} cases[] = {
{"script-src https://example.com", "https://example.com/script.js", true},
{"script-src https://example.com/", "https://example.com/script.js",
true},
{"script-src https://example.com/",
"https://example.com/script/script.js", true},
{"script-src https://example.com/script", "https://example.com/script.js",
false},
{"script-src https://example.com/script",
"https://example.com/script/script.js", false},
{"script-src https://example.com/script/",
"https://example.com/script.js", false},
{"script-src https://example.com/script/",
"https://example.com/script/script.js", true},
{"script-src https://example.com", "https://not.example.com/script.js",
false},
{"script-src https://*.example.com", "https://not.example.com/script.js",
true},
{"script-src https://*.example.com", "https://example.com/script.js",
false},
// Falls back to default-src:
{"default-src https://example.com", "https://example.com/script.js",
true},
{"default-src https://example.com/", "https://example.com/script.js",
true},
{"default-src https://example.com/",
"https://example.com/script/script.js", true},
{"default-src https://example.com/script",
"https://example.com/script.js", false},
{"default-src https://example.com/script",
"https://example.com/script/script.js", false},
{"default-src https://example.com/script/",
"https://example.com/script.js", false},
{"default-src https://example.com/script/",
"https://example.com/script/script.js", true},
{"default-src https://example.com", "https://not.example.com/script.js",
false},
{"default-src https://*.example.com", "https://not.example.com/script.js",
true},
{"default-src https://*.example.com", "https://example.com/script.js",
false},
};
for (const auto& test : cases) {
SCOPED_TRACE(testing::Message()
<< "List: `" << test.list << "`, URL: `" << test.url << "`");
const KURL script_src(test.url);
// Report-only
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
script_src, script_src,
ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(),
IntegrityMetadataSet(), kParserInserted));
// Enforce
directive_list = CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
script_src, script_src,
ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(),
IntegrityMetadataSet(), kParserInserted));
}
}
TEST_F(CSPDirectiveListTest, AllowFromSourceWithNonce) {
struct TestCase {
const char* list;
const char* url;
const char* nonce;
bool expected;
} cases[] = {
// Doesn't affect lists without nonces:
{"https://example.com", "https://example.com/file", "yay", true},
{"https://example.com", "https://example.com/file", "boo", true},
{"https://example.com", "https://example.com/file", "", true},
{"https://example.com", "https://not.example.com/file", "yay", false},
{"https://example.com", "https://not.example.com/file", "boo", false},
{"https://example.com", "https://not.example.com/file", "", false},
// Doesn't affect URLs that match the allowlist.
{"https://example.com 'nonce-yay'", "https://example.com/file", "yay",
true},
{"https://example.com 'nonce-yay'", "https://example.com/file", "boo",
true},
{"https://example.com 'nonce-yay'", "https://example.com/file", "", true},
// Does affect URLs that don't.
{"https://example.com 'nonce-yay'", "https://not.example.com/file", "yay",
true},
{"https://example.com 'nonce-yay'", "https://not.example.com/file", "boo",
false},
{"https://example.com 'nonce-yay'", "https://not.example.com/file", "",
false},
};
for (const auto& test : cases) {
SCOPED_TRACE(testing::Message()
<< "List: `" << test.list << "`, URL: `" << test.url << "`");
const KURL resource(test.url);
// Report-only 'script-src'
network::mojom::blink::ContentSecurityPolicyPtr directive_list = CreateList(
String("script-src ") + test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce),
IntegrityMetadataSet(), kParserInserted));
// Enforce 'script-src'
directive_list = CreateList(String("script-src ") + test.list,
ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce),
IntegrityMetadataSet(), kParserInserted));
// Report-only 'style-src'
directive_list = CreateList(String("style-src ") + test.list,
ContentSecurityPolicyType::kReport);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::StyleSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce)));
// Enforce 'style-src'
directive_list = CreateList(String("style-src ") + test.list,
ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::StyleSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce)));
// Report-only 'style-src'
directive_list = CreateList(String("default-src ") + test.list,
ContentSecurityPolicyType::kReport);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce)));
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::StyleSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce)));
// Enforce 'style-src'
directive_list = CreateList(String("default-src ") + test.list,
ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce),
IntegrityMetadataSet(), kParserInserted));
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::StyleSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(test.nonce)));
}
}
TEST_F(CSPDirectiveListTest, AllowScriptFromSourceWithHash) {
struct TestCase {
const char* list;
const char* url;
const char* integrity;
bool expected;
} cases[] = {
// Doesn't affect lists without hashes.
{"https://example.com", "https://example.com/file", "sha256-yay", true},
{"https://example.com", "https://example.com/file", "sha256-boo", true},
{"https://example.com", "https://example.com/file", "", true},
{"https://example.com", "https://not.example.com/file", "sha256-yay",
false},
{"https://example.com", "https://not.example.com/file", "sha256-boo",
false},
{"https://example.com", "https://not.example.com/file", "", false},
// Doesn't affect URLs that match the allowlist.
{"https://example.com 'sha256-yay'", "https://example.com/file",
"sha256-yay", true},
{"https://example.com 'sha256-yay'", "https://example.com/file",
"sha256-boo", true},
{"https://example.com 'sha256-yay'", "https://example.com/file", "",
true},
// Does affect URLs that don't match the allowlist.
{"https://example.com 'sha256-yay'", "https://not.example.com/file",
"sha256-yay", true},
{"https://example.com 'sha256-yay'", "https://not.example.com/file",
"sha256-boo", false},
{"https://example.com 'sha256-yay'", "https://not.example.com/file", "",
false},
// Both algorithm and digest must match.
{"'sha256-yay'", "https://a.com/file", "sha384-yay", false},
// Sha-1 is not supported, but -384 and -512 are.
{"'sha1-yay'", "https://a.com/file", "sha1-yay", false},
{"'sha384-yay'", "https://a.com/file", "sha384-yay", true},
{"'sha512-yay'", "https://a.com/file", "sha512-yay", true},
// Unknown (or future) hash algorithms don't work.
{"'asdf256-yay'", "https://a.com/file", "asdf256-yay", false},
// But they also don't interfere.
{"'sha256-yay'", "https://a.com/file", "sha256-yay asdf256-boo", true},
// Additional allowlisted hashes in the CSP don't interfere.
{"'sha256-yay' 'sha384-boo'", "https://a.com/file", "sha256-yay", true},
{"'sha256-yay' 'sha384-boo'", "https://a.com/file", "sha384-boo", true},
// All integrity hashes must appear in the CSP (and match).
{"'sha256-yay'", "https://a.com/file", "sha256-yay sha384-boo", false},
{"'sha384-boo'", "https://a.com/file", "sha256-yay sha384-boo", false},
{"'sha256-yay' 'sha384-boo'", "https://a.com/file",
"sha256-yay sha384-yay", false},
{"'sha256-yay' 'sha384-boo'", "https://a.com/file",
"sha256-boo sha384-boo", false},
{"'sha256-yay' 'sha384-boo'", "https://a.com/file",
"sha256-yay sha384-boo", true},
// At least one integrity hash must be present.
{"'sha256-yay'", "https://a.com/file", "", false},
};
for (const auto& test : cases) {
SCOPED_TRACE(testing::Message()
<< "List: `" << test.list << "`, URL: `" << test.url
<< "`, Integrity: `" << test.integrity << "`");
const KURL resource(test.url);
IntegrityMetadataSet integrity_metadata;
EXPECT_EQ(
SubresourceIntegrity::kIntegrityParseValidResult,
SubresourceIntegrity::ParseIntegrityAttribute(
test.integrity, SubresourceIntegrity::IntegrityFeatures::kDefault,
integrity_metadata));
// Report-only 'script-src'
network::mojom::blink::ContentSecurityPolicyPtr directive_list = CreateList(
String("script-src ") + test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(),
integrity_metadata, kParserInserted));
// Enforce 'script-src'
directive_list = CreateList(String("script-src ") + test.list,
ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(
test.expected,
CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(), CSPDirectiveName::ScriptSrcElem,
resource, resource, ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting, String(),
integrity_metadata, kParserInserted));
}
}
TEST_F(CSPDirectiveListTest, WorkerSrc) {
struct TestCase {
const char* list;
bool allowed;
} cases[] = {
{"worker-src 'none'", false},
{"worker-src http://not.example.test", false},
{"worker-src https://example.test", true},
{"default-src *; worker-src 'none'", false},
{"default-src *; worker-src http://not.example.test", false},
{"default-src *; worker-src https://example.test", true},
{"script-src *; worker-src 'none'", false},
{"script-src *; worker-src http://not.example.test", false},
{"script-src *; worker-src https://example.test", true},
{"default-src *; script-src *; worker-src 'none'", false},
{"default-src *; script-src *; worker-src http://not.example.test",
false},
{"default-src *; script-src *; worker-src https://example.test", true},
// Fallback to script-src.
{"script-src 'none'", false},
{"script-src http://not.example.test", false},
{"script-src https://example.test", true},
{"default-src *; script-src 'none'", false},
{"default-src *; script-src http://not.example.test", false},
{"default-src *; script-src https://example.test", true},
// Fallback to default-src.
{"default-src 'none'", false},
{"default-src http://not.example.test", false},
{"default-src https://example.test", true},
};
for (const auto& test : cases) {
SCOPED_TRACE(test.list);
const KURL resource("https://example.test/worker.js");
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.allowed, CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(),
CSPDirectiveName::WorkerSrc, resource, resource,
ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting));
}
}
TEST_F(CSPDirectiveListTest, WorkerSrcChildSrcFallback) {
// TODO(mkwst): Remove this test once we remove the temporary fallback
// behavior. https://crbug.com/662930
struct TestCase {
const char* list;
bool allowed;
} cases[] = {
// When 'worker-src' is not present, 'child-src' can allow a worker when
// present.
{"child-src https://example.test", true},
{"child-src https://not-example.test", false},
{"script-src https://example.test", true},
{"script-src https://not-example.test", false},
{"child-src https://example.test; script-src https://example.test", true},
{"child-src https://example.test; script-src https://not-example.test",
true},
{"child-src https://not-example.test; script-src https://example.test",
false},
{"child-src https://not-example.test; script-src "
"https://not-example.test",
false},
// If 'worker-src' is present, 'child-src' will not allow a worker.
{"worker-src https://example.test; child-src https://example.test", true},
{"worker-src https://example.test; child-src https://not-example.test",
true},
{"worker-src https://not-example.test; child-src https://example.test",
false},
{"worker-src https://not-example.test; child-src "
"https://not-example.test",
false},
};
for (const auto& test : cases) {
SCOPED_TRACE(test.list);
const KURL resource("https://example.test/worker.js");
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.allowed, CSPDirectiveListAllowFromSource(
*directive_list, csp.Get(),
CSPDirectiveName::WorkerSrc, resource, resource,
ResourceRequest::RedirectStatus::kNoRedirect,
ReportingDisposition::kSuppressReporting));
}
}
TEST_F(CSPDirectiveListTest, OperativeDirectiveGivenType) {
struct TestCase {
CSPDirectiveName directive;
Vector<CSPDirectiveName> fallback_list;
} cases[] = {
// Directives with default directive.
{CSPDirectiveName::ChildSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::ConnectSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::FontSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::ImgSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::ManifestSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::MediaSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::ObjectSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::ScriptSrc, {CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::StyleSrc, {CSPDirectiveName::DefaultSrc}},
// Directives with no default directive.
{CSPDirectiveName::BaseURI, {}},
{CSPDirectiveName::DefaultSrc, {}},
{CSPDirectiveName::FrameAncestors, {}},
{CSPDirectiveName::FormAction, {}},
// Directive with multiple default directives.
{CSPDirectiveName::ScriptSrcAttr,
{CSPDirectiveName::ScriptSrc, CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::ScriptSrcElem,
{CSPDirectiveName::ScriptSrc, CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::FrameSrc,
{CSPDirectiveName::ChildSrc, CSPDirectiveName::DefaultSrc}},
{CSPDirectiveName::WorkerSrc,
{CSPDirectiveName::ChildSrc, CSPDirectiveName::ScriptSrc,
CSPDirectiveName::DefaultSrc}},
};
std::stringstream all_directives;
for (const auto& test : cases) {
const char* name = ContentSecurityPolicy::GetDirectiveName(test.directive);
all_directives << name << " http://" << name << ".com; ";
}
network::mojom::blink::ContentSecurityPolicyPtr empty =
CreateList("", ContentSecurityPolicyType::kEnforce);
std::string directive_string;
network::mojom::blink::ContentSecurityPolicyPtr directive_list;
// Initial set-up.
for (auto& test : cases) {
// With an empty directive list the returned directive should always be
// null.
EXPECT_FALSE(
CSPDirectiveListOperativeDirective(*empty, test.directive).source_list);
// Add the directive itself as it should be the first one to be returned.
test.fallback_list.push_front(test.directive);
// Start the tests with all directives present.
directive_string = all_directives.str();
while (!test.fallback_list.IsEmpty()) {
directive_list = CreateList(directive_string.c_str(),
ContentSecurityPolicyType::kEnforce);
CSPOperativeDirective operative_directive =
CSPDirectiveListOperativeDirective(*directive_list, test.directive);
// We should have an actual directive returned here.
EXPECT_TRUE(operative_directive.source_list);
// The OperativeDirective should be first one in the fallback chain.
EXPECT_EQ(test.fallback_list.front(), operative_directive.type);
// Remove the first directive in the fallback chain from the directive
// list and continue by testing that the next one is returned until we
// have no more directives in the fallback list.
const char* current_directive_name =
ContentSecurityPolicy::GetDirectiveName(test.fallback_list.front());
std::stringstream current_directive;
current_directive << current_directive_name << " http://"
<< current_directive_name << ".com; ";
size_t index = directive_string.find(current_directive.str());
directive_string.replace(index, current_directive.str().size(), "");
test.fallback_list.erase(test.fallback_list.begin());
}
// After we have checked and removed all the directives in the fallback
// chain we should ensure that there is no unexpected directive outside of
// the fallback chain that is returned.
directive_list = CreateList(directive_string.c_str(),
ContentSecurityPolicyType::kEnforce);
EXPECT_FALSE(
CSPDirectiveListOperativeDirective(*directive_list, test.directive)
.source_list);
}
}
TEST_F(CSPDirectiveListTest, ReportEndpointsProperlyParsed) {
struct TestCase {
const char* policy;
ContentSecurityPolicySource header_source;
Vector<String> expected_endpoints;
bool expected_use_reporting_api;
} cases[] = {
{"script-src 'self';", ContentSecurityPolicySource::kHTTP, {}, false},
{"script-src 'self'; report-uri https://example.com",
ContentSecurityPolicySource::kHTTP,
{"https://example.com"},
false},
{"script-src 'self'; report-uri https://example.com "
"https://example2.com",
ContentSecurityPolicySource::kHTTP,
{"https://example.com", "https://example2.com"},
false},
{"script-src 'self'; report-uri https://example.com "
"http://example2.com /relative/path",
// Mixed Content report-uri endpoint is ignored.
ContentSecurityPolicySource::kHTTP,
{"https://example.com", "/relative/path"},
false},
{"script-src 'self'; report-uri https://example.com",
ContentSecurityPolicySource::kMeta,
{},
false},
{"script-src 'self'; report-to group",
ContentSecurityPolicySource::kHTTP,
{"group"},
true},
// report-to supersedes report-uri
{"script-src 'self'; report-to group; report-uri https://example.com",
ContentSecurityPolicySource::kHTTP,
{"group"},
true},
{"script-src 'self'; report-to group",
ContentSecurityPolicySource::kMeta,
{"group"},
true},
{"script-src 'self'; report-to group group2",
ContentSecurityPolicySource::kHTTP,
// Only the first report-to endpoint is used. The other ones are ignored.
{"group"},
true},
{"script-src 'self'; report-to group; report-to group2;",
ContentSecurityPolicySource::kHTTP,
{"group"},
true},
{"script-src 'self'; report-to group; report-uri https://example.com; "
"report-to group2",
ContentSecurityPolicySource::kHTTP,
{"group"},
true},
{"script-src 'self'; report-uri https://example.com; report-to group; "
"report-to group2",
ContentSecurityPolicySource::kHTTP,
{"group"},
true},
{"script-src 'self'; report-uri https://example.com "
"https://example2.com; report-to group",
ContentSecurityPolicySource::kHTTP,
{"group"},
true},
{"script-src 'self'; report-uri https://example.com; report-to group; "
"report-uri https://example.com",
ContentSecurityPolicySource::kHTTP,
{"group"},
true},
};
for (const auto& test : cases) {
// Test both enforce and report, there should not be a difference
for (const auto& header_type : {ContentSecurityPolicyType::kEnforce,
ContentSecurityPolicyType::kReport}) {
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.policy, header_type, test.header_source);
EXPECT_EQ(directive_list->use_reporting_api,
test.expected_use_reporting_api);
EXPECT_EQ(directive_list->report_endpoints.size(),
test.expected_endpoints.size());
for (const String& endpoint : test.expected_endpoints) {
EXPECT_TRUE(directive_list->report_endpoints.Contains(endpoint));
}
for (const String& endpoint : directive_list->report_endpoints) {
EXPECT_TRUE(test.expected_endpoints.Contains(endpoint));
}
}
}
}
TEST_F(CSPDirectiveListTest, ReasonableObjectRestriction) {
struct TestCase {
const char* list;
bool expected;
} cases[] = {// Insufficient restriction!
{"img-src *", false},
{"object-src *", false},
{"object-src https://very.safe.test/", false},
{"object-src https:", false},
{"script-src *", false},
{"script-src https://very.safe.test/", false},
{"script-src https:", false},
{"script-src 'none'; object-src *", false},
{"script-src 'none'; object-src https://very.safe.test/", false},
{"script-src 'none'; object-src https:", false},
// Sufficient restrictions!
{"default-src 'none'", true},
{"object-src 'none'", true},
{"object-src 'none'; script-src 'unsafe-inline'", true},
{"object-src 'none'; script-src *", true}};
for (const auto& test : cases) {
SCOPED_TRACE(testing::Message() << "List: `" << test.list << "`");
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(test.expected,
CSPDirectiveListIsObjectRestrictionReasonable(*directive_list));
directive_list = CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.expected,
CSPDirectiveListIsObjectRestrictionReasonable(*directive_list));
}
}
TEST_F(CSPDirectiveListTest, ReasonableBaseRestriction) {
struct TestCase {
const char* list;
bool expected;
} cases[] = {// Insufficient restriction!
{"default-src 'none'", false},
{"base-uri https://very.safe.test/", false},
{"base-uri *", false},
{"base-uri https:", false},
// Sufficient restrictions!
{"base-uri 'none'", true},
{"base-uri 'self'", true}};
for (const auto& test : cases) {
SCOPED_TRACE(testing::Message() << "List: `" << test.list << "`");
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(test.expected,
CSPDirectiveListIsBaseRestrictionReasonable(*directive_list));
directive_list = CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.expected,
CSPDirectiveListIsBaseRestrictionReasonable(*directive_list));
}
}
TEST_F(CSPDirectiveListTest, ReasonableScriptRestriction) {
struct TestCase {
const char* list;
bool expected;
} cases[] = {
// Insufficient restriction!
{"img-src *", false},
{"script-src *", false},
{"script-src https://very.safe.test/", false},
{"script-src https:", false},
{"default-src 'none'; script-src *", false},
{"default-src 'none'; script-src https://very.safe.test/", false},
{"default-src 'none'; script-src https:", false},
// Sufficient restrictions!
{"default-src 'none'", true},
{"script-src 'none'", true},
{"script-src 'nonce-abc'", true},
{"script-src 'sha256-abc'", true},
{"script-src 'nonce-abc' 'unsafe-inline'", true},
{"script-src 'sha256-abc' 'unsafe-inline'", true},
{"script-src 'nonce-abc' 'strict-dynamic'", true},
{"script-src 'sha256-abc' 'strict-dynamic'", true},
{"script-src 'nonce-abc' 'unsafe-inline' 'strict-dynamic'", true},
{"script-src 'sha256-abc' 'unsafe-inline' 'strict-dynamic'", true},
{"script-src 'nonce-abc' 'unsafe-inline' 'unsafe-eval' 'unsafe-hashes'",
true},
{"script-src 'sha256-abc' 'unsafe-inline' 'unsafe-eval' 'unsafe-hashes'",
true},
{"script-src 'nonce-abc' 'strict-dynamic' 'unsafe-eval' 'unsafe-hashes'",
true},
{"script-src 'sha256-abc' 'strict-dynamic' 'unsafe-eval' 'unsafe-hashes'",
true},
{"script-src 'nonce-abc' 'unsafe-inline' 'strict-dynamic' 'unsafe-eval' "
"'unsafe-hashes'",
true},
{"script-src 'sha256-abc' 'unsafe-inline' 'strict-dynamic' 'unsafe-eval' "
"'unsafe-hashes'",
true}};
for (const auto& test : cases) {
SCOPED_TRACE(testing::Message() << "List: `" << test.list << "`");
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(test.list, ContentSecurityPolicyType::kReport);
EXPECT_EQ(test.expected,
CSPDirectiveListIsScriptRestrictionReasonable(*directive_list));
directive_list = CreateList(test.list, ContentSecurityPolicyType::kEnforce);
EXPECT_EQ(test.expected,
CSPDirectiveListIsScriptRestrictionReasonable(*directive_list));
}
}
// Tests that report-uri directives are discarded from policies
// delivered in <meta> elements.
TEST_F(CSPDirectiveListTest, ReportURIInMeta) {
String policy = "img-src 'none'; report-uri https://foo.test";
network::mojom::blink::ContentSecurityPolicyPtr directive_list =
CreateList(policy, ContentSecurityPolicyType::kEnforce,
ContentSecurityPolicySource::kMeta);
EXPECT_TRUE(directive_list->report_endpoints.IsEmpty());
directive_list = CreateList(policy, ContentSecurityPolicyType::kEnforce,
ContentSecurityPolicySource::kHTTP);
EXPECT_FALSE(directive_list->report_endpoints.IsEmpty());
}
} // namespace blink