blob: 9c7e419be8978ff05d318ff140fd6d943f1e2389 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/file_system_access/safe_move_helper.h"
#include "base/files/file.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/thread_annotations.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "components/services/quarantine/quarantine.h"
#include "content/browser/file_system_access/file_system_access_error.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_client.h"
#include "crypto/secure_hash.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "storage/browser/file_system/copy_or_move_hook_delegate.h"
#include "storage/browser/file_system/file_stream_reader.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_operation.h"
#include "storage/browser/file_system/file_system_operation_runner.h"
#include "storage/common/file_system/file_system_util.h"
namespace content {
namespace {
// For after write checks we need the hash and size of the file. That data is
// calculated on the IO thread by this class.
// This class is ref-counted to make it easier to integrate with the
// FileStreamReader API where methods either return synchronously or invoke
// their callback asynchronously.
class HashCalculator : public base::RefCounted<HashCalculator> {
public:
// Must be called on the FileSystemContext's IO runner.
static void CreateAndStart(scoped_refptr<storage::FileSystemContext> context,
SafeMoveHelper::HashCallback callback,
const storage::FileSystemURL& source_url,
storage::FileSystemOperationRunner*) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
auto calculator = base::MakeRefCounted<HashCalculator>(std::move(context),
std::move(callback));
calculator->Start(source_url);
}
HashCalculator(scoped_refptr<storage::FileSystemContext> context,
SafeMoveHelper::HashCallback callback)
: context_(std::move(context)), callback_(std::move(callback)) {
DCHECK(context_);
}
private:
friend class base::RefCounted<HashCalculator>;
~HashCalculator() = default;
void Start(const storage::FileSystemURL& source_url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
reader_ = context_->CreateFileStreamReader(
source_url, 0, storage::kMaximumLength, base::Time());
int64_t length =
reader_->GetLength(base::BindOnce(&HashCalculator::GotLength, this));
if (length == net::ERR_IO_PENDING)
return;
GotLength(length);
}
void GotLength(int64_t length) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (length < 0) {
std::move(callback_).Run(storage::NetErrorToFileError(length),
std::string(), -1);
return;
}
file_size_ = length;
ReadMore();
}
void ReadMore() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_GE(file_size_, 0);
int read_result =
reader_->Read(buffer_.get(), buffer_->size(),
base::BindOnce(&HashCalculator::DidRead, this));
if (read_result == net::ERR_IO_PENDING)
return;
DidRead(read_result);
}
void DidRead(int bytes_read) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_GE(file_size_, 0);
if (bytes_read < 0) {
std::move(callback_).Run(storage::NetErrorToFileError(bytes_read),
std::string(), -1);
return;
}
if (bytes_read == 0) {
std::string hash_str(hash_->GetHashLength(), 0);
hash_->Finish(std::data(hash_str), hash_str.size());
std::move(callback_).Run(base::File::FILE_OK, hash_str, file_size_);
return;
}
hash_->Update(buffer_->data(), bytes_read);
ReadMore();
}
SEQUENCE_CHECKER(sequence_checker_);
const scoped_refptr<storage::FileSystemContext> context_;
SafeMoveHelper::HashCallback callback_ GUARDED_BY_CONTEXT(sequence_checker_);
const scoped_refptr<net::IOBufferWithSize> buffer_{
base::MakeRefCounted<net::IOBufferWithSize>(8 * 1024)};
const std::unique_ptr<crypto::SecureHash> hash_{
crypto::SecureHash::Create(crypto::SecureHash::SHA256)};
std::unique_ptr<storage::FileStreamReader> reader_
GUARDED_BY_CONTEXT(sequence_checker_);
int64_t file_size_ GUARDED_BY_CONTEXT(sequence_checker_) = -1;
};
} // namespace
SafeMoveHelper::SafeMoveHelper(
base::WeakPtr<FileSystemAccessManagerImpl> manager,
const FileSystemAccessManagerImpl::BindingContext& context,
const storage::FileSystemURL& source_url,
const storage::FileSystemURL& dest_url,
storage::FileSystemOperation::CopyOrMoveOptionSet options,
download::QuarantineConnectionCallback quarantine_connection_callback,
bool has_transient_user_activation)
: manager_(std::move(manager)),
context_(context),
source_url_(source_url),
dest_url_(dest_url),
options_(options),
quarantine_connection_callback_(
std::move(quarantine_connection_callback)),
has_transient_user_activation_(has_transient_user_activation) {}
SafeMoveHelper::~SafeMoveHelper() = default;
void SafeMoveHelper::Start(SafeMoveHelperCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
callback_ = std::move(callback);
if (!manager_) {
std::move(callback_).Run(file_system_access_error::FromStatus(
blink::mojom::FileSystemAccessStatus::kOperationAborted));
return;
}
if (!RequireSecurityChecks() || !manager_->permission_context()) {
DidAfterWriteCheck(
FileSystemAccessPermissionContext::AfterWriteCheckResult::kAllow);
return;
}
ComputeHashForSourceFile(base::BindOnce(&SafeMoveHelper::DoAfterWriteCheck,
weak_factory_.GetWeakPtr()));
}
void SafeMoveHelper::ComputeHashForSourceFile(HashCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!manager_) {
std::move(callback_).Run(file_system_access_error::FromStatus(
blink::mojom::FileSystemAccessStatus::kOperationAborted));
return;
}
auto wrapped_callback = base::BindPostTask(
base::SequencedTaskRunnerHandle::Get(), std::move(callback));
manager_->operation_runner().PostTaskWithThisObject(
base::BindOnce(&HashCalculator::CreateAndStart,
base::WrapRefCounted(manager_->context()),
std::move(wrapped_callback), source_url()));
}
void SafeMoveHelper::DoAfterWriteCheck(base::File::Error hash_result,
const std::string& hash,
int64_t size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (hash_result != base::File::FILE_OK) {
// Calculating the hash failed.
std::move(callback_).Run(file_system_access_error::FromStatus(
blink::mojom::FileSystemAccessStatus::kOperationAborted,
"Failed to perform Safe Browsing check."));
return;
}
if (!manager_) {
std::move(callback_).Run(file_system_access_error::FromStatus(
blink::mojom::FileSystemAccessStatus::kOperationAborted));
return;
}
content::GlobalRenderFrameHostId outermost_main_frame_id;
auto* rfh = content::RenderFrameHost::FromID(context_.frame_id);
if (rfh)
outermost_main_frame_id = rfh->GetOutermostMainFrame()->GetGlobalId();
auto item = std::make_unique<FileSystemAccessWriteItem>();
item->target_file_path = dest_url().path();
item->full_path = source_url().path();
item->sha256_hash = hash;
item->size = size;
item->frame_url = context_.url;
item->outermost_main_frame_id = outermost_main_frame_id;
item->has_user_gesture = has_transient_user_activation_;
manager_->permission_context()->PerformAfterWriteChecks(
std::move(item), context_.frame_id,
base::BindOnce(&SafeMoveHelper::DidAfterWriteCheck,
weak_factory_.GetWeakPtr()));
}
void SafeMoveHelper::DidAfterWriteCheck(
FileSystemAccessPermissionContext::AfterWriteCheckResult result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (result !=
FileSystemAccessPermissionContext::AfterWriteCheckResult::kAllow) {
// Safe browsing check failed.
std::move(callback_).Run(file_system_access_error::FromStatus(
blink::mojom::FileSystemAccessStatus::kOperationAborted,
"Blocked by Safe Browsing."));
return;
}
if (!manager_) {
std::move(callback_).Run(file_system_access_error::FromStatus(
blink::mojom::FileSystemAccessStatus::kOperationAborted));
return;
}
// If the move operation succeeds, the path pointing to the source file will
// not exist anymore. In case of error, the source file URL will point to a
// valid filesystem location.
base::OnceCallback<void(base::File::Error)> result_callback;
if (RequireSecurityChecks()) {
GURL referrer_url = manager_->is_off_the_record() ? GURL() : context_.url;
mojo::Remote<quarantine::mojom::Quarantine> quarantine_remote;
if (quarantine_connection_callback_) {
quarantine_connection_callback_.Run(
quarantine_remote.BindNewPipeAndPassReceiver());
}
result_callback = base::BindOnce(
&SafeMoveHelper::DidFileDoQuarantine, weak_factory_.GetWeakPtr(),
dest_url(), referrer_url, std::move(quarantine_remote));
} else {
result_callback = base::BindOnce(&SafeMoveHelper::DidFileSkipQuarantine,
weak_factory_.GetWeakPtr());
}
manager_->DoFileSystemOperation(
FROM_HERE, &storage::FileSystemOperationRunner::Move,
std::move(result_callback), source_url(), dest_url(), options_,
storage::FileSystemOperationRunner::ErrorBehavior::ERROR_BEHAVIOR_ABORT,
std::make_unique<storage::CopyOrMoveHookDelegate>());
}
void SafeMoveHelper::DidFileSkipQuarantine(base::File::Error result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(callback_).Run(file_system_access_error::FromFileError(result));
}
void SafeMoveHelper::DidFileDoQuarantine(
const storage::FileSystemURL& target_url,
const GURL& referrer_url,
mojo::Remote<quarantine::mojom::Quarantine> quarantine_remote,
base::File::Error result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (result != base::File::FILE_OK) {
DLOG(ERROR) << "Move operation failed source: " << source_url().path()
<< " dest: " << target_url.path()
<< " error: " << base::File::ErrorToString(result);
std::move(callback_).Run(file_system_access_error::FromFileError(result));
return;
}
// The quarantine service operates on files identified by a base::FilePath. As
// such we can only quarantine files that are actual local files.
// On ChromeOS on the other hand anything that isn't in the sandboxed file
// system is also uniquely identifiable by its FileSystemURL::path(), and
// thus we accept all other FileSystemURL types.
#if BUILDFLAG(IS_CHROMEOS_ASH)
DCHECK(target_url.type() != storage::kFileSystemTypeTemporary &&
target_url.type() != storage::kFileSystemTypePersistent)
<< target_url.type();
#else
DCHECK(target_url.type() == storage::kFileSystemTypeLocal ||
target_url.type() == storage::kFileSystemTypeTest)
<< target_url.type();
#endif
GURL authority_url =
referrer_url.is_valid() && referrer_url.SchemeIsHTTPOrHTTPS()
? referrer_url
: GURL();
if (quarantine_remote) {
quarantine::mojom::Quarantine* raw_quarantine = quarantine_remote.get();
raw_quarantine->QuarantineFile(
target_url.path(), authority_url, referrer_url,
GetContentClient()
->browser()
->GetApplicationClientGUIDForQuarantineCheck(),
mojo::WrapCallbackWithDefaultInvokeIfNotRun(
base::BindOnce(&SafeMoveHelper::DidAnnotateFile,
weak_factory_.GetWeakPtr(),
std::move(quarantine_remote)),
quarantine::mojom::QuarantineFileResult::ANNOTATION_FAILED));
} else {
#if BUILDFLAG(IS_WIN)
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&quarantine::SetInternetZoneIdentifierDirectly,
target_url.path(), authority_url, referrer_url),
base::BindOnce(&SafeMoveHelper::DidAnnotateFile,
weak_factory_.GetWeakPtr(),
std::move(quarantine_remote)));
#else
DidAnnotateFile(std::move(quarantine_remote),
quarantine::mojom::QuarantineFileResult::ANNOTATION_FAILED);
#endif
}
}
void SafeMoveHelper::DidAnnotateFile(
mojo::Remote<quarantine::mojom::Quarantine> quarantine_remote,
quarantine::mojom::QuarantineFileResult result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (result != quarantine::mojom::QuarantineFileResult::OK &&
result != quarantine::mojom::QuarantineFileResult::ANNOTATION_FAILED) {
// If malware was detected, or the file referrer was blocked by policy, the
// file will be deleted at this point by AttachmentServices on Windows.
// There is nothing to do except to return the error message to the
// application.
std::move(callback_).Run(file_system_access_error::FromStatus(
blink::mojom::FileSystemAccessStatus::kOperationAborted,
"Aborted due to security policy."));
return;
}
std::move(callback_).Run(file_system_access_error::Ok());
}
} // namespace content