blob: 5931f1c0f0a9622fc77c427b61f79d02e84c59ae [file] [log] [blame]
// Copyright 2019 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 "chrome/browser/native_file_system/chrome_native_file_system_permission_context.h"
#include <string>
#include <utility>
#include "base/base_paths.h"
#include "base/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/path_service.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/native_file_system/native_file_system_permission_context_factory.h"
#include "chrome/browser/native_file_system/native_file_system_permission_request_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#include "chrome/browser/ui/native_file_system_dialogs.h"
#include "chrome/common/chrome_paths.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/safe_browsing/buildflags.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
namespace {
using HandleType = content::NativeFileSystemPermissionContext::HandleType;
void ShowNativeFileSystemRestrictedDirectoryDialogOnUIThread(
content::GlobalFrameRoutingId frame_id,
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
base::OnceCallback<
void(ChromeNativeFileSystemPermissionContext::SensitiveDirectoryResult)>
callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
if (!rfh || !rfh->IsCurrent()) {
// Requested from a no longer valid render frame host.
std::move(callback).Run(ChromeNativeFileSystemPermissionContext::
SensitiveDirectoryResult::kAbort);
return;
}
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(rfh);
if (!web_contents) {
// Requested from a worker, or a no longer existing tab.
std::move(callback).Run(ChromeNativeFileSystemPermissionContext::
SensitiveDirectoryResult::kAbort);
return;
}
ShowNativeFileSystemRestrictedDirectoryDialog(
origin, path, handle_type, std::move(callback), web_contents);
}
// Sentinel used to indicate that no PathService key is specified for a path in
// the struct below.
constexpr const int kNoBasePathKey = -1;
enum BlockType {
kBlockAllChildren,
kBlockNestedDirectories,
kDontBlockChildren
};
const struct {
// base::BasePathKey value (or one of the platform specific extensions to it)
// for a path that should be blocked. Specify kNoBasePathKey if |path| should
// be used instead.
int base_path_key;
// Explicit path to block instead of using |base_path_key|. Set to nullptr to
// use |base_path_key| on its own. If both |base_path_key| and |path| are set,
// |path| is treated relative to the path |base_path_key| resolves to.
const base::FilePath::CharType* path;
// If this is set to kDontBlockChildren, only the given path and its parents
// are blocked. If this is set to kBlockAllChildren, all children of the given
// path are blocked as well. Finally if this is set to kBlockNestedDirectories
// access is allowed to individual files in the directory, but nested
// directories are still blocked.
// The BlockType of the nearest ancestor of a path to check is what ultimately
// determines if a path is blocked or not. If a blocked path is a descendent
// of another blocked path, then it may override the child-blocking policy of
// its ancestor. For example, if /home blocks all children, but
// /home/downloads does not, then /home/downloads/file.ext will *not* be
// blocked.
BlockType type;
} kBlockedPaths[] = {
// Don't allow users to share their entire home directory, entire desktop or
// entire documents folder, but do allow sharing anything inside those
// directories not otherwise blocked.
{base::DIR_HOME, nullptr, kDontBlockChildren},
{base::DIR_USER_DESKTOP, nullptr, kDontBlockChildren},
{chrome::DIR_USER_DOCUMENTS, nullptr, kDontBlockChildren},
// Similar restrictions for the downloads directory.
{chrome::DIR_DEFAULT_DOWNLOADS, nullptr, kDontBlockChildren},
{chrome::DIR_DEFAULT_DOWNLOADS_SAFE, nullptr, kDontBlockChildren},
// The Chrome installation itself should not be modified by the web.
{chrome::DIR_APP, nullptr, kBlockAllChildren},
// And neither should the configuration of at least the currently running
// Chrome instance (note that this does not take --user-data-dir command
// line overrides into account).
{chrome::DIR_USER_DATA, nullptr, kBlockAllChildren},
// ~/.ssh is pretty sensitive on all platforms, so block access to that.
{base::DIR_HOME, FILE_PATH_LITERAL(".ssh"), kBlockAllChildren},
// And limit access to ~/.gnupg as well.
{base::DIR_HOME, FILE_PATH_LITERAL(".gnupg"), kBlockAllChildren},
#if defined(OS_WIN)
// Some Windows specific directories to block, basically all apps, the
// operating system itself, as well as configuration data for apps.
{base::DIR_PROGRAM_FILES, nullptr, kBlockAllChildren},
{base::DIR_PROGRAM_FILESX86, nullptr, kBlockAllChildren},
{base::DIR_PROGRAM_FILES6432, nullptr, kBlockAllChildren},
{base::DIR_WINDOWS, nullptr, kBlockAllChildren},
{base::DIR_APP_DATA, nullptr, kBlockAllChildren},
{base::DIR_LOCAL_APP_DATA, nullptr, kBlockAllChildren},
{base::DIR_COMMON_APP_DATA, nullptr, kBlockAllChildren},
// Opening a file from an MTP device, such as a smartphone or a camera, is
// implemented by Windows as opening a file in the temporary internet files
// directory. To support that, allow opening files in that directory, but
// not whole directories.
{base::DIR_IE_INTERNET_CACHE, nullptr, kBlockNestedDirectories},
#endif
#if defined(OS_MAC)
// Similar Mac specific blocks.
{base::DIR_APP_DATA, nullptr, kBlockAllChildren},
{base::DIR_HOME, FILE_PATH_LITERAL("Library"), kBlockAllChildren},
#endif
#if defined(OS_LINUX) || defined(OS_CHROMEOS)
// On Linux also block access to devices via /dev, as well as security
// sensitive data in /sys and /proc.
{kNoBasePathKey, FILE_PATH_LITERAL("/dev"), kBlockAllChildren},
{kNoBasePathKey, FILE_PATH_LITERAL("/sys"), kBlockAllChildren},
{kNoBasePathKey, FILE_PATH_LITERAL("/proc"), kBlockAllChildren},
// And block all of ~/.config, matching the similar restrictions on mac
// and windows.
{base::DIR_HOME, FILE_PATH_LITERAL(".config"), kBlockAllChildren},
// Block ~/.dbus as well, just in case, although there probably isn't much a
// website can do with access to that directory and its contents.
{base::DIR_HOME, FILE_PATH_LITERAL(".dbus"), kBlockAllChildren},
#endif
// TODO(https://crbug.com/984641): Refine this list, for example add
// XDG_CONFIG_HOME when it is not set ~/.config?
};
bool ShouldBlockAccessToPath(const base::FilePath& check_path,
HandleType handle_type) {
DCHECK(!check_path.empty());
DCHECK(check_path.IsAbsolute());
base::FilePath nearest_ancestor;
int nearest_ancestor_path_key = kNoBasePathKey;
BlockType nearest_ancestor_block_type = kDontBlockChildren;
for (const auto& block : kBlockedPaths) {
base::FilePath blocked_path;
if (block.base_path_key != kNoBasePathKey) {
if (!base::PathService::Get(block.base_path_key, &blocked_path))
continue;
if (block.path)
blocked_path = blocked_path.Append(block.path);
} else {
DCHECK(block.path);
blocked_path = base::FilePath(block.path);
}
if (check_path == blocked_path || check_path.IsParent(blocked_path)) {
VLOG(1) << "Blocking access to " << check_path
<< " because it is a parent of " << blocked_path << " ("
<< block.base_path_key << ")";
return true;
}
if (blocked_path.IsParent(check_path) &&
(nearest_ancestor.empty() || nearest_ancestor.IsParent(blocked_path))) {
nearest_ancestor = blocked_path;
nearest_ancestor_path_key = block.base_path_key;
nearest_ancestor_block_type = block.type;
}
}
// The path we're checking is not in a potentially blocked directory, or the
// nearest ancestor does not block access to its children. Grant access.
if (nearest_ancestor.empty() ||
nearest_ancestor_block_type == kDontBlockChildren) {
return false;
}
// The path we're checking is a file, and the nearest ancestor only blocks
// access to directories. Grant access.
if (handle_type == HandleType::kFile &&
nearest_ancestor_block_type == kBlockNestedDirectories) {
return false;
}
// The nearest ancestor blocks access to its children, so block access.
VLOG(1) << "Blocking access to " << check_path << " because it is inside "
<< nearest_ancestor << " (" << nearest_ancestor_path_key << ")";
return true;
}
// Returns a callback that calls the passed in |callback| by posting a task to
// the current sequenced task runner.
template <typename... ResultTypes>
base::OnceCallback<void(ResultTypes... results)>
BindResultCallbackToCurrentSequence(
base::OnceCallback<void(ResultTypes... results)> callback) {
return base::BindOnce(
[](scoped_refptr<base::TaskRunner> task_runner,
base::OnceCallback<void(ResultTypes... results)> callback,
ResultTypes... results) {
task_runner->PostTask(FROM_HERE,
base::BindOnce(std::move(callback), results...));
},
base::SequencedTaskRunnerHandle::Get(), std::move(callback));
}
void DoSafeBrowsingCheckOnUIThread(
content::GlobalFrameRoutingId frame_id,
std::unique_ptr<content::NativeFileSystemWriteItem> item,
safe_browsing::CheckDownloadCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Download Protection Service is not supported on Android.
#if BUILDFLAG(FULL_SAFE_BROWSING)
safe_browsing::SafeBrowsingService* sb_service =
g_browser_process->safe_browsing_service();
if (!sb_service || !sb_service->download_protection_service() ||
!sb_service->download_protection_service()->enabled()) {
std::move(callback).Run(safe_browsing::DownloadCheckResult::UNKNOWN);
return;
}
if (!item->browser_context) {
content::RenderProcessHost* rph =
content::RenderProcessHost::FromID(frame_id.child_id);
if (!rph) {
std::move(callback).Run(safe_browsing::DownloadCheckResult::UNKNOWN);
return;
}
item->browser_context = rph->GetBrowserContext();
}
if (!item->web_contents) {
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
if (rfh)
item->web_contents = content::WebContents::FromRenderFrameHost(rfh);
}
sb_service->download_protection_service()->CheckNativeFileSystemWrite(
std::move(item), std::move(callback));
#endif
}
ChromeNativeFileSystemPermissionContext::AfterWriteCheckResult
InterpretSafeBrowsingResult(safe_browsing::DownloadCheckResult result) {
using Result = safe_browsing::DownloadCheckResult;
switch (result) {
// Only allow downloads that are marked as SAFE or UNKNOWN by SafeBrowsing.
// All other types are going to be blocked. UNKNOWN could be the result of a
// failed safe browsing ping.
case Result::UNKNOWN:
case Result::SAFE:
case Result::WHITELISTED_BY_POLICY:
return ChromeNativeFileSystemPermissionContext::AfterWriteCheckResult::
kAllow;
case Result::DANGEROUS:
case Result::UNCOMMON:
case Result::DANGEROUS_HOST:
case Result::POTENTIALLY_UNWANTED:
case Result::BLOCKED_PASSWORD_PROTECTED:
case Result::BLOCKED_TOO_LARGE:
case Result::BLOCKED_UNSUPPORTED_FILE_TYPE:
return ChromeNativeFileSystemPermissionContext::AfterWriteCheckResult::
kBlock;
// This shouldn't be returned for Native File System write checks.
case Result::ASYNC_SCANNING:
case Result::SENSITIVE_CONTENT_WARNING:
case Result::SENSITIVE_CONTENT_BLOCK:
case Result::DEEP_SCANNED_SAFE:
case Result::PROMPT_FOR_SCANNING:
NOTREACHED();
return ChromeNativeFileSystemPermissionContext::AfterWriteCheckResult::
kAllow;
}
NOTREACHED();
return ChromeNativeFileSystemPermissionContext::AfterWriteCheckResult::kBlock;
}
} // namespace
ChromeNativeFileSystemPermissionContext::Grants::Grants() = default;
ChromeNativeFileSystemPermissionContext::Grants::~Grants() = default;
ChromeNativeFileSystemPermissionContext::Grants::Grants(Grants&&) = default;
ChromeNativeFileSystemPermissionContext::Grants&
ChromeNativeFileSystemPermissionContext::Grants::operator=(Grants&&) = default;
ChromeNativeFileSystemPermissionContext::
ChromeNativeFileSystemPermissionContext(content::BrowserContext* context) {
DETACH_FROM_SEQUENCE(sequence_checker_);
auto* profile = Profile::FromBrowserContext(context);
content_settings_ = base::WrapRefCounted(
HostContentSettingsMapFactory::GetForProfile(profile));
}
ChromeNativeFileSystemPermissionContext::
~ChromeNativeFileSystemPermissionContext() = default;
ContentSetting
ChromeNativeFileSystemPermissionContext::GetWriteGuardContentSetting(
const url::Origin& origin) {
return content_settings()->GetContentSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_WRITE_GUARD,
/*provider_id=*/std::string());
}
ContentSetting
ChromeNativeFileSystemPermissionContext::GetReadGuardContentSetting(
const url::Origin& origin) {
return content_settings()->GetContentSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::FILE_SYSTEM_READ_GUARD,
/*provider_id=*/std::string());
}
bool ChromeNativeFileSystemPermissionContext::CanObtainReadPermission(
const url::Origin& origin) {
return GetReadGuardContentSetting(origin) == CONTENT_SETTING_ASK ||
GetReadGuardContentSetting(origin) == CONTENT_SETTING_ALLOW;
}
bool ChromeNativeFileSystemPermissionContext::CanObtainWritePermission(
const url::Origin& origin) {
return GetWriteGuardContentSetting(origin) == CONTENT_SETTING_ASK ||
GetWriteGuardContentSetting(origin) == CONTENT_SETTING_ALLOW;
}
void ChromeNativeFileSystemPermissionContext::ConfirmSensitiveDirectoryAccess(
const url::Origin& origin,
const std::vector<base::FilePath>& paths,
HandleType handle_type,
content::GlobalFrameRoutingId frame_id,
base::OnceCallback<void(SensitiveDirectoryResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (paths.empty()) {
std::move(callback).Run(SensitiveDirectoryResult::kAllowed);
return;
}
// It is enough to only verify access to the first path, as multiple
// file selection is only supported if all files are in the same
// directory.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&ShouldBlockAccessToPath, paths[0], handle_type),
base::BindOnce(&ChromeNativeFileSystemPermissionContext::
DidConfirmSensitiveDirectoryAccess,
GetWeakPtr(), origin, paths, handle_type, frame_id,
std::move(callback)));
}
void ChromeNativeFileSystemPermissionContext::PerformAfterWriteChecks(
std::unique_ptr<content::NativeFileSystemWriteItem> item,
content::GlobalFrameRoutingId frame_id,
base::OnceCallback<void(AfterWriteCheckResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&DoSafeBrowsingCheckOnUIThread, frame_id, std::move(item),
base::BindOnce(
[](scoped_refptr<base::TaskRunner> task_runner,
base::OnceCallback<void(AfterWriteCheckResult result)>
callback,
safe_browsing::DownloadCheckResult result) {
task_runner->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
InterpretSafeBrowsingResult(result)));
},
base::SequencedTaskRunnerHandle::Get(), std::move(callback))));
}
void ChromeNativeFileSystemPermissionContext::
DidConfirmSensitiveDirectoryAccess(
const url::Origin& origin,
const std::vector<base::FilePath>& paths,
HandleType handle_type,
content::GlobalFrameRoutingId frame_id,
base::OnceCallback<void(SensitiveDirectoryResult)> callback,
bool should_block) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!should_block) {
std::move(callback).Run(SensitiveDirectoryResult::kAllowed);
return;
}
auto result_callback =
BindResultCallbackToCurrentSequence(std::move(callback));
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&ShowNativeFileSystemRestrictedDirectoryDialogOnUIThread,
frame_id, origin, paths[0], handle_type,
std::move(result_callback)));
}
bool ChromeNativeFileSystemPermissionContext::OriginHasReadAccess(
const url::Origin& origin) {
NOTREACHED();
return false;
}
bool ChromeNativeFileSystemPermissionContext::OriginHasWriteAccess(
const url::Origin& origin) {
NOTREACHED();
return false;
}