Skip to content

Commit

Permalink
[Extensions] Verify document ID during pageCapture.saveAsMHTML
Browse files Browse the repository at this point in the history
This CL adds logic into pageCapture.saveAsMHTML to keep track of the
document ID for the page to make sure the page didn't navigate between
the time access was checked and the page content is saved. If a
navigation did occur in that time and the page changed, a new error is
emitted.

Adds a new unittest to exercise this case.

Bug: 1494490
Change-Id: Ib084388653fc4d3609a1a0bba36f6ec2ac0da0da
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4985374
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Tim <tjudkins@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1218515}
  • Loading branch information
Tim Judkins authored and Chromium LUCI CQ committed Nov 1, 2023
1 parent 7c4aaa0 commit 9ee850d
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 14 deletions.
44 changes: 31 additions & 13 deletions chrome/browser/extensions/api/page_capture/page_capture_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
#include "content/public/browser/child_process_security_policy.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/mhtml_generation_params.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_util.h"
#include "extensions/common/permissions/permissions_data.h"
#include "url/origin.h"

using content::BrowserThread;
using content::ChildProcessSecurityPolicy;
Expand All @@ -36,6 +38,8 @@ const char kFileTooBigError[] = "The MHTML file generated is too big.";
const char kMHTMLGenerationFailedError[] = "Failed to generate MHTML.";
const char kTemporaryFileError[] = "Failed to create a temporary file.";
const char kTabClosedError[] = "Cannot find the tab for this request.";
const char kTabNavigatedError[] =
"Tab navigated before capture could complete.";
const char kPageCaptureNotAllowed[] =
"Don't have permissions required to capture this page.";
constexpr base::TaskTraits kCreateTemporaryFileTaskTraits = {
Expand Down Expand Up @@ -75,10 +79,19 @@ ExtensionFunction::ResponseAction PageCaptureSaveAsMHTMLFunction::Run() {
params_ = SaveAsMHTML::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params_);

WebContents* web_contents = GetWebContents();
if (!web_contents) {
return RespondNow(Error(kTabClosedError));
}

std::string error;
if (!CanCaptureCurrentPage(&error)) {
if (!CanCaptureCurrentPage(*web_contents, &error)) {
return RespondNow(Error(std::move(error)));
}
// Store the document ID for the WebContents to check it hasn't changed by the
// time we do the capture.
document_id_ = ExtensionApiFrameIdMap::GetDocumentId(
web_contents->GetPrimaryMainFrame());

base::ThreadPool::PostTask(
FROM_HERE, kCreateTemporaryFileTaskTraits,
Expand All @@ -87,26 +100,26 @@ ExtensionFunction::ResponseAction PageCaptureSaveAsMHTMLFunction::Run() {
return RespondLater();
}

bool PageCaptureSaveAsMHTMLFunction::CanCaptureCurrentPage(std::string* error) {
WebContents* web_contents = GetWebContents();
if (!web_contents) {
*error = kTabClosedError;
return false;
}
const GURL& url = web_contents->GetLastCommittedURL();
const GURL origin_url = url::Origin::Create(url).GetURL();
bool PageCaptureSaveAsMHTMLFunction::CanCaptureCurrentPage(
WebContents& web_contents,
std::string* error) {
const url::Origin& origin =
web_contents.GetPrimaryMainFrame()->GetLastCommittedOrigin();
bool can_capture_page = false;
if (origin_url.SchemeIs(url::kFileScheme)) {
if (origin.scheme() == url::kFileScheme) {
// We special case file schemes, since we don't check for URL permissions
// in CanCaptureVisiblePage() with the pageCapture API. This ensures
// file:// URLs are only capturable with the proper permission.
can_capture_page = extensions::util::AllowFileAccess(
extension()->id(), web_contents->GetBrowserContext());
extension()->id(), web_contents.GetBrowserContext());
} else {
std::string unused_error;
// TODO(tjudkins): We should change CanCaptureVisiblePage to take the
// url::Origin directly, as it converts the GURL to an origin itself anyway.
can_capture_page = extension()->permissions_data()->CanCaptureVisiblePage(
url, sessions::SessionTabHelper::IdForTab(web_contents).id(),
&unused_error, extensions::CaptureRequirement::kPageCapture);
origin.GetURL(),
sessions::SessionTabHelper::IdForTab(&web_contents).id(), &unused_error,
extensions::CaptureRequirement::kPageCapture);
}

if (!can_capture_page) {
Expand Down Expand Up @@ -174,6 +187,11 @@ void PageCaptureSaveAsMHTMLFunction::TemporaryFileCreatedOnUI(bool success) {
ReturnFailure(kTabClosedError);
return;
}
if (document_id_ != ExtensionApiFrameIdMap::GetDocumentId(
web_contents->GetPrimaryMainFrame())) {
ReturnFailure(kTabNavigatedError);
return;
}

web_contents->GenerateMHTML(
content::MHTMLGenerationParams(mhtml_path_),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "base/memory/ref_counted.h"
#include "build/chromeos_buildflags.h"
#include "chrome/common/extensions/api/page_capture.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_function.h"
#include "storage/browser/blob/shareable_file_reference.h"

Expand Down Expand Up @@ -47,7 +48,8 @@ class PageCaptureSaveAsMHTMLFunction : public ExtensionFunction {

// Returns whether or not the extension has permission to capture the current
// page. Sets |*error| to an error value on failure.
bool CanCaptureCurrentPage(std::string* error);
bool CanCaptureCurrentPage(content::WebContents& web_contents,
std::string* error);

// Called on the file thread.
void CreateTemporaryFile();
Expand All @@ -65,6 +67,10 @@ class PageCaptureSaveAsMHTMLFunction : public ExtensionFunction {
// Returns the WebContents we are associated with, NULL if it's been closed.
content::WebContents* GetWebContents();

// The document ID for the page being captured. Used to check that the page
// hasn't navigated before the capture completes.
ExtensionApiFrameIdMap::DocumentId document_id_;

absl::optional<extensions::api::page_capture::SaveAsMHTML::Params> params_;

// The path to the temporary file containing the MHTML data.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/extensions/api/page_capture/page_capture_api.h"

#include <memory>

#include "base/test/values_test_util.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/test_browser_window.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "url/gurl.h"

namespace extensions {

class PageCaptureApiUnitTest : public ExtensionServiceTestBase {
protected:
void SetUp() override {
ExtensionServiceTestBase::SetUp();
InitializeEmptyExtensionService();

browser_window_ = std::make_unique<TestBrowserWindow>();
Browser::CreateParams params(profile(), true);
params.type = Browser::TYPE_NORMAL;
params.window = browser_window_.get();
browser_ = std::unique_ptr<Browser>(Browser::Create(params));
}

void TearDown() override {
browser_.reset();
browser_window_.reset();
ExtensionServiceTestBase::TearDown();
}
Browser* browser() { return browser_.get(); }

private:
std::unique_ptr<TestBrowserWindow> browser_window_;
std::unique_ptr<Browser> browser_;
};

// Tests that if a page navigates during a call to pageCature.saveAsMHTML(), the
// API call will result in an error.
TEST_F(PageCaptureApiUnitTest, PageNavigationDuringSaveAsMHTML) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("Page Capture").AddPermission("pageCapture").Build();
auto function = base::MakeRefCounted<PageCaptureSaveAsMHTMLFunction>();
function->set_extension(extension.get());

// Add a visible tab.
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
content::WebContentsTester* web_contents_tester =
content::WebContentsTester::For(web_contents.get());
browser()->tab_strip_model()->AppendWebContents(std::move(web_contents),
/*foreground=*/true);
web_contents_tester->NavigateAndCommit(GURL("https://www.google.com"));
const int tab_id = ExtensionTabUtil::GetTabId(
browser()->tab_strip_model()->GetWebContentsAt(0));

// To capture the page as MHTML, the extension function needs to hop from the
// UI thread to the IO thread to create the temporary file, then back to the
// UI thread to actually save the page contents. Since the URL access is only
// checked initially and a navigation could happen during the thread hops, the
// extension function should result in an error if a navigation happens
// between the initial check and the actual capture. To simulate this we start
// the extension function running, then trigger a synchronous navigation
// using the WebContentsTester immediately which will happen before the
// messaging between threads finishes.
// Regression test for crbug.com/1494490
function->SetBrowserContextForTesting(profile());
function->SetArgs(
base::Value::List().Append(base::Value::Dict().Set("tabId", tab_id)));
api_test_utils::SendResponseHelper response_helper(function.get());
function->RunWithValidation().Execute();

web_contents_tester->NavigateAndCommit(GURL("https://www.chromium.org"));
response_helper.WaitForResponse();
const base::Value::List* results = function->GetResultListForTest();
ASSERT_TRUE(results);
EXPECT_TRUE(results->empty()) << "Did not expect a result";
CHECK(function->response_type());
EXPECT_EQ(ExtensionFunction::FAILED, *function->response_type());
EXPECT_EQ("Tab navigated before capture could complete.",
function->GetError());

// Clean up.
browser()->tab_strip_model()->CloseAllTabs();
}

} // namespace extensions
1 change: 1 addition & 0 deletions chrome/test/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -8768,6 +8768,7 @@ test("unit_tests") {
"../browser/extensions/api/notifications/extension_notification_handler_unittest.cc",
"../browser/extensions/api/omnibox/omnibox_unittest.cc",
"../browser/extensions/api/omnibox/suggestion_parser_unittest.cc",
"../browser/extensions/api/page_capture/page_capture_api_unittest.cc",
"../browser/extensions/api/passwords_private/password_access_auth_timeout_handler_unittest.cc",
"../browser/extensions/api/passwords_private/password_check_delegate_unittest.cc",
"../browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc",
Expand Down

0 comments on commit 9ee850d

Please sign in to comment.