blob: 9a649787083db95104445f62265c495baab19802 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/scoped_feature_list.h"
#include "content/browser/process_lock.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_base.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 "testing/gtest/include/gtest/gtest.h"
namespace content {
// A set of tests for loading data: URL subframes in their own SiteInstance in
// their initiator's SiteInstanceGroup. Parameterized to also run in the legacy
// mode where data: URLs load in their initiator's SiteInstance. See
// https://crbug.com/40176090.
class DataURLSiteInstanceGroupTest
: public ContentBrowserTestBase,
public ::testing::WithParamInterface<bool> {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolateAllSitesForTesting(command_line);
if (IsSiteInstanceGroupEnabled()) {
feature_list_.InitAndEnableFeature(
features::kSiteInstanceGroupsForDataUrls);
} else {
feature_list_.InitAndDisableFeature(
features::kSiteInstanceGroupsForDataUrls);
}
}
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
return info.param ? "DataSubframeHasOwnSiteInstance"
: "DataSubframeInInitiatorSiteInstance";
}
protected:
bool IsSiteInstanceGroupEnabled() const { return GetParam(); }
private:
base::test::ScopedFeatureList feature_list_;
};
// A b.com subframe navigates itself to a data: URL. This allows testing that a
// navigation from b.com to a data: URL is a local-to-local frame navigation and
// does not create a proxy, despite being cross-SiteInstance.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTest,
SiteInstanceGroupDataURLSubframe) {
GURL url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b,b)"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Both subframes share a site and therefore SiteInstance.
EXPECT_EQ(main_frame()
->child_at(0)
->render_manager()
->current_frame_host()
->GetSiteInstance(),
main_frame()
->child_at(1)
->render_manager()
->current_frame_host()
->GetSiteInstance());
// The first B subframe navigates itself to a data: URL, so the data: subframe
// has initiator b.com.
GURL data_url("data:text/html,test");
EXPECT_TRUE(NavigateToURLFromRenderer(main_frame()->child_at(0), data_url));
SiteInstanceGroup* root_group = main_frame_host()->GetSiteInstance()->group();
SiteInstanceImpl* child0_instance = main_frame()
->child_at(0)
->render_manager()
->current_frame_host()
->GetSiteInstance();
SiteInstanceImpl* child1_instance = main_frame()
->child_at(1)
->render_manager()
->current_frame_host()
->GetSiteInstance();
// In both parameterization modes, the root is cross-site from the subframes
// and should not share a process or SiteInstanceGroup.
EXPECT_NE(root_group, child0_instance->group());
if (ShouldCreateSiteInstanceForDataUrls()) {
// The data: subframe should have its own SiteInstance in the same
// SiteInstanceGroup as the other B subframe.
EXPECT_NE(child0_instance, child1_instance);
EXPECT_EQ(child0_instance->group(), child1_instance->group());
EXPECT_EQ(
" Site A ------------ proxies for {B,C}\n"
" |--Site B ------- proxies for A\n"
" +--Site C ------- proxies for A\n"
"Where A = http://a.com/\n"
" B = data:nonce_B\n"
" C = http://b.com/",
DepictFrameTree(*main_frame()));
} else {
// The data: subframe shares a SiteInstance with the other B subframe.
EXPECT_EQ(child0_instance, child1_instance);
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" |--Site B ------- proxies for A\n"
" +--Site B ------- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://b.com/",
DepictFrameTree(*main_frame()));
}
}
// Check that for a main frame data: URL, about:blank frames end up in the data:
// URL's SiteInstance and can be scripted.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTest,
MainFrameDataURLWithAboutBlank) {
// Load a main frame data: URL with an about:blank subframe. The main frame
// and subframe should be in the same SiteInstance.
GURL data_url(
"data:text/html,<body> This page has one about:blank iframe:"
"<iframe name='frame1' src='about:blank'></iframe> </body>");
EXPECT_TRUE(NavigateToURL(shell(), data_url));
EXPECT_EQ(main_frame_host()->GetSiteInstance(),
main_frame()->child_at(0)->current_frame_host()->GetSiteInstance());
EXPECT_EQ(
" Site A\n"
" +--Site A\n"
"Where A = data:nonce_A",
DepictFrameTree(*main_frame()));
// The main frame should be able to script its about:blank subframe.
EXPECT_TRUE(ExecJs(main_frame(), "frames[0].window.name = 'new-name'"));
EXPECT_EQ(main_frame()->child_at(0)->frame_name(), "new-name");
}
// Check that for a subframe data: URL, about:blank frames end up in the data:
// URL's SiteInstance and can be scripted.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTest,
SubframeDataURLWithAboutBlank) {
// Navigate to a main frame with a cross-site iframe.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Navigate the subframe to a data: URL with an about:blank subframe.
GURL data_url(
"data:text/html,<body> This page has one about:blank iframe:"
"<iframe name='frame1' src='about:blank'></iframe> </body>");
TestNavigationObserver observer(web_contents());
FrameTreeNode* child = main_frame()->child_at(0);
EXPECT_TRUE(NavigateToURLFromRenderer(child, data_url));
EXPECT_TRUE(observer.last_navigation_succeeded());
// The data: frame should be able to script its about:blank subframe.
EXPECT_TRUE(ExecJs(child, "frames[0].window.name = 'new-name'"));
EXPECT_EQ(child->child_at(0)->frame_name(), "new-name");
if (ShouldCreateSiteInstanceForDataUrls()) {
EXPECT_EQ(
" Site A ------------ proxies for {B,C}\n"
" +--Site B ------- proxies for A\n"
" +--Site B -- proxies for A\n"
"Where A = http://a.com/\n"
" B = data:nonce_B\n"
" C = http://b.com/",
DepictFrameTree(*main_frame()));
} else {
EXPECT_EQ(
" Site A ------------ proxies for B\n"
" +--Site B ------- proxies for A\n"
" +--Site B -- proxies for A\n"
"Where A = http://a.com/\n"
" B = http://b.com/",
DepictFrameTree(*main_frame()));
}
}
// Test that data: subframes are added to the correct SiteInstanceGroup. In the
// case of A(B_sandbox, B(data_sandbox)), data_sandbox should go in the group of
// its initiator, B. However, because data is sandboxed and B isn't, B's group
// is not the correct group, but B_sandbox's group is.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTest,
SandboxedDataFindsExistingSiteInstance) {
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Create sandboxed cross-origin child frame.
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
{
std::string js_str = base::StringPrintf(
"var frame = document.createElement('iframe'); "
"frame.id = 'test_frame_sandboxed'; "
"frame.sandbox = ''; "
"frame.src = '%s'; "
"document.body.appendChild(frame);",
b_url.spec().c_str());
EXPECT_TRUE(ExecJs(shell(), js_str));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
}
FrameTreeNode* child0 = main_frame()->child_at(0);
SiteInstanceImpl* child0_instance =
child0->current_frame_host()->GetSiteInstance();
EXPECT_TRUE(child0_instance->GetSiteInfo().is_sandboxed());
// Create a non-sandboxed cross-origin child frame, which has the same site as
// the sandboxed subframe, but is in a different (non-sandboxed) SiteInstance.
{
std::string js_str = base::StringPrintf(
"var frame = document.createElement('iframe'); "
"frame.id = 'test_frame_not_sandboxed'; "
"frame.src = '%s'; "
"document.body.appendChild(frame);",
b_url.spec().c_str());
EXPECT_TRUE(ExecJs(shell(), js_str));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
}
FrameTreeNode* child1 = main_frame()->child_at(1);
SiteInstanceImpl* child1_instance =
child1->current_frame_host()->GetSiteInstance();
EXPECT_FALSE(child1_instance->GetSiteInfo().is_sandboxed());
EXPECT_NE(child0_instance, child1_instance);
// Add a sandboxed data: URL subframe as a subframe of `child1`. It should get
// its own SiteInstance.
{
GURL data_url("data:text/html,test");
std::string js_str = base::StringPrintf(
"var frame = document.createElement('iframe'); "
"frame.id = 'test_frame_data_sandboxed'; "
"frame.sandbox = ''; "
"frame.src = '%s'; "
"document.body.appendChild(frame);",
data_url.spec().c_str());
EXPECT_TRUE(ExecJs(child1, js_str));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
}
FrameTreeNode* data = child1->child_at(0);
SiteInstanceImpl* data_instance =
data->current_frame_host()->GetSiteInstance();
// The last committed URL is the data: URL, but because sandboxed data frames
// are not handled, the data SiteInstance is in the `child0` instance.
// TODO(crbug.com/40269084): After sandboxed data: subframes are supported,
// update expectation for `data_instance` to have a separate SiteInstance in
// the `child0` group with a data: site URL.
EXPECT_TRUE(data->current_frame_host()->GetLastCommittedURL().SchemeIs(
url::kDataScheme));
EXPECT_EQ(data_instance, child0_instance);
EXPECT_NE(data_instance, child1_instance);
EXPECT_EQ(data_instance->group(), child0_instance->group());
EXPECT_NE(data_instance->group(), child1_instance->group());
}
// When an unsandboxed main frame A opens a data: subframe in a sandboxed
// iframe, e.g. A(data_sandboxed), the data subframe is currently in a sandboxed
// version of A's SiteInstance and SiteInstanceGroup because sandboxed data:
// URL subframes are not supported in SiteInstanceGroup.
// TODO(crbug.com/390452841): Add SiteInstanceGroup support for sandboxed data:
// subframes.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTest,
MainFrameWithSandboxedSubframe) {
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
{
// Create a sandboxed data: subframe.
GURL data_url("data:text/html,test");
std::string js_str = base::StringPrintf(
"var frame = document.createElement('iframe'); "
"frame.id = 'test_frame_sandboxed'; "
"frame.sandbox = ''; "
"frame.src = '%s'; "
"document.body.appendChild(frame);",
data_url.spec().c_str());
EXPECT_TRUE(ExecJs(shell(), js_str));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
}
SiteInstanceImpl* main_instance = main_frame_host()->GetSiteInstance();
FrameTreeNode* data = main_frame()->child_at(0);
SiteInstanceImpl* data_instance =
data->current_frame_host()->GetSiteInstance();
EXPECT_NE(main_instance, data_instance);
// Sandboxed data: subframes are not currently supported. This means the data:
// subframe will be in an A_sandbox SiteInstance.
// TODO(crbug.com/390452841): When sandboxed data: subframes are supported,
// `data_instance` should be in a SiteInstance with a data:nonce site URL, in
// a SiteInstanceGroup with nothing else in it.
EXPECT_TRUE(data->current_frame_host()->GetLastCommittedURL().SchemeIs(
url::kDataScheme));
EXPECT_FALSE(data_instance->GetSiteURL().SchemeIs(url::kDataScheme));
EXPECT_TRUE(data_instance->IsSandboxed());
RenderProcessHost* main_process = main_instance->GetProcess();
RenderProcessHost* data_process = data_instance->GetProcess();
EXPECT_NE(main_process, data_process);
// The data: subframe's process lock should be a sandboxed version of main
// frame A's process lock.
ProcessLock main_process_lock = main_process->GetProcessLock();
ProcessLock data_process_lock = data_process->GetProcessLock();
EXPECT_NE(main_process_lock, data_process_lock);
EXPECT_FALSE(main_process_lock.is_sandboxed());
EXPECT_TRUE(data_process_lock.is_sandboxed());
// Compare hosts here, because `main_process_lock` is for the site "a.com" and
// `data_process_lock` is for the origin "a.com:port", since it's sandboxed.
EXPECT_EQ(main_process_lock.lock_url().host(),
data_process_lock.lock_url().host());
}
// Test where a main frame has multiple data: URL subframes. This tests that
// the data: URL subframes each have their own SiteInstance, that the
// SiteInstances are in the same SiteInstanceGroup, and that a SiteInstanceGroup
// can have multiple data: URL SiteInstances.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTest, ParentNavigatesSubframes) {
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string data_url1_str("data:text/html,test1");
std::string data_url2_str("data:text/html,test2");
{
std::string js_str = base::StringPrintf(
"for (let i = 0; i < 2; i++) { "
" var frame = document.createElement('iframe'); "
" frame.id = 'test_frame'; "
" frame.src = 'data:text/html,' + i; "
" document.body.appendChild(frame);"
"}");
EXPECT_TRUE(ExecJs(shell(), js_str));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
}
FrameTreeNode* child0 = main_frame()->child_at(0);
SiteInstanceImpl* child0_instance =
child0->current_frame_host()->GetSiteInstance();
FrameTreeNode* child1 = main_frame()->child_at(1);
SiteInstanceImpl* child1_instance =
child1->current_frame_host()->GetSiteInstance();
if (ShouldCreateSiteInstanceForDataUrls()) {
EXPECT_NE(child0_instance, child1_instance);
} else {
EXPECT_EQ(child0_instance, child1_instance);
}
EXPECT_EQ(child0_instance->group(), child1_instance->group());
EXPECT_EQ(main_frame_host()->GetSiteInstance()->group(),
child0_instance->group());
}
// The same as DataURLSiteInstanceGroupTest, but with kSitePerProcess disabled.
class DataURLSiteInstanceGroupTestWithoutSiteIsolation
: public DataURLSiteInstanceGroupTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->RemoveSwitch(switches::kSitePerProcess);
command_line->AppendSwitch(switches::kDisableSiteIsolation);
}
};
// Ensure that when SiteIsolation is disabled, a data: subframe in an isolated
// main frame can create and navigate a subframe successfully. E.g.
// A_isolated(data(B)). This is a regression test for crbug.com/379385125.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTestWithoutSiteIsolation,
DataSubframeOpensSubframeAndNavigates) {
// Explicitly isolate a.com since SiteIsolation is disabled.
IsolateOriginsForTesting(embedded_test_server(), web_contents(), {"a.com"});
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// Add a data: URL subframe.
{
TestNavigationObserver observer(web_contents());
GURL data_url("data:text/html,test");
std::string js_str = base::StringPrintf(
"var frame = document.createElement('iframe'); "
"frame.id = 'data_frame'; "
"frame.src = '%s'; "
"document.body.appendChild(frame);",
data_url.spec().c_str());
EXPECT_TRUE(ExecJs(shell(), js_str));
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
ASSERT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(data_url, observer.last_navigation_url());
}
SiteInstanceImpl* a_instance = main_frame_host()->GetSiteInstance();
FrameTreeNode* data_frame = main_frame()->child_at(0);
SiteInstanceImpl* data_instance =
data_frame->current_frame_host()->GetSiteInstance();
if (ShouldCreateSiteInstanceForDataUrls()) {
EXPECT_NE(a_instance, data_instance);
} else {
EXPECT_EQ(a_instance, data_instance);
}
EXPECT_EQ(a_instance->group(), data_instance->group());
// Add a subframe of the data: URL, and navigate it to b.com.
GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
{
TestNavigationObserver observer(web_contents());
std::string js_str = base::StringPrintf(
"var frame = document.createElement('iframe'); "
"frame.id = 'child_frame_of_data'; "
"frame.src = '%s'; "
"document.body.appendChild(frame);",
url_b.spec().c_str());
EXPECT_TRUE(ExecJs(data_frame, js_str));
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(url_b, observer.last_navigation_url());
}
FrameTreeNode* b_frame = data_frame->child_at(0);
SiteInstanceImpl* b_instance =
b_frame->current_frame_host()->GetSiteInstance();
EXPECT_NE(data_instance, b_instance);
EXPECT_NE(data_instance->group(), b_instance->group());
EXPECT_NE(data_instance->GetProcess(), b_instance->GetProcess());
}
// Ensure that when default SiteInstances and SiteInstanceGroups for data: URLs
// are enabled, data: URL subframes are in a separate SiteInstance, but share a
// process with the default SiteInstance.
IN_PROC_BROWSER_TEST_P(DataURLSiteInstanceGroupTestWithoutSiteIsolation,
DataSubframeInDefaultSiteInstance) {
GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_a));
// Add a data: URL subframe.
{
TestNavigationObserver observer(web_contents());
GURL data_url("data:text/html,test");
std::string js_str = base::StringPrintf(
"var frame = document.createElement('iframe'); "
"frame.id = 'data_frame'; "
"frame.src = '%s'; "
"document.body.appendChild(frame);",
data_url.spec().c_str());
EXPECT_TRUE(ExecJs(shell(), js_str));
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
ASSERT_TRUE(WaitForLoadStop(web_contents()));
EXPECT_EQ(data_url, observer.last_navigation_url());
}
SiteInstanceImpl* a_instance = main_frame_host()->GetSiteInstance();
EXPECT_TRUE(a_instance->IsDefaultSiteInstance());
FrameTreeNode* data_frame = main_frame()->child_at(0);
SiteInstanceImpl* data_instance =
data_frame->current_frame_host()->GetSiteInstance();
if (ShouldCreateSiteInstanceForDataUrls()) {
EXPECT_NE(a_instance, data_instance);
EXPECT_FALSE(data_instance->IsDefaultSiteInstance());
} else {
EXPECT_EQ(a_instance, data_instance);
}
// In both cases, A and data: should share a group. If the feature is enabled,
// data: should have its own SiteInstance, but should still be in the default
// group as it does not need further isolating. If the feature is disabled,
// both should be in the default SiteInstance.
EXPECT_EQ(a_instance->group(), data_instance->group());
}
INSTANTIATE_TEST_SUITE_P(All,
DataURLSiteInstanceGroupTest,
::testing::Bool(),
&DataURLSiteInstanceGroupTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
DataURLSiteInstanceGroupTestWithoutSiteIsolation,
::testing::Bool(),
&DataURLSiteInstanceGroupTest::DescribeParams);
} // namespace content