blob: 88d0d367f460765d180d871099dd4d719f3e3a28 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include <memory>
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "components/security_interstitials/content/security_interstitial_controller_client.h"
#include "components/security_interstitials/content/security_interstitial_page.h"
#include "components/security_interstitials/content/settings_page_helper.h"
#include "components/security_interstitials/core/controller_client.h"
#include "components/security_interstitials/core/metrics_helper.h"
#include "content/public/browser/certificate_request_result_type.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "net/base/net_errors.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
namespace security_interstitials {
const char kTestSslMetricsName[] = "test_blocking_page";
std::unique_ptr<security_interstitials::MetricsHelper> CreateTestMetricsHelper(
content::WebContents* web_contents) {
MetricsHelper::ReportDetails report_details;
report_details.metric_prefix = kTestSslMetricsName;
return std::make_unique<security_interstitials::MetricsHelper>(
GURL(), report_details, nullptr);
}
class TestInterstitialPage : public SecurityInterstitialPage {
public:
// |*destroyed_tracker| is set to true in the destructor.
TestInterstitialPage(content::WebContents* web_contents,
const GURL& request_url,
bool* destroyed_tracker)
: SecurityInterstitialPage(
web_contents,
request_url,
std::make_unique<SecurityInterstitialControllerClient>(
web_contents,
CreateTestMetricsHelper(web_contents),
nullptr,
base::i18n::GetConfiguredLocale(),
GURL(),
/* settings_page_helper*/ nullptr)),
destroyed_tracker_(destroyed_tracker) {}
~TestInterstitialPage() override { *destroyed_tracker_ = true; }
void OnInterstitialClosing() override {}
protected:
void PopulateInterstitialStrings(base::Value::Dict& load_time_data) override {
}
private:
raw_ptr<bool> destroyed_tracker_;
};
class SecurityInterstitialTabHelperTest
: public content::RenderViewHostTestHarness {
protected:
void SetUp() override {
RenderViewHostTestHarness::SetUp();
content::RenderFrameHostTester::For(main_rfh())
->InitializeRenderFrameIfNeeded();
}
// Creates a navigation handle for the main frame.
std::unique_ptr<content::MockNavigationHandle> CreateHandle(
bool committed,
bool is_same_document) {
return CreateHandle(main_rfh(), committed, is_same_document);
}
// Creates a navigation handle for a subframe.
std::unique_ptr<content::MockNavigationHandle> CreateSubframeHandle(
bool committed,
bool is_same_document) {
content::RenderFrameHost* subframe =
content::RenderFrameHostTester::For(main_rfh())
->AppendChild("subframe");
return CreateHandle(subframe, committed, is_same_document);
}
// Creates a navigation handle for the given frame.
std::unique_ptr<content::MockNavigationHandle> CreateHandle(
content::RenderFrameHost* frame,
bool committed,
bool is_same_document) {
std::unique_ptr<content::MockNavigationHandle> handle =
std::make_unique<content::MockNavigationHandle>(GURL(), frame);
handle->set_has_committed(committed);
handle->set_is_same_document(is_same_document);
return handle;
}
// The lifetime of the blocking page is managed by the
// SecurityInterstitialTabHelper for the test's web_contents.
// |destroyed_tracker| will be set to true when the corresponding blocking
// page is destroyed.
void CreateAssociatedBlockingPage(content::NavigationHandle* handle,
bool* destroyed_tracker) {
SecurityInterstitialTabHelper::AssociateBlockingPage(
handle, std::make_unique<TestInterstitialPage>(web_contents(), GURL(),
destroyed_tracker));
}
};
// Tests that the helper properly handles the lifetime of a single blocking
// page, interleaved with other navigations.
TEST_F(SecurityInterstitialTabHelperTest, SingleBlockingPage) {
std::unique_ptr<content::MockNavigationHandle> blocking_page_handle =
CreateHandle(true, false);
bool blocking_page_destroyed = false;
CreateAssociatedBlockingPage(blocking_page_handle.get(),
&blocking_page_destroyed);
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
// Test that a same-document navigation doesn't destroy the blocking page if
// its navigation hasn't committed yet.
std::unique_ptr<content::MockNavigationHandle> same_document_handle =
CreateHandle(true, true);
helper->DidFinishNavigation(same_document_handle.get());
EXPECT_FALSE(blocking_page_destroyed);
// Test that a committed (non-same-document) navigation doesn't destroy the
// blocking page if its navigation hasn't committed yet.
std::unique_ptr<content::MockNavigationHandle> committed_handle1 =
CreateHandle(true, false);
helper->DidFinishNavigation(committed_handle1.get());
EXPECT_FALSE(blocking_page_destroyed);
// Simulate comitting the interstitial.
helper->DidFinishNavigation(blocking_page_handle.get());
EXPECT_FALSE(blocking_page_destroyed);
// Test that a subsequent committed navigation releases the blocking page
// stored for the currently committed navigation.
std::unique_ptr<content::MockNavigationHandle> committed_handle2 =
CreateHandle(true, false);
helper->DidFinishNavigation(committed_handle2.get());
EXPECT_TRUE(blocking_page_destroyed);
}
// Tests that the helper properly handles the lifetime of multiple blocking
// pages for a single FrameTreeNode, committed in a different order than they
// are created.
TEST_F(SecurityInterstitialTabHelperTest, MultipleBlockingPages) {
// Simulate associating the first interstitial.
std::unique_ptr<content::MockNavigationHandle> handle1 =
CreateHandle(true, false);
bool blocking_page1_destroyed = false;
CreateAssociatedBlockingPage(handle1.get(), &blocking_page1_destroyed);
// We can directly retrieve the helper for testing once
// CreateAssociatedBlockingPage() was called.
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
// Simulate committing the first interstitial.
helper->DidFinishNavigation(handle1.get());
EXPECT_FALSE(blocking_page1_destroyed);
EXPECT_TRUE(helper->IsDisplayingInterstitial());
// Associate the second interstitial.
std::unique_ptr<content::MockNavigationHandle> handle2 =
CreateHandle(true, false);
bool blocking_page2_destroyed = false;
CreateAssociatedBlockingPage(handle2.get(), &blocking_page2_destroyed);
EXPECT_FALSE(blocking_page1_destroyed);
EXPECT_FALSE(blocking_page2_destroyed);
// Associate the third interstitial.
std::unique_ptr<content::MockNavigationHandle> handle3 =
CreateHandle(true, false);
bool blocking_page3_destroyed = false;
CreateAssociatedBlockingPage(handle3.get(), &blocking_page3_destroyed);
EXPECT_FALSE(blocking_page1_destroyed);
EXPECT_FALSE(blocking_page2_destroyed);
EXPECT_FALSE(blocking_page3_destroyed);
// Simulate committing the third interstitial.
helper->DidFinishNavigation(handle3.get());
EXPECT_TRUE(blocking_page1_destroyed);
EXPECT_FALSE(blocking_page2_destroyed);
EXPECT_FALSE(blocking_page3_destroyed);
// Simulate commiting the second interstitial.
helper->DidFinishNavigation(handle2.get());
EXPECT_TRUE(blocking_page1_destroyed);
EXPECT_FALSE(blocking_page2_destroyed);
EXPECT_TRUE(blocking_page3_destroyed);
// Test that a subsequent committed navigation releases the last blocking
// page.
std::unique_ptr<content::MockNavigationHandle> committed_handle4 =
CreateHandle(true, false);
helper->DidFinishNavigation(committed_handle4.get());
EXPECT_TRUE(blocking_page2_destroyed);
}
// Tests that the helper properly handles a navigation that finishes without
// committing.
TEST_F(SecurityInterstitialTabHelperTest, NavigationDoesNotCommit) {
std::unique_ptr<content::MockNavigationHandle> committed_handle =
CreateHandle(true, false);
bool committed_blocking_page_destroyed = false;
CreateAssociatedBlockingPage(committed_handle.get(),
&committed_blocking_page_destroyed);
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
helper->DidFinishNavigation(committed_handle.get());
EXPECT_FALSE(committed_blocking_page_destroyed);
// Simulate a navigation that does not commit.
std::unique_ptr<content::MockNavigationHandle> non_committed_handle =
CreateHandle(false, false);
helper->DidFinishNavigation(non_committed_handle.get());
// The blocking page for the non-committed navigation should have been cleaned
// up, but the one for the previous committed navigation should still be
// around.
EXPECT_FALSE(committed_blocking_page_destroyed);
// When a navigation does commit, the previous one should be cleaned up.
std::unique_ptr<content::MockNavigationHandle> next_committed_handle =
CreateHandle(true, false);
helper->DidFinishNavigation(next_committed_handle.get());
EXPECT_TRUE(committed_blocking_page_destroyed);
}
// Tests that main frame committed blocking interstitial is cleaned up on tab
// closing.
TEST_F(SecurityInterstitialTabHelperTest, ClosingTabForMainFrameInterstitial) {
std::unique_ptr<content::MockNavigationHandle> committed_handle =
CreateHandle(true, false);
bool committed_blocking_page_destroyed = false;
CreateAssociatedBlockingPage(committed_handle.get(),
&committed_blocking_page_destroyed);
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
helper->DidFinishNavigation(committed_handle.get());
EXPECT_FALSE(committed_blocking_page_destroyed);
// Only subframe interstitials are destroyed on frame deletion.
helper->FrameDeleted(main_rfh()->GetFrameTreeNodeId());
EXPECT_FALSE(committed_blocking_page_destroyed);
helper->WebContentsDestroyed();
EXPECT_TRUE(committed_blocking_page_destroyed);
}
// Tests that multiple FrameTreeNodes can display blocking interstitials.
TEST_F(SecurityInterstitialTabHelperTest, MultipleFramesBlocked) {
// Simulate associating the first interstitial.
std::unique_ptr<content::MockNavigationHandle> handle1 =
CreateSubframeHandle(true, false);
bool blocking_page1_destroyed = false;
CreateAssociatedBlockingPage(handle1.get(), &blocking_page1_destroyed);
// Simulate associating the second interstitial to another frame.
std::unique_ptr<content::MockNavigationHandle> handle2 =
CreateSubframeHandle(true, false);
bool blocking_page2_destroyed = false;
CreateAssociatedBlockingPage(handle2.get(), &blocking_page2_destroyed);
// We can directly retrieve the helper for testing once
// CreateAssociatedBlockingPage() was called.
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
// Simulate committing the first interstitial.
helper->DidFinishNavigation(handle1.get());
EXPECT_FALSE(blocking_page1_destroyed);
EXPECT_FALSE(blocking_page2_destroyed);
// The interstitial at frame 1 moved from pending to commit state.
EXPECT_TRUE(
helper->IsInterstitialCommittedForFrame(handle1->GetFrameTreeNodeId()));
EXPECT_FALSE(
helper->IsInterstitialCommittedForFrame(handle2->GetFrameTreeNodeId()));
EXPECT_FALSE(
helper->IsInterstitialPendingForNavigation(handle1->GetNavigationId()));
EXPECT_TRUE(
helper->IsInterstitialPendingForNavigation(handle2->GetNavigationId()));
// Simulate committing the second interstitial.
helper->DidFinishNavigation(handle2.get());
EXPECT_FALSE(blocking_page1_destroyed);
EXPECT_FALSE(blocking_page2_destroyed);
// The interstitial at frame 2 moved from pending to commit state.
EXPECT_TRUE(
helper->IsInterstitialCommittedForFrame(handle1->GetFrameTreeNodeId()));
EXPECT_TRUE(
helper->IsInterstitialCommittedForFrame(handle2->GetFrameTreeNodeId()));
EXPECT_FALSE(
helper->IsInterstitialPendingForNavigation(handle1->GetNavigationId()));
EXPECT_FALSE(
helper->IsInterstitialPendingForNavigation(handle2->GetNavigationId()));
helper->FrameDeleted(handle1->GetFrameTreeNodeId());
EXPECT_TRUE(blocking_page1_destroyed);
helper->FrameDeleted(handle2->GetFrameTreeNodeId());
EXPECT_TRUE(blocking_page2_destroyed);
}
// Tests that navigation events associated to one FrameTreeNode do not affect
// the pending interstitial for another FrameTreeNode.
TEST_F(SecurityInterstitialTabHelperTest,
NavigationForSubframeWithPendingInterstitial) {
content::RenderFrameHost* subframe1 =
content::RenderFrameHostTester::For(main_rfh())
->AppendChild("subframe 1");
content::RenderFrameHost* subframe2 =
content::RenderFrameHostTester::For(main_rfh())
->AppendChild("subframe 2");
std::unique_ptr<content::MockNavigationHandle> committed_handle =
CreateHandle(subframe1, true, false);
bool committed_blocking_page_destroyed = false;
CreateAssociatedBlockingPage(committed_handle.get(),
&committed_blocking_page_destroyed);
std::unique_ptr<content::MockNavigationHandle> pending_handle =
CreateHandle(subframe2, true, false);
bool pending_blocking_page_destroyed = false;
CreateAssociatedBlockingPage(pending_handle.get(),
&pending_blocking_page_destroyed);
// Simulate committing the interstitial for frame 1.
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
helper->DidFinishNavigation(committed_handle.get());
EXPECT_FALSE(committed_blocking_page_destroyed);
EXPECT_FALSE(pending_blocking_page_destroyed);
// The interstitial at frame 1 moved from pending to commit state.
EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
committed_handle->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
pending_handle->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle->GetNavigationId()));
EXPECT_TRUE(helper->IsInterstitialPendingForNavigation(
pending_handle->GetNavigationId()));
// Simulate a navigation that does not commit.
std::unique_ptr<content::MockNavigationHandle> non_committed_handle =
CreateHandle(subframe1, false, false);
helper->DidFinishNavigation(non_committed_handle.get());
// The non-committed navigation should not affect the state of committed or
// pending blocking pages.
EXPECT_FALSE(committed_blocking_page_destroyed);
EXPECT_FALSE(pending_blocking_page_destroyed);
EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
committed_handle->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
pending_handle->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle->GetNavigationId()));
EXPECT_TRUE(helper->IsInterstitialPendingForNavigation(
pending_handle->GetNavigationId()));
// When a navigation does commit, the previous one should be cleaned up.
std::unique_ptr<content::MockNavigationHandle> next_committed_handle =
CreateHandle(subframe1, true, false);
helper->DidFinishNavigation(next_committed_handle.get());
EXPECT_TRUE(committed_blocking_page_destroyed);
EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
committed_handle->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle->GetNavigationId()));
// The pending interstitial should still be present.
EXPECT_FALSE(pending_blocking_page_destroyed);
EXPECT_TRUE(helper->IsInterstitialPendingForNavigation(
pending_handle->GetNavigationId()));
EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
pending_handle->GetFrameTreeNodeId()));
// Frame deletions only affect committed interstitials.
helper->FrameDeleted(pending_handle->GetFrameTreeNodeId());
EXPECT_FALSE(pending_blocking_page_destroyed);
// Delete pending interstitials via web contents destruction.
DeleteContents();
EXPECT_TRUE(pending_blocking_page_destroyed);
}
// Tests that navigation events associated to one FrameTreeNode do not affect
// the committed interstitial for another FrameTreeNode.
TEST_F(SecurityInterstitialTabHelperTest,
NavigationForSubframeWithCommittedInterstitial) {
content::RenderFrameHost* subframe1 =
content::RenderFrameHostTester::For(main_rfh())
->AppendChild("subframe 1");
content::RenderFrameHost* subframe2 =
content::RenderFrameHostTester::For(main_rfh())
->AppendChild("subframe 2");
std::unique_ptr<content::MockNavigationHandle> committed_handle =
CreateHandle(subframe1, true, false);
bool committed_blocking_page_destroyed = false;
CreateAssociatedBlockingPage(committed_handle.get(),
&committed_blocking_page_destroyed);
std::unique_ptr<content::MockNavigationHandle> committed_handle2 =
CreateHandle(subframe2, true, false);
bool committed_blocking_page2_destroyed = false;
CreateAssociatedBlockingPage(committed_handle2.get(),
&committed_blocking_page2_destroyed);
// Simulate committing the interstitial for frame 1.
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
helper->DidFinishNavigation(committed_handle.get());
EXPECT_FALSE(committed_blocking_page_destroyed);
EXPECT_FALSE(committed_blocking_page2_destroyed);
// Simulate committing the interstitial for frame 2.
helper->DidFinishNavigation(committed_handle2.get());
EXPECT_FALSE(committed_blocking_page_destroyed);
EXPECT_FALSE(committed_blocking_page2_destroyed);
// Both interstitials have moved from pending to commit state.
EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
committed_handle->GetFrameTreeNodeId()));
EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
committed_handle2->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle->GetNavigationId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle2->GetNavigationId()));
// Simulate a navigation that does not commit.
std::unique_ptr<content::MockNavigationHandle> non_committed_handle =
CreateHandle(subframe1, false, false);
helper->DidFinishNavigation(non_committed_handle.get());
// The non-committed navigation should not affect the state of committed or
// pending blocking pages.
EXPECT_FALSE(committed_blocking_page_destroyed);
EXPECT_FALSE(committed_blocking_page2_destroyed);
EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
committed_handle->GetFrameTreeNodeId()));
EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
committed_handle2->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle->GetNavigationId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle2->GetNavigationId()));
// When a navigation does commit, the previous one should be cleaned up.
std::unique_ptr<content::MockNavigationHandle> next_committed_handle =
CreateHandle(subframe1, true, false);
helper->DidFinishNavigation(next_committed_handle.get());
EXPECT_TRUE(committed_blocking_page_destroyed);
EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
committed_handle->GetFrameTreeNodeId()));
EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
committed_handle->GetNavigationId()));
// The pending interstitial should still be present.
EXPECT_FALSE(committed_blocking_page2_destroyed);
EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
committed_handle2->GetFrameTreeNodeId()));
helper->FrameDeleted(committed_handle2->GetFrameTreeNodeId());
EXPECT_TRUE(committed_blocking_page2_destroyed);
}
class SecurityInterstitialTabHelperFencedFrameTest
: public SecurityInterstitialTabHelperTest {
public:
SecurityInterstitialTabHelperFencedFrameTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
}
~SecurityInterstitialTabHelperFencedFrameTest() override = default;
content::RenderFrameHost* CreateFencedFrame(
content::RenderFrameHost* parent) {
content::RenderFrameHost* fenced_frame =
content::RenderFrameHostTester::For(parent)->AppendFencedFrame();
return fenced_frame;
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests that a FencedFrame does not close the interstitial page.
TEST_F(SecurityInterstitialTabHelperFencedFrameTest,
FencedFrameDoesNotCloseInterstitialPage) {
std::unique_ptr<content::NavigationHandle> blocking_page_handle =
CreateHandle(true, false);
bool blocking_page_destroyed = false;
CreateAssociatedBlockingPage(blocking_page_handle.get(),
&blocking_page_destroyed);
SecurityInterstitialTabHelper* helper =
SecurityInterstitialTabHelper::FromWebContents(web_contents());
helper->DidFinishNavigation(blocking_page_handle.get());
EXPECT_FALSE(blocking_page_destroyed);
EXPECT_TRUE(helper->IsDisplayingInterstitial());
// Navigate a fenced frame and the interstitial page should be kept visible on
// the fenced frame.
GURL fenced_frame_url = GURL("https://fencedframe.com");
content::RenderFrameHostTester::For(main_rfh())
->InitializeRenderFrameIfNeeded();
content::RenderFrameHost* fenced_frame_rfh = CreateFencedFrame(main_rfh());
std::unique_ptr<content::NavigationSimulator> navigation_simulator =
content::NavigationSimulator::CreateRendererInitiated(fenced_frame_url,
fenced_frame_rfh);
navigation_simulator->Commit();
fenced_frame_rfh = navigation_simulator->GetFinalRenderFrameHost();
EXPECT_TRUE(fenced_frame_rfh->IsFencedFrameRoot());
EXPECT_FALSE(blocking_page_destroyed);
EXPECT_TRUE(helper->IsDisplayingInterstitial());
// When a navigation does commit, the fenced frame one should be cleaned up.
std::unique_ptr<content::NavigationHandle> next_committed_handle =
CreateHandle(true, false);
helper->DidFinishNavigation(next_committed_handle.get());
EXPECT_TRUE(blocking_page_destroyed);
EXPECT_FALSE(helper->IsDisplayingInterstitial());
}
} // namespace security_interstitials