blob: 536b32bf1e9e3d3cefa30f16cd4a706163f84f2b [file] [log] [blame]
// Copyright 2025 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/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "components/guest_contents/browser/guest_contents_handle.h"
#include "components/guest_contents/browser/guest_contents_host_impl.h"
#include "components/guest_contents/common/guest_contents.mojom.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/public/browser/unowned_inner_web_contents_client.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.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 "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
class GuestContentsSecurityBrowsertest : public content::ContentBrowserTest {
public:
GuestContentsSecurityBrowsertest() {
scoped_feature_list_.InitAndEnableFeature(
features::kAttachUnownedInnerWebContents);
}
void SetUpOnMainThread() override {
content::ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
embedded_https_test_server().SetSSLConfig(net::EmbeddedTestServer::CERT_OK);
embedded_https_test_server().ServeFilesFromSourceDirectory(
"components/test/data/guest_contents");
ASSERT_TRUE(embedded_https_test_server().Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// HTTPS server only serves a valid cert for localhost, so this is needed
// to load pages from other hosts without an error.
command_line->AppendSwitch(switches::kIgnoreCertificateErrors);
}
void TearDownOnMainThread() override { inner_webcontents_.reset(); }
content::WebContents* GetInnerWebContents() {
if (!inner_webcontents_) {
SetUpEmbeddedWebContents();
}
return inner_webcontents_.get();
}
content::WebContents* GetOuterWebContents() {
return GetInnerWebContents()->GetOuterWebContents();
}
private:
// Helper function to set up embedded web contents for tests.
void SetUpEmbeddedWebContents() {
const GURL outer_url(
embedded_https_test_server().GetURL("outer.com", "/iframe.html"));
const GURL inner_url(
embedded_https_test_server().GetURL("inner.com", "/inner.html"));
// Navigate to iframe.html
ASSERT_TRUE(NavigateToURL(shell(), outer_url));
content::WebContents* main_web_contents = shell()->web_contents();
ASSERT_TRUE(main_web_contents);
// Wait for the page to load and iframe to be ready.
EXPECT_TRUE(content::WaitForLoadStop(main_web_contents));
// Setup inner WebContents and navigate it to inner.html.
content::WebContents::CreateParams inner_params(
shell()->web_contents()->GetBrowserContext());
inner_webcontents_ = content::WebContents::Create(inner_params);
content::NavigationController::LoadURLParams load_params(inner_url);
inner_webcontents_->GetController().LoadURLWithParams(load_params);
// Wait for guest content to load.
EXPECT_TRUE(content::WaitForLoadStop(inner_webcontents_.get()));
guest_contents::GuestContentsHandle* guest_handle =
guest_contents::GuestContentsHandle::CreateForWebContents(
inner_webcontents_.get());
ASSERT_TRUE(guest_handle);
mojo::Remote<guest_contents::mojom::GuestContentsHost> guest_host_remote;
guest_contents::GuestContentsHostImpl::Create(
main_web_contents, guest_host_remote.BindNewPipeAndPassReceiver());
// Get the iframe's frame token and guest_id for the attach call.
blink::LocalFrameToken iframe_token =
content::ChildFrameAt(main_web_contents, 0)->GetFrameToken();
guest_contents::GuestId guest_id = guest_handle->id();
// Perform the attach.
base::RunLoop attach_loop;
guest_host_remote->Attach(iframe_token, guest_id,
base::BindLambdaForTesting([&](bool result) {
EXPECT_TRUE(result);
attach_loop.Quit();
}));
attach_loop.Run();
// Verify that the connection is established.
EXPECT_EQ(main_web_contents, inner_webcontents_->GetOuterWebContents());
}
std::unique_ptr<content::WebContents> inner_webcontents_;
base::test::ScopedFeatureList scoped_feature_list_;
};
// Test that parent history is not affected by embedded navigation.
// The history.length should be independent between inner and outer
// webcontents.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
HistoryLengthIndependent) {
content::WebContents* outer_webcontents = GetOuterWebContents();
content::WebContents* inner_webcontents = GetInnerWebContents();
EXPECT_FALSE(outer_webcontents->GetOuterWebContents());
EXPECT_TRUE(outer_webcontents);
EXPECT_EQ(1, EvalJs(outer_webcontents, "window.history.length"));
// Navigate the inner contents another cross origin URL and verify outer
// remains 1.
GURL url = embedded_https_test_server().GetURL("b.com", "/simple.html");
EXPECT_TRUE(NavigateToURL(inner_webcontents, url));
EXPECT_TRUE(content::WaitForLoadStop(inner_webcontents));
EXPECT_EQ(1, EvalJs(outer_webcontents, "window.history.length"));
EXPECT_EQ(2, EvalJs(inner_webcontents, "window.history.length"));
}
// Test that the frame tree isolation between inner and outer webcontents.
// The outer webcontents is able to see the GuestContents but not the other
// way around.
// NOTE: This is a known security issue.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest, WindowFramesLength) {
content::WebContents* inner_webcontents = GetInnerWebContents();
content::WebContents* outer_webcontents = GetOuterWebContents();
EXPECT_EQ(0, EvalJs(inner_webcontents, "window.frames.length"));
EXPECT_EQ(1, EvalJs(outer_webcontents, "window.frames.length"));
}
// Test whether the parent window counts the embedded content as a frame.
// NOTE: This is a known security issue.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
WindowLengthIndependent) {
content::WebContents* outer_webcontents = GetOuterWebContents();
EXPECT_EQ(1, EvalJs(outer_webcontents, "window.length"));
}
// Test that the embedded content acts as top level. window.top in the
// embedded content should equal window (itself), not the actual parent's
// top-level window.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest, WindowTopIndependent) {
content::WebContents* inner_webcontents = GetInnerWebContents();
EXPECT_TRUE(EvalJs(inner_webcontents, "window.top === window").ExtractBool());
}
// Test that the embedded content acts as top level.
// window.opener should be null since the embedded content should not
// have access to the parent that "opened" it.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
WindowOpenerIndependent) {
content::WebContents* inner_webcontents = GetInnerWebContents();
EXPECT_TRUE(
EvalJs(inner_webcontents, "window.opener === null").ExtractBool());
}
// Test that the embedded content acts as top level.
// window.parent should equal window (itself) since there should be
// no accessible parent window from the embedded content's perspective.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
WindowParentIndependent) {
content::WebContents* inner_webcontents = GetInnerWebContents();
EXPECT_TRUE(
EvalJs(inner_webcontents, "window.parent === window").ExtractBool());
}
// Test that the embedded content acts as top level.
// window.frameElement should be null since the embedded content should
// not appear to be contained within a frame element.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
WindowFrameElementIndependent) {
content::WebContents* inner_webcontents = GetInnerWebContents();
EXPECT_TRUE(
EvalJs(inner_webcontents, "window.frameElement === null").ExtractBool());
}
// Test that inner webcontents cannot target outer webcontents
// _parent and _top should all target the inner webcontents itself.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
WindowOpenTargetingIndependent) {
content::WebContents* inner_webcontents = GetInnerWebContents();
content::WebContents* outer_webcontents = GetOuterWebContents();
// Store current URLs to verify navigation targets.
GURL outer_url = outer_webcontents->GetLastCommittedURL();
// Test _parent targeting from inner webcontents.
GURL test_url =
embedded_https_test_server().GetURL("b.com", "/defaultresponse");
EXPECT_TRUE(
ExecJs(inner_webcontents,
content::JsReplace("window.open($1, '_parent')", test_url)));
// Verify inner webcontents navigated, outer did not.
EXPECT_TRUE(content::WaitForLoadStop(inner_webcontents));
EXPECT_EQ(inner_webcontents->GetLastCommittedURL().host(), test_url.host());
EXPECT_EQ(outer_webcontents->GetLastCommittedURL(), outer_url);
// Test _top also just navigates inner.
test_url = embedded_https_test_server().GetURL("a.com", "/defaultresponse");
EXPECT_TRUE(ExecJs(inner_webcontents,
content::JsReplace("window.open($1, '_top')", test_url)));
// Verify inner webcontents navigated, outer did not.
EXPECT_TRUE(content::WaitForLoadStop(inner_webcontents));
EXPECT_EQ(inner_webcontents->GetLastCommittedURL().host(), test_url.host());
EXPECT_EQ(outer_webcontents->GetLastCommittedURL(), outer_url);
}
// Test that cross-context window references are not useful.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
WindowOpenReferenceIndependent) {
content::WebContents* inner_webcontents = GetInnerWebContents();
content::WebContents* outer_webcontents = GetOuterWebContents();
// Part 1. outer makes a window, it's not accessible in inner.
EXPECT_TRUE(ExecJs(outer_webcontents,
"window.testWindow = window.open('about:blank')"));
EXPECT_TRUE(EvalJs(outer_webcontents, "window.hasOwnProperty('testWindow')")
.ExtractBool());
EXPECT_FALSE(EvalJs(inner_webcontents, "window.hasOwnProperty('testWindow')")
.ExtractBool());
// Part 2. inner makes a window, it's not accessible in outer.
EXPECT_TRUE(ExecJs(inner_webcontents,
"window.innerWindow = window.open('about:blank')"));
EXPECT_FALSE(EvalJs(outer_webcontents, "window.hasOwnProperty('innerWindow')")
.ExtractBool());
}
// Array accessor on the outer window is able to access inner window.
// NOTE: This is a known security issue.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
WindowIndexedAccessor) {
content::WebContents* outer_webcontents = GetOuterWebContents();
EXPECT_FALSE(
EvalJs(outer_webcontents, "window[0] === undefined").ExtractBool());
}
// Test postMessage from outer to inner. The expectation is that communication
// in either direction is not allowed. That holds true here in content_shell.
// However, this test fails in a webium environment. See crbug.com/452082277
// for more information.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
OuterToInnerPostMessage) {
content::WebContents* inner_webcontents = GetInnerWebContents();
content::WebContents* outer_webcontents = GetOuterWebContents();
// 1. Prepare the inner to receive postMessage and mark receipt.
EXPECT_TRUE(ExecJs(inner_webcontents,
"window.addEventListener('message', (event) => { "
"window.postMessageReceived = true; "
"});"));
// 2. PostMessage from outer.
EXPECT_TRUE(ExecJs(outer_webcontents,
"var iframe = document.querySelector('#iframe');"));
EXPECT_TRUE(ExecJs(outer_webcontents,
"iframe.contentWindow.postMessage('test', '*');"));
// 3. Verify inner did not receive the postMessage.
EXPECT_FALSE(
EvalJs(inner_webcontents, "window.hasOwnProperty('postMessageReceived')")
.ExtractBool());
}
// Test PostMessage to '*' from outer does not affect inner.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
OuterToInnerStarPostMessage) {
content::WebContents* inner_webcontents = GetInnerWebContents();
content::WebContents* outer_webcontents = GetOuterWebContents();
// 1. Prepare the inner to receive postMessage and mark receipt.
EXPECT_TRUE(ExecJs(inner_webcontents,
"window.addEventListener('message', (event) => { "
"window.postMessageReceived = true; "
"});"));
// 2. PostMessage from outer.
EXPECT_TRUE(ExecJs(outer_webcontents, "window.postMessage('test', '*');"));
// 3. Verify inner did not receive the postMessage.
EXPECT_FALSE(
EvalJs(inner_webcontents, "window.hasOwnProperty('postMessageReceived')")
.ExtractBool());
}
// Test PostMessage to '*' from inner does not affect outer.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
InnerToOuterStarPostMessage) {
content::WebContents* inner_webcontents = GetInnerWebContents();
content::WebContents* outer_webcontents = GetOuterWebContents();
// 1. Prepare the outer to receive postMessage and mark receipt.
EXPECT_TRUE(ExecJs(outer_webcontents,
"window.addEventListener('message', (event) => { "
"window.postMessageReceived = true; "
"});"));
// 2. PostMessage from inner.
EXPECT_TRUE(ExecJs(inner_webcontents, "window.postMessage('test', '*');"));
// 3. Verify outer did not receive the postMessage.
EXPECT_FALSE(
EvalJs(outer_webcontents, "window.hasOwnProperty('postMessageReceived')")
.ExtractBool());
}
// Test that the outer has no perception into nested iframes inside the guest
// contents.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest, NestedIframeInGuest) {
content::WebContents* inner_webcontents = GetInnerWebContents();
content::WebContents* outer_webcontents = GetOuterWebContents();
// Navigate to same-origin as in non-guest-contents scenarios, reach down
// into a nested iframe is possible. The outer is navigated to the outer.com
// host.
GURL url = embedded_https_test_server().GetURL("outer.com",
"/inner_with_iframe.html");
EXPECT_TRUE(NavigateToURL(inner_webcontents, url));
EXPECT_TRUE(content::WaitForLoadStop(inner_webcontents));
// The embedder should be able to see the guest contents via window.frames.
EXPECT_EQ(1, EvalJs(outer_webcontents, "window.frames.length"));
EXPECT_EQ(1, EvalJs(outer_webcontents, "window.length"));
// window[0] should point at the top iframe. We should not be able to access
// "window" or "frame" inside of it. Attempting to do so throws a
// SecurityError.
EXPECT_TRUE(EvalJs(outer_webcontents,
"try { window[0].window[0] } catch (e) { e.name === "
"'SecurityError' }")
.ExtractBool());
EXPECT_TRUE(EvalJs(outer_webcontents,
"try { window[0].frames[0] } catch (e) { e.name === "
"'SecurityError' }")
.ExtractBool());
// window[0] refers to the guest contents, [1] should not refer to anything.
EXPECT_TRUE(
EvalJs(outer_webcontents, "window[1] === undefined").ExtractBool());
}
// Test HTMLIFrameElement's contentWindow and contentDocument accessibility.
// NOTE: This is a known security issue.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest, HTMLIFrameElement) {
content::WebContents* outer_webcontents = GetOuterWebContents();
EXPECT_TRUE(ExecJs(outer_webcontents,
"var iframe = document.querySelector('#iframe');"));
EXPECT_FALSE(
EvalJs(outer_webcontents, "iframe.contentWindow === null").ExtractBool());
EXPECT_TRUE(EvalJs(outer_webcontents, "iframe.contentDocument === null")
.ExtractBool());
}
// Test that a link with target="_parent" or target="_top" from the inner
// webcontents does not navigate the outer webcontents.
IN_PROC_BROWSER_TEST_F(GuestContentsSecurityBrowsertest,
NavigateParentWithLink) {
content::WebContents* outer_webcontents = GetOuterWebContents();
content::WebContents* inner_webcontents = GetInnerWebContents();
// Store the outer's current URL to verify it doesn't change.
GURL outer_url = outer_webcontents->GetLastCommittedURL();
GURL inner_url = inner_webcontents->GetLastCommittedURL();
// Test 1: Create a link with target="_parent" and click it.
GURL target_url =
embedded_https_test_server().GetURL("attacker.com", "/simple.html");
EXPECT_TRUE(
ExecJs(inner_webcontents,
content::JsReplace("const link = document.createElement('a');"
"link.href = $1;"
"link.target = '_parent';"
"link.id = 'testLink';"
"link.textContent = 'Click me';"
"document.body.appendChild(link);"
"link.click();",
target_url)));
EXPECT_TRUE(content::WaitForLoadStop(inner_webcontents));
// Verify the inner webcontents navigated to the target URL.
EXPECT_EQ(inner_webcontents->GetLastCommittedURL(), target_url);
// Verify the outer webcontents did NOT navigate.
EXPECT_EQ(outer_webcontents->GetLastCommittedURL(), outer_url);
// Navigate inner back to original URL for next test.
EXPECT_TRUE(NavigateToURL(inner_webcontents, inner_url));
// Test 2: Create a link with target="_top" and click it.
EXPECT_TRUE(
ExecJs(inner_webcontents,
content::JsReplace("const link2 = document.createElement('a');"
"link2.href = $1;"
"link2.target = '_top';"
"link2.textContent = 'Click me too';"
"document.body.appendChild(link2);"
"link2.click();",
target_url)));
EXPECT_TRUE(content::WaitForLoadStop(inner_webcontents));
// Verify the inner webcontents navigated to the target URL.
EXPECT_EQ(inner_webcontents->GetLastCommittedURL(), target_url);
// Verify the outer webcontents still did NOT navigate.
EXPECT_EQ(outer_webcontents->GetLastCommittedURL(), outer_url);
}