blob: f35e252329ddbe7276fcedb79cc27f7272347123 [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 "content/browser/renderer_host/ancestor_throttle.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/ref_counted.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/navigation_simulator_impl.h"
#include "content/test/test_navigation_url_loader.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/features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
namespace {
using HeaderDisposition = AncestorThrottle::HeaderDisposition;
net::HttpResponseHeaders* GetAncestorHeaders(const char* xfo, const char* csp) {
std::string header_string("HTTP/1.1 200 OK\nX-Frame-Options: ");
header_string += xfo;
if (csp != nullptr) {
header_string += "\nContent-Security-Policy: ";
header_string += csp;
}
header_string += "\n\n";
std::replace(header_string.begin(), header_string.end(), '\n', '\0');
net::HttpResponseHeaders* headers =
new net::HttpResponseHeaders(header_string);
EXPECT_TRUE(headers->HasHeader("X-Frame-Options"));
if (csp != nullptr)
EXPECT_TRUE(headers->HasHeader("Content-Security-Policy"));
return headers;
}
network::mojom::ContentSecurityPolicyPtr ParsePolicy(
const std::string& policy) {
scoped_refptr<net::HttpResponseHeaders> headers(
new net::HttpResponseHeaders("HTTP/1.1 200 OK"));
headers->SetHeader("Content-Security-Policy", policy);
std::vector<network::mojom::ContentSecurityPolicyPtr> policies;
network::AddContentSecurityPolicyFromHeaders(
*headers, GURL("https://example.com/"), &policies);
return std::move(policies[0]);
}
} // namespace
// AncestorThrottleTest
// -------------------------------------------------------------
class AncestorThrottleTest : public testing::Test {};
TEST_F(AncestorThrottleTest, ParsingXFrameOptions) {
struct TestCase {
const char* header;
AncestorThrottle::HeaderDisposition expected;
const char* value;
} cases[] = {
// Basic keywords
{"DENY", HeaderDisposition::DENY, "DENY"},
{"SAMEORIGIN", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
{"ALLOWALL", HeaderDisposition::ALLOWALL, "ALLOWALL"},
// Repeated keywords
{"DENY,DENY", HeaderDisposition::DENY, "DENY, DENY"},
{"SAMEORIGIN,SAMEORIGIN", HeaderDisposition::SAMEORIGIN,
"SAMEORIGIN, SAMEORIGIN"},
{"ALLOWALL,ALLOWALL", HeaderDisposition::ALLOWALL, "ALLOWALL, ALLOWALL"},
// Case-insensitive
{"deNy", HeaderDisposition::DENY, "deNy"},
{"sAmEorIgIn", HeaderDisposition::SAMEORIGIN, "sAmEorIgIn"},
{"AlLOWaLL", HeaderDisposition::ALLOWALL, "AlLOWaLL"},
// Trim whitespace
{" DENY", HeaderDisposition::DENY, "DENY"},
{"SAMEORIGIN ", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
{" ALLOWALL ", HeaderDisposition::ALLOWALL, "ALLOWALL"},
{" DENY", HeaderDisposition::DENY, "DENY"},
{"SAMEORIGIN ", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
{" ALLOWALL ", HeaderDisposition::ALLOWALL, "ALLOWALL"},
{" DENY , DENY ", HeaderDisposition::DENY, "DENY, DENY"},
{"SAMEORIGIN, SAMEORIGIN", HeaderDisposition::SAMEORIGIN,
"SAMEORIGIN, SAMEORIGIN"},
{"ALLOWALL ,ALLOWALL", HeaderDisposition::ALLOWALL,
"ALLOWALL, ALLOWALL"},
};
AncestorThrottle throttle(nullptr);
for (const auto& test : cases) {
SCOPED_TRACE(test.header);
scoped_refptr<net::HttpResponseHeaders> headers =
GetAncestorHeaders(test.header, nullptr);
std::string header_value;
EXPECT_EQ(test.expected,
throttle.ParseXFrameOptionsHeader(headers.get(), &header_value));
EXPECT_EQ(test.value, header_value);
}
}
TEST_F(AncestorThrottleTest, ErrorsParsingXFrameOptions) {
struct TestCase {
const char* header;
AncestorThrottle::HeaderDisposition expected;
const char* failure;
} cases[] = {
// Empty == Invalid.
{"", HeaderDisposition::INVALID, ""},
// Invalid
{"INVALID", HeaderDisposition::INVALID, "INVALID"},
{"INVALID DENY", HeaderDisposition::INVALID, "INVALID DENY"},
{"DENY DENY", HeaderDisposition::INVALID, "DENY DENY"},
{"DE NY", HeaderDisposition::INVALID, "DE NY"},
// Conflicts
{"INVALID,DENY", HeaderDisposition::CONFLICT, "INVALID, DENY"},
{"DENY,ALLOWALL", HeaderDisposition::CONFLICT, "DENY, ALLOWALL"},
{"SAMEORIGIN,DENY", HeaderDisposition::CONFLICT, "SAMEORIGIN, DENY"},
{"ALLOWALL,SAMEORIGIN", HeaderDisposition::CONFLICT,
"ALLOWALL, SAMEORIGIN"},
{"DENY, SAMEORIGIN", HeaderDisposition::CONFLICT, "DENY, SAMEORIGIN"}};
AncestorThrottle throttle(nullptr);
for (const auto& test : cases) {
SCOPED_TRACE(test.header);
scoped_refptr<net::HttpResponseHeaders> headers =
GetAncestorHeaders(test.header, nullptr);
std::string header_value;
EXPECT_EQ(test.expected,
throttle.ParseXFrameOptionsHeader(headers.get(), &header_value));
EXPECT_EQ(test.failure, header_value);
}
}
TEST_F(AncestorThrottleTest, AllowsBlanketEnforcementOfRequiredCSP) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(network::features::kOutOfBlinkCSPEE);
struct TestCase {
const char* name;
const char* request_origin;
const char* response_origin;
const char* allow_csp_from;
bool expected_result;
} cases[] = {
{
"About scheme allows",
"http://example.com",
"about://me",
nullptr,
true,
},
{
"File scheme allows",
"http://example.com",
"file://me",
nullptr,
true,
},
{
"Data scheme allows",
"http://example.com",
"data://me",
nullptr,
true,
},
{
"Filesystem scheme allows",
"http://example.com",
"filesystem://me",
nullptr,
true,
},
{
"Blob scheme allows",
"http://example.com",
"blob://me",
nullptr,
true,
},
{
"Same origin allows",
"http://example.com",
"http://example.com",
nullptr,
true,
},
{
"Same origin allows independently of header",
"http://example.com",
"http://example.com",
"http://not-example.com",
true,
},
{
"Different origin does not allow",
"http://example.com",
"http://not.example.com",
nullptr,
false,
},
{
"Different origin with right header allows",
"http://example.com",
"http://not-example.com",
"http://example.com",
true,
},
{
"Different origin with right header 2 allows",
"http://example.com",
"http://not-example.com",
"http://example.com/",
true,
},
{
"Different origin with wrong header does not allow",
"http://example.com",
"http://not-example.com",
"http://not-example.com",
false,
},
{
"Wildcard header allows",
"http://example.com",
"http://not-example.com",
"*",
true,
},
{
"Malformed header does not allow",
"http://example.com",
"http://not-example.com",
"*; http://example.com",
false,
},
};
for (const auto& test : cases) {
SCOPED_TRACE(test.name);
auto headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
if (test.allow_csp_from)
headers->AddHeader("allow-csp-from", test.allow_csp_from);
auto allow_csp_from = network::ParseAllowCSPFromHeader(*headers);
bool actual = AncestorThrottle::AllowsBlanketEnforcementOfRequiredCSP(
url::Origin::Create(GURL(test.request_origin)),
GURL(test.response_origin), allow_csp_from);
EXPECT_EQ(test.expected_result, actual);
}
}
using AncestorThrottleNavigationTest = RenderViewHostTestHarness;
TEST_F(AncestorThrottleNavigationTest,
WillStartRequestAddsSecRequiredCSPHeader) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(network::features::kOutOfBlinkCSPEE);
// Create a frame tree with different 'csp' attributes according to the
// following graph:
//
// FRAME NAME | 'csp' attribute
// ------------------------------|-------------------------------------
// main_frame | (none)
// ├─child_with_csp | script-src 'none'
// │ ├─grandchild_same_csp | script-src 'none'
// │ ├─grandchild_no_csp | (none)
// │ │ └─grandgrandchild | (none)
// │ ├─grandchild_invalid_csp | report-to group
// │ └─grandchild_invalid_csp2 | script-src 'none'; invalid-directive
// └─sibling | (none)
//
// Test that the required CSP of every frame is computed/inherited correctly
// and that the Sec-Required-CSP header is set.
auto test = [](TestRenderFrameHost* frame, std::string csp_attr,
std::string expect_csp) {
SCOPED_TRACE(frame->GetFrameName());
if (!csp_attr.empty())
frame->frame_tree_node()->set_csp_attribute(ParsePolicy(csp_attr));
std::unique_ptr<NavigationSimulator> simulator =
content::NavigationSimulator::CreateRendererInitiated(
// Chrome blocks a frame navigating to a URL if more than one of its
// ancestors have the same URL. Use a different URL every time, to
// avoid blocking navigation of the grandchild frame.
GURL("https://www.example.com/" + frame->GetFrameName()), frame);
simulator->Start();
NavigationRequest* request =
NavigationRequest::From(simulator->GetNavigationHandle());
std::string header_value;
bool found = request->GetRequestHeaders().GetHeader("sec-required-csp",
&header_value);
if (!expect_csp.empty()) {
EXPECT_TRUE(found);
EXPECT_EQ(expect_csp, header_value);
} else {
EXPECT_FALSE(found);
}
// Complete the navigation so that the required csp is stored in the
// RenderFrameHost, so that when we will add children to this frame they
// will be able to get the parent's required csp (and hence also test that
// the whole logic works).
auto response_headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
response_headers->SetHeader("Allow-CSP-From", "*");
simulator->SetResponseHeaders(response_headers);
simulator->Commit();
};
auto* main_frame = static_cast<TestRenderFrameHost*>(main_rfh());
test(main_frame, "", "");
auto* child_with_csp = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(main_frame)
->AppendChild("child_with_csp"));
test(child_with_csp, "script-src 'none'", "script-src 'none'");
auto* grandchild_same_csp = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(child_with_csp)
->AppendChild("grandchild_same_csp"));
test(grandchild_same_csp, "script-src 'none'", "script-src 'none'");
auto* grandchild_no_csp = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(child_with_csp)
->AppendChild("grandchild_no_csp"));
test(grandchild_no_csp, "", "script-src 'none'");
auto* grandgrandchild = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(grandchild_no_csp)
->AppendChild("grandgrandchild"));
test(grandgrandchild, "", "script-src 'none'");
auto* grandchild_invalid_csp = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(child_with_csp)
->AppendChild("grandchild_invalid_csp"));
test(grandchild_invalid_csp, "report-to group", "script-src 'none'");
auto* grandchild_invalid_csp2 = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(child_with_csp)
->AppendChild("grandchild_invalid_csp2"));
test(grandchild_invalid_csp2, "script-src 'none'; invalid-directive",
"script-src 'none'");
auto* sibling = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(main_frame)->AppendChild("sibling"));
test(sibling, "", "");
}
TEST_F(AncestorThrottleNavigationTest, EvaluateCSPEmbeddedEnforcement) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(network::features::kOutOfBlinkCSPEE);
// We need one initial navigation to set up everything.
NavigateAndCommit(GURL("https://www.example.org"));
auto* main_frame = static_cast<TestRenderFrameHost*>(main_rfh());
struct TestCase {
const char* name;
const char* required_csp;
const char* frame_url;
const char* allow_csp_from;
const char* returned_csp;
bool expect_allow;
} cases[] = {
{
"No required csp",
nullptr,
"https://www.not-example.org",
nullptr,
nullptr,
true,
},
{
"Required csp - Same origin",
"script-src 'none'",
"https://www.example.org",
nullptr,
nullptr,
true,
},
{
"Required csp - Cross origin",
"script-src 'none'",
"https://www.not-example.org",
nullptr,
nullptr,
false,
},
{
"Required csp - Cross origin with Allow-CSP-From",
"script-src 'none'",
"https://www.not-example.org",
"*",
nullptr,
true,
},
{
"Required csp - Cross origin with wrong Allow-CSP-From",
"script-src 'none'",
"https://www.not-example.org",
"https://www.another-example.org",
nullptr,
false,
},
{
"Required csp - Cross origin with non-subsuming CSPs",
"script-src 'none'",
"https://www.not-example.org",
nullptr,
"style-src 'none'",
false,
},
{
"Required csp - Cross origin with subsuming CSPs",
"script-src 'none'",
"https://www.not-example.org",
nullptr,
"script-src 'none'",
true,
},
{
"Required csp - Cross origin with wrong Allow-CSP-From but subsuming "
"CSPs",
"script-src 'none'",
"https://www.not-example.org",
"https://www.another-example.org",
"script-src 'none'",
true,
},
};
for (auto test : cases) {
SCOPED_TRACE(test.name);
auto* frame = static_cast<TestRenderFrameHost*>(
content::RenderFrameHostTester::For(main_frame)
->AppendChild(test.name));
if (test.required_csp) {
frame->frame_tree_node()->set_csp_attribute(
ParsePolicy(test.required_csp));
}
std::unique_ptr<NavigationSimulator> simulator =
content::NavigationSimulator::CreateRendererInitiated(
GURL(test.frame_url), frame);
auto response_headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
if (test.allow_csp_from)
response_headers->SetHeader("Allow-CSP-From", test.allow_csp_from);
if (test.returned_csp)
response_headers->SetHeader("Content-Security-Policy", test.returned_csp);
simulator->SetResponseHeaders(response_headers);
simulator->ReadyToCommit();
if (test.expect_allow) {
EXPECT_EQ(NavigationThrottle::PROCEED,
simulator->GetLastThrottleCheckResult());
} else {
EXPECT_EQ(NavigationThrottle::BLOCK_RESPONSE,
simulator->GetLastThrottleCheckResult());
}
}
}
} // namespace content