blob: ba7265dbf5deabfa9ac6e13cb2ea66e13ca76a37 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// 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/policy_container_host.h"
#include "base/command_line.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/network/public/mojom/referrer_policy.mojom-shared.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
using ::testing::ByRef;
using ::testing::Eq;
using ::testing::IsNull;
using ::testing::Pointee;
namespace {
class PolicyContainerHostBrowserTest : public content::ContentBrowserTest {
protected:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
RenderFrameHostImpl* current_frame_host() {
return web_contents()->GetPrimaryMainFrame();
}
};
} // namespace
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
ReferrerPolicyFromHeader) {
using ReferrerPolicy = network::mojom::ReferrerPolicy;
const struct {
const char* headers;
ReferrerPolicy expected_referrer;
} kTestCases[] = {
{"", ReferrerPolicy::kDefault},
{"Referrer-Policy: no-referrer", ReferrerPolicy::kNever},
{"Referrer-Policy: no-referrer-when-downgrade",
ReferrerPolicy::kNoReferrerWhenDowngrade},
{"Referrer-Policy: origin", ReferrerPolicy::kOrigin},
{"Referrer-Policy: origin-when-cross-origin",
ReferrerPolicy::kOriginWhenCrossOrigin},
{"Referrer-Policy: same-origin", ReferrerPolicy::kSameOrigin},
{"Referrer-Policy: strict-origin", ReferrerPolicy::kStrictOrigin},
{"Referrer-Policy: strict-origin-when-cross-origin",
ReferrerPolicy::kStrictOriginWhenCrossOrigin},
{"Referrer-Policy: unsafe-url", ReferrerPolicy::kAlways},
};
for (const auto& test_case : kTestCases) {
GURL url = embedded_test_server()->GetURL(
"a.com", "/set-header?" + std::string(test_case.headers));
ASSERT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(test_case.expected_referrer,
current_frame_host()->policy_container_host()->referrer_policy());
}
}
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
ReferrerPolicyMetaUpdates) {
GURL page = embedded_test_server()->GetURL("a.com", "/empty.html");
ASSERT_TRUE(NavigateToURL(shell(), page));
EXPECT_EQ(network::mojom::ReferrerPolicy::kDefault,
current_frame_host()->policy_container_host()->referrer_policy());
ASSERT_TRUE(ExecJs(current_frame_host(),
"var meta = document.createElement('meta');"
"meta.name = 'referrer';"
"meta.content = 'no-referrer';"
"document.head.appendChild(meta);"));
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
current_frame_host()->policy_container_host()->referrer_policy());
}
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest, CopiedFromPopupOpener) {
GURL policies_a_url = embedded_test_server()->GetURL(
"a.com", "/set-header?Referrer-Policy: no-referrer");
GURL policies_b_url = embedded_test_server()->GetURL(
"a.com",
"/set-header?"
"Referrer-Policy: origin&"
"Content-Security-Policy: img-src 'self'; style-src 'self'");
ASSERT_TRUE(NavigateToURL(shell(), policies_b_url));
PolicyContainerPolicies main_document_policies =
current_frame_host()->policy_container_host()->policies().Clone();
{
// Open a popup. It stays on the initial empty document. The
// PolicyContainerHost's policies must have been inherited from the opener.
ShellAddedObserver shell_observer;
ASSERT_TRUE(ExecJs(current_frame_host(), "window.open();"));
WebContentsImpl* popup_webcontents = static_cast<WebContentsImpl*>(
shell_observer.GetShell()->web_contents());
RenderFrameHostImpl* popup_frame =
popup_webcontents->GetPrimaryFrameTree().root()->current_frame_host();
EXPECT_EQ(network::mojom::ReferrerPolicy::kOrigin,
popup_frame->policy_container_host()->referrer_policy());
EXPECT_EQ(main_document_policies,
popup_frame->policy_container_host()->policies());
}
{
// Open a popup that navigates to another document, the referrer policy
// of the current frame (referrer-policy: origin) should be applied, which
// induces on the following document the |document.referrer| value of
// |origin_referrer_page.GetWithEmptyPath()| as tested below.
// The document loaded in the popup should have
// referrer-policy: no-referrer and no CSPs from the response headers.
ShellAddedObserver shell_observer;
ASSERT_TRUE(ExecJs(current_frame_host(),
JsReplace("window.open($1);", policies_a_url)));
WebContentsImpl* popup_webcontents = static_cast<WebContentsImpl*>(
shell_observer.GetShell()->web_contents());
WaitForLoadStop(popup_webcontents);
RenderFrameHostImpl* popup_frame =
popup_webcontents->GetPrimaryFrameTree().root()->current_frame_host();
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
popup_frame->policy_container_host()->referrer_policy());
EXPECT_EQ(0u, popup_frame->policy_container_host()
->policies()
.content_security_policies.size());
EXPECT_EQ(policies_b_url.GetWithEmptyPath(),
EvalJs(popup_frame, "document.referrer;"));
}
}
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest, CopiedFromParent) {
GURL policies_a_url = embedded_test_server()->GetURL(
"a.com", "/set-header?Referrer-Policy: no-referrer");
GURL policies_b_url = embedded_test_server()->GetURL(
"a.com",
"/set-header?"
"Referrer-Policy: origin&"
"Content-Security-Policy: img-src 'self'; style-src 'self'");
ASSERT_TRUE(NavigateToURL(shell(), policies_b_url));
PolicyContainerPolicies main_document_policies =
current_frame_host()->policy_container_host()->policies().Clone();
std::string create_srcdoc_iframe_script(
"var iframe = document.createElement('iframe');"
"iframe.srcdoc = '<p>hello world!</p>';"
"document.body.appendChild(iframe);");
{
// Add an iframe and verify its policies.
ASSERT_TRUE(ExecJs(current_frame_host(), create_srcdoc_iframe_script));
ASSERT_EQ(1U, current_frame_host()->child_count());
WaitForLoadStop(web_contents());
FrameTreeNode* iframe_node =
current_frame_host()->child_at(current_frame_host()->child_count() - 1);
EXPECT_TRUE(iframe_node->current_url().IsAboutSrcdoc());
EXPECT_EQ(network::mojom::ReferrerPolicy::kOrigin,
iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy());
EXPECT_EQ(
main_document_policies,
iframe_node->current_frame_host()->policy_container_host()->policies());
// Navigate the document and verify the policies are updated and the
// initiator referrer policy (referrer-policy: origin) is used which induces
// on the following document the |document.referrer| value of
// |origin_referrer_page.GetWithEmptyPath()| as tested below.
ASSERT_TRUE(
ExecJs(iframe_node->current_frame_host(),
JsReplace("document.location.href = $1", policies_a_url)));
WaitForLoadStop(web_contents());
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy());
EXPECT_EQ(0u, iframe_node->current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
EXPECT_EQ(policies_b_url.GetWithEmptyPath(),
EvalJs(iframe_node->current_frame_host(), "document.referrer;"));
}
// Taint the RFH Policy container with a value that will not be the same in
// the renderer (kOrigin). This is not possible in a normal situation, but
// could occur if the renderer was compromised.
// This will enable the following test, which verifies that the copied policy
// comes from the browser.
static_cast<blink::mojom::PolicyContainerHost*>(
current_frame_host()->policy_container_host())
->SetReferrerPolicy(network::mojom::ReferrerPolicy::kSameOrigin);
// Repeat the previous test with the tainted policy container:
{
// Add an iframe and verify its Policy Container value.
ASSERT_TRUE(ExecJs(current_frame_host(), create_srcdoc_iframe_script));
// Check that the iframe was properly added.
ASSERT_EQ(2U, current_frame_host()->child_count());
WaitForLoadStop(web_contents());
FrameTreeNode* iframe_node =
current_frame_host()->child_at(current_frame_host()->child_count() - 1);
EXPECT_TRUE(iframe_node->current_url().IsAboutSrcdoc());
EXPECT_EQ(network::mojom::ReferrerPolicy::kSameOrigin,
iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy());
ASSERT_TRUE(
ExecJs(iframe_node->current_frame_host(),
JsReplace("document.location.href = $1", policies_a_url)));
WaitForLoadStop(web_contents());
// The referrer policy is the one of the newly loaded document:
// no_referrer_page.
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy());
// The referrer is determined using the policy container inherited when the
// frame was created and navigated to the srcdoc. The tainted value, within
// the browser process, was inherited during the navigation, and is expected
// to be referrer-policy: same-origin, leading to the full url being used as
// a referrer.
EXPECT_EQ(policies_b_url,
EvalJs(iframe_node->current_frame_host(), "document.referrer;"));
}
}
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
CopiedFromParentCreatedBySibling) {
GURL origin_referrer_page = embedded_test_server()->GetURL(
"a.com",
"/set-header?"
"Referrer-Policy: origin&"
"Content-Security-Policy: style-src-attr 'none'");
GURL same_origin_referrer_page = embedded_test_server()->GetURL(
"a.com", "/set-header?Referrer-Policy: same-origin");
GURL no_referrer_page = embedded_test_server()->GetURL(
"a.com", "/set-header?Referrer-Policy: no-referrer");
ASSERT_TRUE(NavigateToURL(shell(), origin_referrer_page));
PolicyContainerPolicies main_document_policies =
current_frame_host()->policy_container_host()->policies().Clone();
{
// Add an iframe and verify its referrer policy.
ASSERT_TRUE(
ExecJs(current_frame_host(),
JsReplace("var iframe = document.createElement('iframe');"
"iframe.src = $1;"
"iframe.name = 'first';"
"document.body.appendChild(iframe);",
same_origin_referrer_page)));
ASSERT_EQ(1U, current_frame_host()->child_count());
EXPECT_TRUE(WaitForLoadStop(web_contents()));
FrameTreeNode* first_iframe_node =
current_frame_host()->child_at(current_frame_host()->child_count() - 1);
EXPECT_EQ(network::mojom::ReferrerPolicy::kSameOrigin,
first_iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy());
// From the iframe, create a sibling.
ASSERT_TRUE(ExecJs(first_iframe_node->current_frame_host(),
"var iframe = document.createElement('iframe');"
"iframe.srcdoc = 'hello world';"
"iframe.name = 'second';"
"parent.document.body.appendChild(iframe);"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
ASSERT_EQ(2U, current_frame_host()->child_count());
FrameTreeNode* second_iframe_node =
current_frame_host()->child_at(current_frame_host()->child_count() - 1);
// The policies should be inherited from the creator of the iframe, which is
// the node to which the iframe is attached to, i.e. the parent, here the
// main document, which has referrer-policy: origin.
EXPECT_EQ(network::mojom::ReferrerPolicy::kOrigin,
second_iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy())
<< "Sibling policy container inherited from parent.";
EXPECT_EQ(main_document_policies, second_iframe_node->current_frame_host()
->policy_container_host()
->policies());
// The second iframe navigates its sibling, the first iframe, the inherited
// referrer policy of second (from the main frame) is applied.
// The document loaded in first is a local scheme, and as such inherits from
// the initiator, the second iframe.
ASSERT_TRUE(ExecJs(second_iframe_node->current_frame_host(),
"window.open('about:blank', 'first');"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(network::mojom::ReferrerPolicy::kOrigin,
first_iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy());
EXPECT_EQ(main_document_policies, first_iframe_node->current_frame_host()
->policy_container_host()
->policies());
EXPECT_EQ(
origin_referrer_page.GetWithEmptyPath(),
EvalJs(first_iframe_node->current_frame_host(), "document.referrer;"))
<< "Referrer obtained from applying same-origin referrer policy.";
// Navigate the second iframe, verify the policies are updated from
// the response. The document.referrer comes from the application of the
// inherited referrer policy of the main frame
// (referrer-policy: same-origin).
ASSERT_TRUE(
ExecJs(second_iframe_node->current_frame_host(),
JsReplace("document.location.href = $1", no_referrer_page)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
second_iframe_node->current_frame_host()
->policy_container_host()
->referrer_policy());
EXPECT_EQ(0u, second_iframe_node->current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
EXPECT_EQ(
origin_referrer_page.GetWithEmptyPath(),
EvalJs(second_iframe_node->current_frame_host(), "document.referrer;"))
<< "Referrer obtained from applying same-origin referrer policy.";
}
}
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest, HistoryForMainFrame) {
NavigationControllerImpl& controller = web_contents()->GetController();
GURL policies_a_url(embedded_test_server()->GetURL(
"/set-header?"
"Referrer-Policy: no-referrer&"
"Content-Security-Policy: img-src 'none'"));
GURL policies_b_url(embedded_test_server()->GetURL(
"/set-header?"
"Referrer-Policy: unsafe-url&"
"Content-Security-Policy: style-src 'none'"));
// Navigate to a page setting policies_a.
ASSERT_TRUE(NavigateToURL(shell(), policies_a_url));
ASSERT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
current_frame_host()->policy_container_host()->referrer_policy());
PolicyContainerPolicies policies_a =
current_frame_host()->policy_container_host()->policies().Clone();
// Now navigate to a local scheme.
ASSERT_TRUE(ExecJs(current_frame_host(), "window.location = 'about:blank'"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
ASSERT_EQ(policies_a,
current_frame_host()->policy_container_host()->policies());
ASSERT_EQ(2, controller.GetEntryCount());
NavigationEntryImpl* entry2 = controller.GetEntryAtIndex(1);
// Check that RendererDidNavigateToNewEntry stored the correct policy
// container in the FrameNavigationEntry.
EXPECT_THAT(entry2->root_node()->frame_entry->policy_container_policies(),
Pointee(Eq(ByRef(policies_a))));
// Same document navigation.
ASSERT_TRUE(ExecJs(current_frame_host(), "window.location.href = '#top'"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(policies_a,
current_frame_host()->policy_container_host()->policies());
ASSERT_EQ(3, controller.GetEntryCount());
NavigationEntryImpl* entry3 = controller.GetEntryAtIndex(2);
EXPECT_THAT(entry3->root_node()->frame_entry->policy_container_policies(),
Pointee(Eq(ByRef(policies_a))));
// Navigate to a third page.
ASSERT_TRUE(NavigateToURL(shell(), policies_b_url));
ASSERT_EQ(4, controller.GetEntryCount());
ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways,
current_frame_host()->policy_container_host()->referrer_policy());
// Go back to "about:blank#top"
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The correct policies should be restored from history.
EXPECT_EQ(policies_a,
current_frame_host()->policy_container_host()->policies());
// The function RendererDidNavigateToExistingEntry should not have changed
// anything.
EXPECT_THAT(entry3->root_node()->frame_entry->policy_container_policies(),
Pointee(Eq(ByRef(policies_a))));
// Go back to "about:blank".
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The correct policies should be restored from history.
EXPECT_EQ(policies_a,
current_frame_host()->policy_container_host()->policies());
// The function RendererDidNavigateToExistingEntry should not have changed
// anything.
EXPECT_THAT(entry2->root_node()->frame_entry->policy_container_policies(),
Pointee(Eq(ByRef(policies_a))));
// Same URL navigation, which gets converted to a reload.
ASSERT_TRUE(NavigateFrameToURL(current_frame_host()->frame_tree_node(),
GURL("about:blank")));
EXPECT_EQ(policies_a,
current_frame_host()->policy_container_host()->policies());
ASSERT_EQ(4, controller.GetEntryCount());
// Check that after RendererDidNavigateToExistingEntry the policy container in
// the FrameNavigationEntry is still correct.
EXPECT_THAT(entry2->root_node()->frame_entry->policy_container_policies(),
Pointee(Eq(ByRef(policies_a))));
}
namespace {
bool EqualsExceptCOOPAndTopNavigation(const PolicyContainerPolicies& lhs,
const PolicyContainerPolicies& rhs) {
PolicyContainerPolicies rhs_modulo_coop = rhs.Clone();
rhs_modulo_coop.cross_origin_opener_policy = lhs.cross_origin_opener_policy;
rhs_modulo_coop.can_navigate_top_without_user_gesture =
lhs.can_navigate_top_without_user_gesture;
return lhs == rhs_modulo_coop;
}
} // namespace
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest, HistoryForChildFrame) {
NavigationControllerImpl& controller = web_contents()->GetController();
GURL policies_a_url(embedded_test_server()->GetURL(
"/set-header?"
"Referrer-Policy: unsafe-url&"
"Content-Security-Policy: style-src 'none'"));
GURL strict_origin_when_cross_origin_referrer_url(
embedded_test_server()->GetURL(
"/set-header?Referrer-Policy: strict-origin-when-cross-origin"));
GURL main_url(embedded_test_server()->GetURL("/page_with_blank_iframe.html"));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
ASSERT_TRUE(NavigateToURL(shell(), main_url));
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
ASSERT_NE(nullptr, child);
ASSERT_EQ(1, controller.GetEntryCount());
// The child has default policies (same as the parent).
EXPECT_EQ(
network::mojom::ReferrerPolicy::kDefault,
child->current_frame_host()->policy_container_host()->referrer_policy());
EXPECT_EQ(0u, child->current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
NavigationEntryImpl* entry1 = controller.GetEntryAtIndex(0);
EXPECT_THAT(
entry1->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
// Change policies of the main frame.
ASSERT_TRUE(ExecJs(current_frame_host(),
"var meta = document.createElement('meta');"
"meta.name = 'referrer';"
"meta.content = 'same-origin';"
"document.head.appendChild(meta);"
"var meta2 = document.createElement('meta');"
"meta2.httpEquiv = 'content-security-policy';"
"meta2.content = 'img-src';"
"document.head.appendChild(meta2);"));
EXPECT_EQ(network::mojom::ReferrerPolicy::kSameOrigin,
current_frame_host()->policy_container_host()->referrer_policy());
EXPECT_EQ(1u, current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
PolicyContainerPolicies main_frame_new_policies =
current_frame_host()->policy_container_host()->policies().Clone();
// 1) Navigate the child frame to a local scheme url.
ASSERT_TRUE(ExecJs(current_frame_host(),
"window.open('data:text/html,Hello', 'test_iframe');"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The new document inherits from the navigation initiator, except for COOP.
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(
main_frame_new_policies,
child->current_frame_host()->policy_container_host()->policies()));
// The new page replaces the initial about:blank page in the subframe, so no
// new navigation entry is created.
ASSERT_EQ(1, controller.GetEntryCount());
// The policy container of the FrameNavigationEntry should have been
// updated. Test that the function RendererDidNavigateAutoSubframe updates the
// FrameNavigationEntry properly.
EXPECT_THAT(
entry1->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
// 2) Same document navigation.
ASSERT_TRUE(
ExecJs(child->current_frame_host(), "window.location.href = '#top';"));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
// The policies have not changed.
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(
main_frame_new_policies,
child->current_frame_host()->policy_container_host()->policies()));
ASSERT_EQ(2, controller.GetEntryCount());
NavigationEntryImpl* entry2 = controller.GetEntryAtIndex(1);
EXPECT_THAT(
entry1->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
// 3) Navigate the child frame to a network scheme url.
ASSERT_TRUE(NavigateFrameToURL(child, policies_a_url));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
ASSERT_EQ(3, controller.GetEntryCount());
EXPECT_EQ(
network::mojom::ReferrerPolicy::kAlways,
child->current_frame_host()->policy_container_host()->referrer_policy());
PolicyContainerPolicies policies_a =
child->current_frame_host()->policy_container_host()->policies().Clone();
// 4) Navigate the child frame to another local scheme url.
ASSERT_TRUE(ExecJs(child->current_frame_host(),
"window.location = 'data:text/html,Hello2';"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The new document inherits from the navigation initiator.
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(
policies_a,
child->current_frame_host()->policy_container_host()->policies()));
// Now test that the function RendererDidNavigateNewSubframe properly stored
// the policy container in the FrameNavigationEntry.
ASSERT_EQ(4, controller.GetEntryCount());
NavigationEntryImpl* entry4 = controller.GetEntryAtIndex(3);
EXPECT_THAT(
entry4->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
// 5) Navigate the child frame to another network scheme url.
ASSERT_TRUE(
NavigateFrameToURL(child, strict_origin_when_cross_origin_referrer_url));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
ASSERT_EQ(5, controller.GetEntryCount());
EXPECT_EQ(
network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin,
child->current_frame_host()->policy_container_host()->referrer_policy());
EXPECT_EQ(0u, child->current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
// 6) Navigate the main frame cross-document to destroy the subframes.
GURL foo_url(embedded_test_server()->GetURL(
"foo.com", "/navigation_controller/simple_page_1.html"));
ASSERT_TRUE(NavigateToURL(shell(), foo_url));
// 7) Navigate all the way back and check that we properly reload the policy
// container from history.
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
child = web_contents()->GetPrimaryFrameTree().root()->child_at(0);
ASSERT_NE(nullptr, child);
// The correct referrer policy should be restored from history.
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(
policies_a,
child->current_frame_host()->policy_container_host()->policies()));
// The frame entry should not have changed.
EXPECT_THAT(
entry4->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The correct referrer policy should be restored from history.
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(
main_frame_new_policies,
child->current_frame_host()->policy_container_host()->policies()));
// The frame entry should not have changed.
EXPECT_THAT(
entry2->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The correct referrer policy should be restored from history.
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(
main_frame_new_policies,
child->current_frame_host()->policy_container_host()->policies()));
// The frame entry should not have changed.
EXPECT_THAT(
entry1->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
}
// Test for https://crbug.com/364773822.
IN_PROC_BROWSER_TEST_F(
PolicyContainerHostBrowserTest,
PoliciesAreNotReloadedFromHistoryIfNavigationEncounteredError) {
NavigationControllerImpl& controller = web_contents()->GetController();
GURL main_url(embedded_test_server()->GetURL("/page_with_blank_iframe.html"));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
ASSERT_TRUE(NavigateToURL(shell(), main_url));
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
ASSERT_NE(nullptr, child);
GURL child_frame_url(
embedded_test_server()->GetURL("/page_with_srcdoc_iframe_and_csp.html"));
ASSERT_TRUE(NavigateFrameToURL(child, child_frame_url));
ASSERT_EQ(1, controller.GetEntryCount());
PolicyContainerPolicies child_frame_policies =
child->current_frame_host()->policy_container_host()->policies().Clone();
EXPECT_EQ(1u, child_frame_policies.content_security_policies.size());
NavigationEntryImpl* entry1 = controller.GetEntryAtIndex(0);
EXPECT_THAT(
entry1->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(
child->current_frame_host()->policy_container_host()->policies()))));
// The srcdoc document inherits from the parent, except for COOP.
ASSERT_EQ(1U, child->child_count());
FrameTreeNode* grandchild = child->child_at(0);
ASSERT_NE(nullptr, grandchild);
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(
child_frame_policies,
grandchild->current_frame_host()->policy_container_host()->policies()));
ASSERT_EQ(1, controller.GetEntryCount());
GURL about_blank("about:blank#1");
ASSERT_TRUE(NavigateFrameToURL(grandchild, about_blank));
// Add a sandbox to the child iframe and navigate away and back to recompute
// the sandbox flag, so that it gets reloaded with an opaque origin.
ASSERT_TRUE(ExecJs(current_frame_host(), R"(
document.getElementById('test_iframe').sandbox = 'allow-scripts';
)"));
ASSERT_TRUE(NavigateFrameToURL(child, about_blank));
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(child->current_origin().opaque());
// Trying to navigate the grandchild back to about:srcdoc is disallowed by
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/navigation_request.cc;l=6957-6967;drc=580a3da6e0ea94caa1f127e4455fbdbd05625065.
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_TRUE(child->child_at(0)->current_frame_host()->IsErrorDocument());
// The error page should be loaded without inheriting the parent policies.
EXPECT_FALSE(EqualsExceptCOOPAndTopNavigation(child_frame_policies,
child->child_at(0)
->current_frame_host()
->policy_container_host()
->policies()));
// The policy container of the FrameNavigationEntry should have been cleared
// so that the error page's policies are not used on later successful loads.
EXPECT_THAT(
entry1->GetFrameEntry(child->child_at(0))->policy_container_policies(),
IsNull());
controller.GoForward();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
controller.GoForward();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// Remove the sandbox flag so that the srcdoc frame can successfully load
// again.
ASSERT_TRUE(ExecJs(current_frame_host(), R"(
document.getElementById('test_iframe').removeAttribute('sandbox');
)"));
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The srcdoc document inherits again from the parent, except for COOP.
EXPECT_TRUE(EqualsExceptCOOPAndTopNavigation(child_frame_policies,
child->child_at(0)
->current_frame_host()
->policy_container_host()
->policies()));
}
// Check that the FrameNavigationEntry for the initial empty document is
// correctly populated, both for main frames and for subframes.
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
HistoryForInitialEmptyDocument) {
GURL policies_a_url =
embedded_test_server()->GetURL("a.com",
"/set-header?"
"Referrer-Policy: origin&"
"Content-Security-Policy: img-src 'none'");
ASSERT_TRUE(NavigateToURL(shell(), policies_a_url));
PolicyContainerPolicies policies_a =
current_frame_host()->policy_container_host()->policies().Clone();
{
// Open a subframe
ASSERT_TRUE(ExecJs(current_frame_host(),
"var frame = document.createElement('iframe');"
"frame.name = 'test_iframe';"
"document.body.appendChild(frame);"));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
ASSERT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
ASSERT_NE(nullptr, child);
// The child inherits from the parent.
EXPECT_EQ(policies_a,
child->current_frame_host()->policy_container_host()->policies());
// The right policy is stored in the FrameNavigationEntry.
NavigationControllerImpl& controller = web_contents()->GetController();
ASSERT_EQ(1, controller.GetEntryCount());
NavigationEntryImpl* entry1 = controller.GetEntryAtIndex(0);
EXPECT_THAT(entry1->GetFrameEntry(child)->policy_container_policies(),
Pointee(Eq(ByRef(policies_a))));
}
{
// Open a popup.
ShellAddedObserver shell_observer;
ASSERT_TRUE(ExecJs(current_frame_host(), "window.open('about:blank');"));
WebContentsImpl* popup_webcontents = static_cast<WebContentsImpl*>(
shell_observer.GetShell()->web_contents());
RenderFrameHostImpl* popup_frame =
popup_webcontents->GetPrimaryFrameTree().root()->current_frame_host();
// The popup inherits from the creator.
EXPECT_EQ(policies_a, popup_frame->policy_container_host()->policies());
// The right policy is stored in the FrameNavigationEntry.
NavigationEntryImpl* entry1 =
popup_webcontents->GetController().GetEntryAtIndex(0);
EXPECT_THAT(entry1->root_node()->frame_entry->policy_container_policies(),
Pointee(Eq(ByRef(policies_a))));
}
}
// This test ensures that the document policies what we store in the
// FrameNavigationEntry are a snapshot of the document policies at
// CommitNavigation time, and do not include updates triggered by Blink.
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
BlinkModificationsDoNotAffectPolicyContainer) {
NavigationControllerImpl& controller = web_contents()->GetController();
GURL always_referrer_url(embedded_test_server()->GetURL(
"/set-header?Referrer-Policy: unsafe-url"));
ASSERT_TRUE(NavigateToURL(shell(), always_referrer_url));
FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root();
// Create child frame
ASSERT_TRUE(ExecJs(current_frame_host(), R"(
let frame = document.createElement('iframe');
let content = '<head><meta name="referrer" content="no-referrer"></head>';
frame.src = 'data:text/html,' + content;
document.body.appendChild(frame);
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
FrameTreeNode* child = root->child_at(0);
ASSERT_NE(nullptr, child);
// Blink should have parsed Referrer-Policy from the meta tag and the
// information should have propagated to the PolicyContainerHost.
ASSERT_EQ(
network::mojom::ReferrerPolicy::kNever,
child->current_frame_host()->policy_container_host()->referrer_policy());
ASSERT_EQ(1, controller.GetEntryCount());
NavigationEntryImpl* entry1 = controller.GetEntryAtIndex(0);
// Policies should be stored in the FrameNavigationEntry before any
// modification coming from blink.
ASSERT_EQ(network::mojom::ReferrerPolicy::kAlways,
entry1->GetFrameEntry(child)
->policy_container_policies()
->referrer_policy);
}
// This is a regression test. A document navigates a remote subframe away from
// about:blank. The new FrameNavigationEntry used to wrongly inherit a
// policies from the about:blank document.
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
PoliciesNotInheritedForRemoteNonLocalScheme) {
NavigationControllerImpl& controller = web_contents()->GetController();
// Create a main frame with referrer policy 'unsafe-url'.
GURL always_referrer_url(embedded_test_server()->GetURL(
"/set-header?Referrer-Policy: unsafe-url"));
ASSERT_TRUE(NavigateToURL(shell(), always_referrer_url));
// Create a cross-origin child frame with referrer policy 'no-referrer'.
GURL cross_origin_no_referrer_url(embedded_test_server()->GetURL(
"b.com", "/set-header?Referrer-Policy: no-referrer"));
EXPECT_TRUE(
ExecJs(current_frame_host(), JsReplace(R"(
child_frame = document.createElement('iframe');
child_frame.src = $1;
document.body.appendChild(child_frame);
)",
cross_origin_no_referrer_url)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
FrameTreeNode* child = current_frame_host()->child_at(0);
EXPECT_NE(nullptr, child);
// Navigate the child frame to "about:blank", but keep it in a remote frame
// w.r.t. the main frame.
EXPECT_TRUE(ExecJs(child->current_frame_host(), R"(
location.href = "about:blank";
)"));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(
network::mojom::ReferrerPolicy::kNever,
child->current_frame_host()->policy_container_host()->referrer_policy());
EXPECT_EQ(2, controller.GetEntryCount());
NavigationEntryImpl* entry1 = controller.GetEntryAtIndex(1);
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
entry1->GetFrameEntry(child)
->policy_container_policies()
->referrer_policy);
// Now navigate the child from the main frame to another empty url.
GURL empty_url(embedded_test_server()->GetURL("c.com", "/empty.html"));
EXPECT_TRUE(ExecJs(current_frame_host(), JsReplace(R"(
child_frame.src = $1;
)",
empty_url)));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
// The child should have default referrer policy.
EXPECT_EQ(
network::mojom::ReferrerPolicy::kDefault,
child->current_frame_host()->policy_container_host()->referrer_policy());
}
// The following tests shows the behavior of the policy container during the
// early commit following a crashed frame: If an embedder pauses the navigation
// that causing the early commit, they can then execute javascript in the
// committed frame and blink's PolicyContainer would not be connected to
// browser's PolicyContainerHost. This has limited impact in practice.
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
CheckRendererPolicyContainerAccessesAfterCrash) {
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
std::string add_referrer_script = R"(
new Promise(async resolve => {
// Make sure the DOM is ready before modifying <header>.
if (document.readyState !== "complete")
await new Promise(r => addEventListener("DOMContentLoaded", r));
// Add <meta name="referrer" content=$1>
let meta = document.createElement("meta");
meta.name = "referrer";
meta.content= $1;
document.head.append(meta);
resolve(true);
})
)";
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// The referrer policy is initially default.
EXPECT_EQ(network::mojom::ReferrerPolicy::kDefault,
current_frame_host()->policy_container_host()->referrer_policy());
// Deliver a new referrer policy in the renderer with
// <meta name="referrer" content="none">.
EXPECT_EQ(true, EvalJs(current_frame_host(),
content::JsReplace(add_referrer_script, "none")));
// The policy is propagated to the browser by policy container.
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
current_frame_host()->policy_container_host()->referrer_policy());
// Crash the renderer.
RenderProcessHost* renderer_process = current_frame_host()->GetProcess();
RenderProcessHostWatcher crash_observer(
renderer_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
renderer_process->Shutdown(0);
crash_observer.Wait();
// Start navigation to B, but don't commit yet.
TestNavigationManager manager(web_contents(), url_b);
shell()->LoadURL(url_b);
EXPECT_TRUE(manager.WaitForRequestStart());
NavigationRequest* navigation_request =
NavigationRequest::From(manager.GetNavigationHandle());
if (ShouldSkipEarlyCommitPendingForCrashedFrame()) {
EXPECT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE);
} else {
// Policy container is properly initialized in the early committed
// RenderFrameHost.
EXPECT_TRUE(current_frame_host()->policy_container_host());
EXPECT_EQ(navigation_request->GetAssociatedRFHType(),
NavigationRequest::AssociatedRenderFrameHostType::CURRENT);
// The policy is copied from the previous RFH following the crash.
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
current_frame_host()->policy_container_host()->referrer_policy());
// Deliver a new referrer policy in the renderer with
// <meta name="referrer" content="origin">. Using "origin" to differ from
// previous "none" and "default".
EXPECT_EQ(true, EvalJs(current_frame_host(),
content::JsReplace(add_referrer_script, "origin")));
// The previous referrer policy is not propagated to the browser since
// blink's policy container is not linked with browser's policy container
// host. Never (a.k.a. "None") is copied from the previous RFH.
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
current_frame_host()->policy_container_host()->referrer_policy());
}
// Let the navigation finish.
ASSERT_TRUE(manager.WaitForNavigationFinished());
EXPECT_EQ(url_b,
web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
// The referrer policy is initialized to default during the navigation (no
// referrer-policy header in the response).
EXPECT_EQ(network::mojom::ReferrerPolicy::kDefault,
current_frame_host()->policy_container_host()->referrer_policy());
// Deliver a new referrer policy in the renderer with
// <meta name="referrer" content="strict-origin">. Using "strict-origin" to
// differ again from the previous uses.
EXPECT_EQ(true,
EvalJs(current_frame_host(),
content::JsReplace(add_referrer_script, "strict-origin")));
// This time renderer's policy container properly propagates the referrer
// policy to the browser.
EXPECT_EQ(network::mojom::ReferrerPolicy::kStrictOrigin,
current_frame_host()->policy_container_host()->referrer_policy());
}
// Test that chrome error pages are loaded with a default policy container and
// don't inherit policies.
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest, FailedNavigation) {
// Perform a navigation setting referrer policy.
GURL url = embedded_test_server()->GetURL(
"/set-header?Referrer-Policy: no-referrer");
EXPECT_TRUE(NavigateToURL(shell(), url));
EXPECT_EQ(network::mojom::ReferrerPolicy::kNever,
current_frame_host()->policy_container_host()->referrer_policy());
// Now open a popup with an unreachable url.
GURL error_url = embedded_test_server()->GetURL("/close-socket");
ShellAddedObserver shell_observer;
EXPECT_TRUE(
ExecJs(current_frame_host(), JsReplace("window.open($1)", error_url)));
WebContentsImpl* popup_webcontents =
static_cast<WebContentsImpl*>(shell_observer.GetShell()->web_contents());
WaitForLoadStop(popup_webcontents);
NavigationEntry* entry =
popup_webcontents->GetController().GetLastCommittedEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
RenderFrameHostImpl* popup_frame =
popup_webcontents->GetPrimaryFrameTree().root()->current_frame_host();
EXPECT_EQ(network::mojom::ReferrerPolicy::kDefault,
popup_frame->policy_container_host()->referrer_policy());
}
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
ContentSecurityPoliciesFromHeader) {
GURL url = embedded_test_server()->GetURL(
"a.com", "/set-header?Content-Security-Policy: img-src 'none'");
ASSERT_TRUE(NavigateToURL(shell(), url));
ASSERT_EQ(1u, current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
auto& csp = current_frame_host()
->policy_container_host()
->policies()
.content_security_policies[0];
EXPECT_EQ("img-src 'none'", csp->header->header_value);
EXPECT_TRUE(csp->directives.find(network::mojom::CSPDirectiveName::ImgSrc) !=
csp->directives.end());
EXPECT_EQ(0u, csp->directives.at(network::mojom::CSPDirectiveName::ImgSrc)
->sources.size());
}
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
ContentSecurityPolicyFromMeta) {
GURL page = embedded_test_server()->GetURL("a.com", "/empty.html");
ASSERT_TRUE(NavigateToURL(shell(), page));
EXPECT_EQ(0u, current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
ASSERT_TRUE(ExecJs(current_frame_host(),
"var meta = document.createElement('meta');"
"meta.httpEquiv = 'content-security-policy';"
"meta.content = \"img-src 'none'\";"
"document.head.appendChild(meta);"));
ASSERT_EQ(1u, current_frame_host()
->policy_container_host()
->policies()
.content_security_policies.size());
auto& csp = current_frame_host()
->policy_container_host()
->policies()
.content_security_policies[0];
EXPECT_EQ("img-src 'none'", csp->header->header_value);
EXPECT_TRUE(csp->directives.find(network::mojom::CSPDirectiveName::ImgSrc) !=
csp->directives.end());
EXPECT_EQ(0u, csp->directives.at(network::mojom::CSPDirectiveName::ImgSrc)
->sources.size());
}
// Regression test for https://crbug.com/1196372. This test passes if the
// renderer does not crash.
IN_PROC_BROWSER_TEST_F(PolicyContainerHostBrowserTest,
PolicyContainerOnClonedDocumentNoCrash) {
GURL page = embedded_test_server()->GetURL("a.com", "/empty.html");
GURL img_url = embedded_test_server()->GetURL("a.com", "/blank.jpg");
ASSERT_TRUE(NavigateToURL(shell(), page));
// Create an empty iframe and clone its document. Then execute a javascript
// URL inside the iframe. This will create a new ExecutionContext, but with
// the same PolicyContainer. However, the clone we created is still around and
// still has an ExecutionContext, which has not PolicyContainer anymore. The
// following code used to trigger a nullptr dereference, while it should not.
ASSERT_TRUE(ExecJs(current_frame_host(), JsReplace(R"(
new Promise((resolve, reject) => {
let iframe = document.createElement('iframe');
document.body.appendChild(iframe);
let d = iframe.contentDocument.cloneNode(true);
iframe.src =
'javascript:"<script>top.postMessage(\'ready\',\'*\');</script>"';
function addStyleSheet() {
let css = 'html { background: url($1); }';
let style = d.createElement('style');
d.head.appendChild(style);
style.type = 'text/css';
style.appendChild(d.createTextNode(css));
};
window.addEventListener('message', e => {
if (e.source !== iframe.contentWindow) return;
if (e.data !== 'ready') return;
addStyleSheet();
resolve();
});
});
)",
img_url)));
}
} // namespace content