blob: 743b37c2007963557ee43021010935ed85e98839 [file] [log] [blame]
// 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 "content/browser/shared_storage/shared_storage_header_observer.h"
#include <deque>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "components/services/storage/shared_storage/shared_storage_manager.h"
#include "content/browser/bad_message.h"
#include "content/browser/navigation_or_document_handle.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/shared_storage/shared_storage_event_params.h"
#include "content/browser/shared_storage/shared_storage_worklet_host_manager.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_client.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/mojom/optional_bool.mojom.h"
#include "third_party/blink/public/common/shared_storage/shared_storage_utils.h"
namespace content {
namespace {
using OperationPtr = network::mojom::SharedStorageOperationPtr;
using ContextType = StoragePartitionImpl::ContextType;
bool IsSharedStorageAllowedByPermissionsPolicy(
SharedStorageHeaderObserver::PermissionsPolicyDoubleCheckStatus
permissions_policy_status) {
return permissions_policy_status ==
SharedStorageHeaderObserver::PermissionsPolicyDoubleCheckStatus::
kNavigationSourceNoPolicy ||
permissions_policy_status ==
SharedStorageHeaderObserver::PermissionsPolicyDoubleCheckStatus::
kEnabled;
}
int GetMainFrameIdFromRFH(RenderFrameHost* rfh) {
return static_cast<RenderFrameHostImpl*>(rfh->GetOutermostMainFrame())
->GetFrameTreeNodeId();
}
int GetMainFrameIdFromNavigationOrDocumentHandle(
NavigationOrDocumentHandle* navigation_or_document_handle) {
if (auto* navigation_request =
navigation_or_document_handle->GetNavigationRequest()) {
return GetMainFrameIdFromRFH(
navigation_request->frame_tree_node()->current_frame_host());
}
if (auto* rfh = navigation_or_document_handle->GetDocument()) {
return GetMainFrameIdFromRFH(rfh);
}
return FrameTreeNode::kFrameTreeNodeInvalidId;
}
} // namespace
SharedStorageHeaderObserver::SharedStorageHeaderObserver(
StoragePartitionImpl* storage_partition)
: storage_partition_(storage_partition) {}
SharedStorageHeaderObserver::~SharedStorageHeaderObserver() = default;
void SharedStorageHeaderObserver::HeaderReceived(
const url::Origin& request_origin,
ContextType context_type,
NavigationOrDocumentHandle* navigation_or_document_handle,
std::vector<OperationPtr> operations,
base::OnceClosure callback,
mojo::ReportBadMessageCallback bad_message_callback,
bool can_defer) {
DCHECK(callback);
DCHECK(bad_message_callback);
if (!network::IsOriginPotentiallyTrustworthy(request_origin)) {
// This could indicate a compromised renderer or network service, since we
// already required a secure context in any previous checks. Terminate the
// network service.
std::move(bad_message_callback)
.Run(
"Shared-Storage-Write is only available for origins deemed "
"potentially trustworthy.");
if (context_type == ContextType::kRenderFrameHostContext) {
// The request was from a subresource source, so we should also terminate
// the renderer.
bad_message::ReceivedBadMessage(
navigation_or_document_handle->GetDocument()->GetProcess(),
bad_message::BadMessageReason::
SSHO_RECEIVED_SHARED_STORAGE_WRITE_HEADER_FROM_UNTRUSTWORTHY_ORIGIN);
}
return;
}
if (request_origin.opaque()) {
// This could indicate a compromised renderer or network service, since we
// already required a non-opaque origin in any previous checks. Terminate
// the network service.
std::move(bad_message_callback)
.Run("Shared-Storage-Write is not available for opaque origins.");
if (context_type == ContextType::kRenderFrameHostContext) {
// The request was from a subresource source, so we should also terminate
// the renderer.
bad_message::ReceivedBadMessage(
navigation_or_document_handle->GetDocument()->GetProcess(),
bad_message::BadMessageReason::
SSHO_RECEIVED_SHARED_STORAGE_WRITE_HEADER_FROM_OPAQUE_ORIGIN);
}
return;
}
PermissionsPolicyDoubleCheckStatus permissions_policy_status =
DoPermissionsPolicyDoubleCheck(request_origin, context_type,
navigation_or_document_handle);
base::UmaHistogramEnumeration(
"Storage.SharedStorage.HeaderObserver.PermissionsPolicyDoubleCheckStatus",
permissions_policy_status);
switch (permissions_policy_status) {
case PermissionsPolicyDoubleCheckStatus::kDisabled: {
// Since we have previously checked `PermissionsPolicy` and the feature
// was said to be enabled then, the discrepancy may indicate a compromised
// renderer or network service. Terminate the network service.
std::move(bad_message_callback)
.Run(
"Permissions-Policy denied permission to Shared-Storage-Write "
"operations.");
if (context_type == ContextType::kRenderFrameHostContext) {
// The request was from a subresource source, so we should also
// terminate the renderer.
bad_message::ReceivedBadMessage(
navigation_or_document_handle->GetDocument()->GetProcess(),
bad_message::BadMessageReason::
SSHO_RECEIVED_SHARED_STORAGE_WRITE_HEADER_WITH_PERMISSION_DISABLED);
}
return;
}
case PermissionsPolicyDoubleCheckStatus::kDisallowedMainFrameNavigation: {
// This could indicate a compromised network service, as we previously
// marked any main frame navigations as ineligible for shared storage in
// `NavigationRequest` before sending information to the network service.
// Terminate the network service.
std::move(bad_message_callback)
.Run(
"Shared-Storage-Write is not available for main frame "
"navigations.");
return;
}
case PermissionsPolicyDoubleCheckStatus::kSubresourceSourceDefer: {
auto* rfh = navigation_or_document_handle->GetDocument();
if (rfh && can_defer) {
// We are in a case where the double check is required, and yet we're
// unable to complete it yet because the RFH has not yet committed. We
// may be able to complete it once the RFH commits, so defer until this
// possibly happens. Note that in the `defer_callback`, we set
// `can_defer` to false in order to prevent any subsequent deferral for
// this header.
base::OnceCallback<void(NavigationOrDocumentHandle*)> defer_callback =
base::BindOnce(
[](base::WeakPtr<SharedStorageHeaderObserver> header_observer,
const url::Origin& request_origin, ContextType context_type,
std::vector<OperationPtr> operations,
mojo::ReportBadMessageCallback bad_message_callback,
NavigationOrDocumentHandle* navigation_or_document_handle) {
if (header_observer) {
header_observer->HeaderReceived(
request_origin, context_type,
navigation_or_document_handle, std::move(operations),
base::DoNothing(), std::move(bad_message_callback),
/*can_defer=*/false);
}
},
weak_ptr_factory_.GetWeakPtr(), request_origin, context_type,
std::move(operations), std::move(bad_message_callback));
static_cast<RenderFrameHostImpl*>(rfh)
->AddDeferredSharedStorageHeaderCallback(std::move(defer_callback));
}
std::move(callback).Run();
return;
}
case PermissionsPolicyDoubleCheckStatus::
kSubresourceSourceOtherLifecycleState:
[[fallthrough]];
case PermissionsPolicyDoubleCheckStatus::kSubresourceSourceNoRFH:
[[fallthrough]];
case PermissionsPolicyDoubleCheckStatus::kSubresourceSourceNoPolicy:
// We are in a case where the double check is required, and yet we're
// unable to complete it due to either a lack of RFH, a lack of policy, or
// a RFH in a LifecycleState other than kPendingCommit or kActive. We have
// no reason to suspect a compromised renderer or network service, so we
// simply drop any operations and return to the network service.
std::move(callback).Run();
return;
case PermissionsPolicyDoubleCheckStatus::kNavigationSourceNoPolicy:
// We can allow this case to proceed because `NavigationRequest` completed
// the previous permissions policy checks in the browser process.
break;
case PermissionsPolicyDoubleCheckStatus::kEnabled:
// This is the expected outcome in most cases.
break;
default:
NOTREACHED_NORETURN();
}
CHECK(IsSharedStorageAllowedByPermissionsPolicy(permissions_policy_status));
if (!IsSharedStorageAllowedBySiteSettings(navigation_or_document_handle,
request_origin,
/*out_debug_message=*/nullptr)) {
// TODO(crbug.com/40064101):
// 1. Log the following error message to console:
// "'Shared-Storage-Write: shared storage is disabled."
// 2. Send a non-null `out_debug_message` param and append it to the above
// error message if the value of
// `blink::features::kSharedStorageExposeDebugMessageForSettingsStatus`
// is true.
std::move(callback).Run();
return;
}
std::deque<network::mojom::SharedStorageOperationPtr> to_process;
to_process.insert(to_process.end(),
std::make_move_iterator(operations.begin()),
std::make_move_iterator(operations.end()));
std::vector<bool> header_results;
int main_frame_id = GetMainFrameIdFromNavigationOrDocumentHandle(
navigation_or_document_handle);
while (!to_process.empty()) {
network::mojom::SharedStorageOperationPtr operation =
std::move(to_process.front());
to_process.pop_front();
header_results.push_back(
Invoke(request_origin, main_frame_id, std::move(operation)));
}
OnHeaderProcessed(request_origin, header_results);
std::move(callback).Run();
}
bool SharedStorageHeaderObserver::Invoke(const url::Origin& request_origin,
int main_frame_id,
OperationPtr operation) {
switch (operation->type) {
case OperationType::kSet:
if (!operation->key.has_value() || !operation->value.has_value()) {
// TODO(crbug.com/40064101): Log the following error message to console:
// "Shared-Storage-Write: 'set' missing parameter 'key' or 'value'."
return false;
}
return Set(
request_origin, main_frame_id, std::move(operation->key.value()),
std::move(operation->value.value()), operation->ignore_if_present);
case OperationType::kAppend:
if (!operation->key.has_value() || !operation->value.has_value()) {
// TODO(crbug.com/40064101): Log the following error message to console:
// "Shared-Storage-Write: 'append' missing parameter 'key' or 'value'."
return false;
}
return Append(request_origin, main_frame_id,
std::move(operation->key.value()),
std::move(operation->value.value()));
case OperationType::kDelete:
if (!operation->key.has_value()) {
// TODO(crbug.com/40064101): Log the following error message to console:
// "Shared-Storage-Write: 'delete' missing parameter 'key'."
return false;
}
return Delete(request_origin, main_frame_id,
std::move(operation->key.value()));
case OperationType::kClear:
return Clear(request_origin, main_frame_id);
default:
NOTREACHED();
}
return false;
}
bool SharedStorageHeaderObserver::Set(
const url::Origin& request_origin,
int main_frame_id,
std::string key,
std::string value,
network::mojom::OptionalBool ignore_if_present) {
std::u16string utf16_key;
std::u16string utf16_value;
if (!base::UTF8ToUTF16(key.c_str(), key.size(), &utf16_key) ||
!base::UTF8ToUTF16(value.c_str(), value.size(), &utf16_value) ||
!blink::IsValidSharedStorageKeyStringLength(utf16_key.size()) ||
!blink::IsValidSharedStorageValueStringLength(utf16_value.size())) {
// TODO(crbug.com/40064101): Log the following error message to console:
// "Shared-Storage-Write: 'set' has invalid parameter 'key' or 'value'."
return false;
}
storage::SharedStorageDatabase::SetBehavior set_behavior =
(ignore_if_present == network::mojom::OptionalBool::kTrue)
? storage::SharedStorageDatabase::SetBehavior::kIgnoreIfPresent
: storage::SharedStorageDatabase::SetBehavior::kDefault;
NotifySharedStorageAccessed(
AccessType::kHeaderSet, main_frame_id, request_origin,
SharedStorageEventParams::CreateForSet(
key, value,
ignore_if_present == network::mojom::OptionalBool::kTrue));
GetSharedStorageManager()->Set(
request_origin, std::move(utf16_key), std::move(utf16_value),
base::BindOnce(&SharedStorageHeaderObserver::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr(), request_origin,
network::mojom::SharedStorageOperation::New(
OperationType::kSet, std::move(key), std::move(value),
ignore_if_present)),
set_behavior);
return true;
}
bool SharedStorageHeaderObserver::Append(const url::Origin& request_origin,
int main_frame_id,
std::string key,
std::string value) {
std::u16string utf16_key;
std::u16string utf16_value;
if (!base::UTF8ToUTF16(key.c_str(), key.size(), &utf16_key) ||
!base::UTF8ToUTF16(value.c_str(), value.size(), &utf16_value) ||
!blink::IsValidSharedStorageKeyStringLength(utf16_key.size()) ||
!blink::IsValidSharedStorageValueStringLength(utf16_value.size())) {
// TODO(crbug.com/40064101): Log the following error message to console:
// "Shared-Storage-Write: 'append' has invalid parameter 'key' or 'value'."
return false;
}
NotifySharedStorageAccessed(
AccessType::kHeaderAppend, main_frame_id, request_origin,
SharedStorageEventParams::CreateForAppend(key, value));
GetSharedStorageManager()->Append(
request_origin, std::move(utf16_key), std::move(utf16_value),
base::BindOnce(
&SharedStorageHeaderObserver::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr(), request_origin,
network::mojom::SharedStorageOperation::New(
OperationType::kAppend, std::move(key), std::move(value),
/*ignore_if_present=*/network::mojom::OptionalBool::kUnset)));
return true;
}
bool SharedStorageHeaderObserver::Delete(const url::Origin& request_origin,
int main_frame_id,
std::string key) {
std::u16string utf16_key;
if (!base::UTF8ToUTF16(key.c_str(), key.size(), &utf16_key) ||
!blink::IsValidSharedStorageKeyStringLength(utf16_key.size())) {
// TODO(crbug.com/40064101): Log the following error message to console:
// "Shared-Storage-Write: 'delete' has invalid parameter 'key'."
return false;
}
NotifySharedStorageAccessed(
AccessType::kHeaderDelete, main_frame_id, request_origin,
SharedStorageEventParams::CreateForGetOrDelete(key));
GetSharedStorageManager()->Delete(
request_origin, std::move(utf16_key),
base::BindOnce(
&SharedStorageHeaderObserver::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr(), request_origin,
network::mojom::SharedStorageOperation::New(
OperationType::kDelete, std::move(key), /*value=*/std::nullopt,
/*ignore_if_present=*/network::mojom::OptionalBool::kUnset)));
return true;
}
bool SharedStorageHeaderObserver::Clear(const url::Origin& request_origin,
int main_frame_id) {
NotifySharedStorageAccessed(AccessType::kHeaderClear, main_frame_id,
request_origin,
SharedStorageEventParams::CreateDefault());
GetSharedStorageManager()->Clear(
request_origin,
base::BindOnce(
&SharedStorageHeaderObserver::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr(), request_origin,
network::mojom::SharedStorageOperation::New(
OperationType::kClear,
/*key=*/std::nullopt, /*value=*/std::nullopt,
/*ignore_if_present=*/network::mojom::OptionalBool::kUnset)));
return true;
}
storage::SharedStorageManager*
SharedStorageHeaderObserver::GetSharedStorageManager() {
DCHECK(storage_partition_);
storage::SharedStorageManager* shared_storage_manager =
storage_partition_->GetSharedStorageManager();
// This `SharedStorageHeaderObserver` is created only if
// `kSharedStorageAPI` is enabled, in which case the `shared_storage_manager`
// must be valid.
DCHECK(shared_storage_manager);
return shared_storage_manager;
}
SharedStorageHeaderObserver::PermissionsPolicyDoubleCheckStatus
SharedStorageHeaderObserver::DoPermissionsPolicyDoubleCheck(
const url::Origin& request_origin,
ContextType context_type,
NavigationOrDocumentHandle* navigation_or_document_handle) {
switch (context_type) {
case ContextType::kRenderFrameHostContext: {
auto* rfh = navigation_or_document_handle->GetDocument();
if (!rfh) {
// This request may arrive after the document is destroyed. We consider
// this case to be ineligible for writing to shared storage.
// TODO(cammie): Investigate why a test unexpectedly hit this condition,
// e.g. "All/SharedStorageHeaderPrefBrowserTest.Basic/"
// + "PrivacySandboxDisabled_3PCookiesAllowed_*" on android-arm64-rel.
return PermissionsPolicyDoubleCheckStatus::kSubresourceSourceNoRFH;
}
if (rfh->IsInLifecycleState(
RenderFrameHost::LifecycleState::kPendingCommit)) {
// Due to the race between the subresource requests and navigations,
// this request may arrive before the commit confirmation is received.
// We can defer and try again later if a corresponding commit
// notification is received.
return PermissionsPolicyDoubleCheckStatus::kSubresourceSourceDefer;
}
if (!rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kActive)) {
// The RenderFrameHost is in a lifecycle state where it is unclear
// if/how we could properly handle the required permissions policy
// check. So we drop these operations.
// TODO(cammie): Investigate further whether any of these cases can be
// handled.
return PermissionsPolicyDoubleCheckStatus::
kSubresourceSourceOtherLifecycleState;
}
auto* permissions_policy =
static_cast<RenderFrameHostImpl*>(rfh)->GetPermissionsPolicy();
if (!permissions_policy) {
// If we cannot obtain a permissions policy, we consider the request to
// be ineligible for writing to shared storage.
return PermissionsPolicyDoubleCheckStatus::kSubresourceSourceNoPolicy;
}
// Create a dummy `network::ResourceRequest` so that we can signal to
// `permissions_policy` that the actual request was opted-in to shared
// storage and hence that
// `blink::mojom::PermissionsPolicyFeature::kSharedStorage` should be
// treated as an assumed opt-in feature during the permissions check.
network::ResourceRequest dummy_request;
dummy_request.shared_storage_writable_eligible = true;
return permissions_policy->IsFeatureEnabledForSubresourceRequest(
blink::mojom::PermissionsPolicyFeature::kSharedStorage,
request_origin, dummy_request)
? PermissionsPolicyDoubleCheckStatus::kEnabled
: PermissionsPolicyDoubleCheckStatus::kDisabled;
}
case ContextType::kNavigationRequestContext: {
auto* frame_tree_node = navigation_or_document_handle->GetFrameTreeNode();
if (!frame_tree_node->parent()) {
return PermissionsPolicyDoubleCheckStatus::
kDisallowedMainFrameNavigation;
}
const blink::PermissionsPolicy* parent_policy =
frame_tree_node->parent()->permissions_policy();
if (!parent_policy) {
return PermissionsPolicyDoubleCheckStatus::kNavigationSourceNoPolicy;
}
return parent_policy->IsFeatureEnabledForOrigin(
blink::mojom::PermissionsPolicyFeature::kSharedStorage,
request_origin)
? PermissionsPolicyDoubleCheckStatus::kEnabled
: PermissionsPolicyDoubleCheckStatus::kDisabled;
}
case ContextType::kServiceWorkerContext:
// TODO(cammie): Handle the service worker case. Currently, the headers
// aren't available to requests initiated by service workers.
[[fallthrough]];
default:
NOTREACHED_NORETURN();
}
}
bool SharedStorageHeaderObserver::IsSharedStorageAllowedBySiteSettings(
NavigationOrDocumentHandle* navigation_or_document_handle,
const url::Origin& request_origin,
std::string* out_debug_message) {
DCHECK(storage_partition_);
DCHECK(storage_partition_->browser_context());
bool has_top_frame_origin =
navigation_or_document_handle &&
navigation_or_document_handle->GetTopmostFrameOrigin().has_value();
url::Origin top_frame_origin =
has_top_frame_origin
? navigation_or_document_handle->GetTopmostFrameOrigin().value()
: url::Origin();
base::UmaHistogramBoolean(
"Storage.SharedStorage.HeaderObserver.CreatedOpaqueOriginForPrefsCheck",
!has_top_frame_origin);
auto* rfh = navigation_or_document_handle
? navigation_or_document_handle->GetDocument()
: nullptr;
return GetContentClient()->browser()->IsSharedStorageAllowed(
storage_partition_->browser_context(), rfh, top_frame_origin,
request_origin, out_debug_message);
}
void SharedStorageHeaderObserver::NotifySharedStorageAccessed(
AccessType type,
int main_frame_id,
const url::Origin& request_origin,
const SharedStorageEventParams& params) {
storage_partition_->GetSharedStorageWorkletHostManager()
->NotifySharedStorageAccessed(type, main_frame_id,
request_origin.Serialize(), params);
}
} // namespace content