Enable sharedStorage.get() in network-restricted fenced frames.
If the proper flag (kFencedFramesLocalUnpartitionedDataAccess) is enabled, sharedStorage.get() can now be called in fenced frames. The call will only succeed if the entire tree rooted at the fenced frame has disabled untrusted network access, including nested fenced frames trees. Bug: 324440086 Change-Id: Ie6788ba6d299a334800ecf11ba90151eaf40f43d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5279221 Reviewed-by: Cammie Smith Barnes <cammie@chromium.org> Reviewed-by: Ben Kelly <wanderview@chromium.org> Commit-Queue: Andrew Verge <averge@chromium.org> Reviewed-by: Garrett Tanzer <gtanzer@chromium.org> Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org> Cr-Commit-Position: refs/heads/main@{#1272982}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
b6651e7592
commit
1935db82b2
content/browser
devtools
protocol
renderer_host
shared_storage
third_party/blink
public
devtools_protocol
mojom
shared_storage
renderer
modules
shared_storage
web_tests
external
wpt
fenced-frame
webexposed
tools/metrics/histograms/metadata/storage
@ -1516,6 +1516,9 @@ void StorageHandler::NotifySharedStorageAccessed(
|
||||
case AccessType::kDocumentClear:
|
||||
type_enum = Storage::SharedStorageAccessTypeEnum::DocumentClear;
|
||||
break;
|
||||
case AccessType::kDocumentGet:
|
||||
type_enum = Storage::SharedStorageAccessTypeEnum::DocumentGet;
|
||||
break;
|
||||
case AccessType::kWorkletSet:
|
||||
type_enum = Storage::SharedStorageAccessTypeEnum::WorkletSet;
|
||||
break;
|
||||
|
@ -16265,6 +16265,39 @@ bool RenderFrameHostImpl::ShouldChangeRenderFrameHostOnSameSiteNavigation()
|
||||
must_be_replaced());
|
||||
}
|
||||
|
||||
bool RenderFrameHostImpl::CanReadFromSharedStorage() {
|
||||
if (!IsNestedWithinFencedFrame()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allowed = true;
|
||||
// TODO(averge): There's probably some redundant traversal happening here,
|
||||
// because if we know that a fenced frame root has network disabled, then
|
||||
// we know all iframe children in the inner FrameTree also have network
|
||||
// disabled. For now we'll check every RFHI to be safest, but it might be
|
||||
// better to add a ForEachFencedFrameRoot traversal to speed things up a bit.
|
||||
// TODO(averge): Credentialless iframes have their own per-`Page` nonce that
|
||||
// will be revoked alongside the fenced frame's nonce. Once revocation is
|
||||
// implemented for credentialless iframes, we should do one of 2 things:
|
||||
// 1. Only set has_disabled_untrusted_network if both nonces are revoked.
|
||||
// 2. If not, this traversal should check if the current `Page` for each RFHI
|
||||
// has a revoked iframe nonce.
|
||||
ForEachRenderFrameHost([&allowed](RenderFrameHostImpl* rfhi) {
|
||||
// URN iframes cannot disable untrusted network on their own, so
|
||||
// kClosestAncestor isn't appropriate here. Using kFrameTreeRoot will search
|
||||
// for the root node of each FrameTree, and for fenced frames, this will
|
||||
// always be the root of the fenced frame tree.
|
||||
auto properties = rfhi->frame_tree_node()->GetFencedFrameProperties(
|
||||
FencedFramePropertiesNodeSource::kFrameTreeRoot);
|
||||
if (!properties.has_value() ||
|
||||
!properties->has_disabled_untrusted_network()) {
|
||||
allowed = false;
|
||||
}
|
||||
});
|
||||
|
||||
return allowed;
|
||||
}
|
||||
|
||||
bool RenderFrameHostImpl::ShouldReuseCompositing(
|
||||
SiteInstanceImpl& speculative_site_instance) const {
|
||||
if (!ShouldChangeRenderFrameHostOnSameSiteNavigation()) {
|
||||
|
@ -3005,6 +3005,12 @@ class CONTENT_EXPORT RenderFrameHostImpl
|
||||
NavigationRequest& navigation_request,
|
||||
blink::mojom::AutomaticBeaconType event_type);
|
||||
|
||||
// Determines if this RenderFrameHostImpl is allowed to read from Shared
|
||||
// Storage. Only true if this RenderFrameHostImpl is in a fenced frame tree,
|
||||
// and if its closest fenced frame root ancestor and all nested fenced frame
|
||||
// roots have disabled untrusted network access.
|
||||
bool CanReadFromSharedStorage();
|
||||
|
||||
// Returns true if this RFH's compositor should be reused by a speculative
|
||||
// RFH with the `speculative_site_instance`.
|
||||
// Returns false if the speculative RFH should initialize a new compositor.
|
||||
|
@ -6439,6 +6439,199 @@ IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameInteractionBrowserTest,
|
||||
EXPECT_EQ(expected_error, extra_result.error);
|
||||
}
|
||||
|
||||
class SharedStorageFencedFrameDocumentGetBrowserTest
|
||||
: public SharedStorageFencedFrameInteractionBrowserTest {
|
||||
public:
|
||||
SharedStorageFencedFrameDocumentGetBrowserTest() {
|
||||
fenced_frame_feature_.InitAndEnableFeature(
|
||||
/*feature=*/
|
||||
blink::features::kFencedFramesLocalUnpartitionedDataAccess);
|
||||
}
|
||||
|
||||
private:
|
||||
base::test::ScopedFeatureList fenced_frame_feature_;
|
||||
};
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
|
||||
GetAllowedInNetworkRestrictedFencedFrame) {
|
||||
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
|
||||
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'apple');
|
||||
)"));
|
||||
|
||||
FrameTreeNode* fenced_frame_root_node =
|
||||
CreateFencedFrame(https_server()->GetURL("a.test", kFencedFramePath));
|
||||
|
||||
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
|
||||
(async () => {
|
||||
await window.fence.disableUntrustedNetwork();
|
||||
return sharedStorage.get('test');
|
||||
})();
|
||||
)");
|
||||
|
||||
EXPECT_EQ(get_result, "apple");
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
|
||||
GetRejectsInFencedFrameWithoutRestrictedNetwork) {
|
||||
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
|
||||
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'apple');
|
||||
)"));
|
||||
|
||||
FrameTreeNode* fenced_frame_root_node =
|
||||
CreateFencedFrame(https_server()->GetURL("a.test", kFencedFramePath));
|
||||
|
||||
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
|
||||
sharedStorage.get('test');
|
||||
)");
|
||||
|
||||
EXPECT_THAT(
|
||||
get_result.error,
|
||||
testing::HasSubstr(
|
||||
"sharedStorage.get() is not allowed in a fenced frame until network "
|
||||
"access for it and all descendent frames has been revoked with "
|
||||
"window.fence.disableUntrustedNetwork()"));
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
|
||||
GetInFencedFrameOnlyFetchesValuesFromCurrentOrigin) {
|
||||
// sharedStorage.set() for a.test
|
||||
GURL main_frame_url1 = https_server()->GetURL("a.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url1));
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'apple');
|
||||
)"));
|
||||
|
||||
// sharedStorage.set() for b.test
|
||||
GURL main_frame_url2 = https_server()->GetURL("b.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url2));
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'banana');
|
||||
)"));
|
||||
|
||||
// An a.test fenced frame embedded in b.test should only read a.test's set
|
||||
// values.
|
||||
FrameTreeNode* fenced_frame_root_node =
|
||||
CreateFencedFrame(https_server()->GetURL("a.test", kFencedFramePath));
|
||||
|
||||
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
|
||||
(async () => {
|
||||
await window.fence.disableUntrustedNetwork();
|
||||
return sharedStorage.get('test');
|
||||
})();
|
||||
)");
|
||||
|
||||
EXPECT_EQ(get_result, "apple");
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
|
||||
GetRejectsInMainFrame) {
|
||||
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
|
||||
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'apple');
|
||||
)"));
|
||||
|
||||
EvalJsResult get_result_main_frame = EvalJs(shell(), R"(
|
||||
sharedStorage.get('test');
|
||||
)");
|
||||
|
||||
EXPECT_THAT(
|
||||
get_result_main_frame.error,
|
||||
testing::HasSubstr("Cannot call get() outside of a fenced frame."));
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(SharedStorageFencedFrameDocumentGetBrowserTest,
|
||||
GetRejectsInIFrame) {
|
||||
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
|
||||
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'apple');
|
||||
)"));
|
||||
|
||||
FrameTreeNode* iframe_root =
|
||||
CreateIFrame(PrimaryFrameTreeNodeRoot(), main_frame_url);
|
||||
|
||||
EvalJsResult get_result_iframe = EvalJs(iframe_root, R"(
|
||||
sharedStorage.get('test');
|
||||
)");
|
||||
|
||||
EXPECT_THAT(
|
||||
get_result_iframe.error,
|
||||
testing::HasSubstr("Cannot call get() outside of a fenced frame."));
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(
|
||||
SharedStorageFencedFrameDocumentGetBrowserTest,
|
||||
GetAllowedInNetworkRestrictedNestedFencedFrameIfParentStillHasNetwork) {
|
||||
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
|
||||
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'apple');
|
||||
)"));
|
||||
|
||||
GURL fenced_frame_url = https_server()->GetURL("a.test", kFencedFramePath);
|
||||
// The parent fenced frame never calls window.fence.disableUntrustedNetwork().
|
||||
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(fenced_frame_url);
|
||||
|
||||
FrameTreeNode* nested_fenced_frame_root_node =
|
||||
CreateFencedFrame(fenced_frame_root_node, fenced_frame_url);
|
||||
|
||||
EvalJsResult get_result = EvalJs(nested_fenced_frame_root_node, R"(
|
||||
(async () => {
|
||||
await window.fence.disableUntrustedNetwork();
|
||||
return sharedStorage.get('test');
|
||||
})();
|
||||
)");
|
||||
|
||||
EXPECT_EQ(get_result, "apple");
|
||||
}
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(
|
||||
SharedStorageFencedFrameDocumentGetBrowserTest,
|
||||
GetNotAllowedInNetworkRestrictedParentFencedFrameIfChildStillHasNetwork) {
|
||||
GURL main_frame_url = https_server()->GetURL("a.test", kSimplePagePath);
|
||||
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
|
||||
|
||||
EXPECT_TRUE(ExecJs(shell(), R"(
|
||||
sharedStorage.set('test', 'apple');
|
||||
)"));
|
||||
|
||||
GURL fenced_frame_url = https_server()->GetURL("a.test", kFencedFramePath);
|
||||
FrameTreeNode* fenced_frame_root_node = CreateFencedFrame(fenced_frame_url);
|
||||
|
||||
CreateFencedFrame(fenced_frame_root_node, fenced_frame_url);
|
||||
|
||||
// Note that we do *not* await the call to disableUntrustedNetwork, because we
|
||||
// need to operate in the top frame while the nested frame still hasn't
|
||||
// disabled network access.
|
||||
// TODO(crbug.com/324440086): disableUntrustedNetwork() should not resolve
|
||||
// until all child frames also disable untrusted network. However, that
|
||||
// behavior is yet to be implemented. We should add a timeout check here
|
||||
// once the async behavior of disableUntrustedNetwork() is correct.
|
||||
EvalJsResult get_result = EvalJs(fenced_frame_root_node, R"(
|
||||
(async () => {
|
||||
let disable_network_promise = window.fence.disableUntrustedNetwork();
|
||||
await sharedStorage.get('test');
|
||||
})();
|
||||
)");
|
||||
|
||||
EXPECT_THAT(
|
||||
get_result.error,
|
||||
testing::HasSubstr(
|
||||
"sharedStorage.get() is not allowed in a fenced frame until network "
|
||||
"access for it and all descendent frames has been revoked with "
|
||||
"window.fence.disableUntrustedNetwork()"));
|
||||
}
|
||||
|
||||
class SharedStorageSelectURLNotAllowedInFencedFrameBrowserTest
|
||||
: public SharedStorageFencedFrameInteractionBrowserTest {
|
||||
public:
|
||||
|
@ -47,6 +47,9 @@ bool IsSecureFrame(RenderFrameHost* frame) {
|
||||
using AccessType =
|
||||
SharedStorageWorkletHostManager::SharedStorageObserverInterface::AccessType;
|
||||
|
||||
using OperationResult = storage::SharedStorageManager::OperationResult;
|
||||
using GetResult = storage::SharedStorageManager::GetResult;
|
||||
|
||||
} // namespace
|
||||
|
||||
const char kSharedStorageDisabledMessage[] = "sharedStorage is disabled";
|
||||
@ -155,6 +158,70 @@ void SharedStorageDocumentServiceImpl::CreateWorklet(
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
void SharedStorageDocumentServiceImpl::SharedStorageGet(
|
||||
const std::u16string& key,
|
||||
SharedStorageGetCallback callback) {
|
||||
if (!render_frame_host().IsNestedWithinFencedFrame()) {
|
||||
mojo::ReportBadMessage(
|
||||
"Attempted to call get() outside of a fenced frame.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string debug_message;
|
||||
if (!IsSharedStorageAllowed(&debug_message)) {
|
||||
std::move(callback).Run(blink::mojom::SharedStorageGetStatus::kError,
|
||||
/*error_message=*/
|
||||
GetSharedStorageErrorMessage(
|
||||
debug_message, kSharedStorageDisabledMessage),
|
||||
/*value=*/{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(static_cast<RenderFrameHostImpl&>(render_frame_host())
|
||||
.CanReadFromSharedStorage())) {
|
||||
std::move(callback).Run(
|
||||
blink::mojom::SharedStorageGetStatus::kError,
|
||||
/*error_message=*/
|
||||
"sharedStorage.get() is not allowed in a fenced frame until network "
|
||||
"access for it and all descendent frames has been revoked with "
|
||||
"window.fence.disableUntrustedNetwork()",
|
||||
/*value=*/{});
|
||||
return;
|
||||
}
|
||||
|
||||
GetSharedStorageWorkletHostManager()->NotifySharedStorageAccessed(
|
||||
AccessType::kDocumentGet, main_frame_id(), SerializeLastCommittedOrigin(),
|
||||
SharedStorageEventParams::CreateForGetOrDelete(base::UTF16ToUTF8(key)));
|
||||
|
||||
auto operation_completed_callback = base::BindOnce(
|
||||
[](SharedStorageGetCallback callback, GetResult result) {
|
||||
// If the key is not found but there is no other error, the worklet will
|
||||
// resolve the promise to undefined.
|
||||
if (result.result == OperationResult::kNotFound ||
|
||||
result.result == OperationResult::kExpired) {
|
||||
std::move(callback).Run(
|
||||
blink::mojom::SharedStorageGetStatus::kNotFound,
|
||||
/*error_message=*/"sharedStorage.get() could not find key",
|
||||
/*value=*/{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.result != OperationResult::kSuccess) {
|
||||
std::move(callback).Run(
|
||||
blink::mojom::SharedStorageGetStatus::kError,
|
||||
/*error_message=*/"sharedStorage.get() failed", /*value=*/{});
|
||||
return;
|
||||
}
|
||||
|
||||
std::move(callback).Run(blink::mojom::SharedStorageGetStatus::kSuccess,
|
||||
/*error_message=*/{}, /*value=*/result.data);
|
||||
},
|
||||
std::move(callback));
|
||||
|
||||
GetSharedStorageManager()->Get(render_frame_host().GetLastCommittedOrigin(),
|
||||
key, std::move(operation_completed_callback));
|
||||
}
|
||||
|
||||
void SharedStorageDocumentServiceImpl::SharedStorageSet(
|
||||
const std::u16string& key,
|
||||
const std::u16string& value,
|
||||
|
@ -67,6 +67,8 @@ class CONTENT_EXPORT SharedStorageDocumentServiceImpl final
|
||||
mojo::PendingAssociatedReceiver<blink::mojom::SharedStorageWorkletHost>
|
||||
worklet_host,
|
||||
CreateWorkletCallback callback) override;
|
||||
void SharedStorageGet(const std::u16string& key,
|
||||
SharedStorageGetCallback callback) override;
|
||||
void SharedStorageSet(const std::u16string& key,
|
||||
const std::u16string& value,
|
||||
bool ignore_if_present,
|
||||
|
@ -47,6 +47,7 @@ class CONTENT_EXPORT SharedStorageWorkletHostManager {
|
||||
kDocumentAppend,
|
||||
kDocumentDelete,
|
||||
kDocumentClear,
|
||||
kDocumentGet,
|
||||
kWorkletSet,
|
||||
kWorkletAppend,
|
||||
kWorkletDelete,
|
||||
|
@ -9674,6 +9674,7 @@ experimental domain Storage
|
||||
documentAppend
|
||||
documentDelete
|
||||
documentClear
|
||||
documentGet
|
||||
workletSet
|
||||
workletAppend
|
||||
workletDelete
|
||||
|
@ -34,9 +34,18 @@ struct SharedStorageUrlWithMetadata {
|
||||
map<string, url.mojom.Url> reporting_metadata;
|
||||
};
|
||||
|
||||
enum SharedStorageGetStatus {
|
||||
kSuccess,
|
||||
kNotFound,
|
||||
kError,
|
||||
};
|
||||
|
||||
// Implemented by the browser and exposed to the renderer process on a
|
||||
// per-worklet basis, to allow initiating worklet operations, etc. Note that
|
||||
// currently each frame can initialize at most one worklet.
|
||||
// TODO: Methods in this interface should should be refactored so that all
|
||||
// possible return values are semantically valid. See details in
|
||||
// https://chromium.googlesource.com/chromium/src/+/main/docs/security/mojo.md#all-possible-message-values-are-semantically-valid
|
||||
interface SharedStorageWorkletHost {
|
||||
// Handle sharedStorage.selectURL(): run the operation
|
||||
// previously registered by register() with matching `name`. Restrictions will
|
||||
@ -91,6 +100,9 @@ interface SharedStorageWorkletHost {
|
||||
//
|
||||
// Implemented by the browser and exposed to the renderer process on a per-frame
|
||||
// basis, to allow accessing the shared storage and creating the worklet.
|
||||
// TODO: Methods in this interface should should be refactored so that all
|
||||
// possible return values are semantically valid. See details in
|
||||
// https://chromium.googlesource.com/chromium/src/+/main/docs/security/mojo.md#all-possible-message-values-are-semantically-valid
|
||||
interface SharedStorageDocumentService {
|
||||
// Create the worklet, and download and load the module script in the worklet
|
||||
// environment. The origin of the `script_source_url` should be
|
||||
@ -104,6 +116,14 @@ interface SharedStorageDocumentService {
|
||||
=> (bool success,
|
||||
string error_message);
|
||||
|
||||
// Handle sharedStorage.get(): get the entry at `key`, or an empty string
|
||||
// if `key` is not present. May only be called from a fenced frame with
|
||||
// network access restricted. Returns an error if the API is disabled, or if
|
||||
// the state of the fenced frame is invalid.
|
||||
SharedStorageGet(blink.mojom.SharedStorageKeyArgument key)
|
||||
=> (SharedStorageGetStatus status, string error_message,
|
||||
mojo_base.mojom.String16 value);
|
||||
|
||||
// Handle sharedStorage.set(): set `key`’s entry to `value`. If
|
||||
// `ignore_if_present` is true, the entry is not updated if `key` already
|
||||
// exists. The only error reported is if the API is disabled; other errors are
|
||||
|
@ -39,12 +39,6 @@ interface SharedStorageEntriesListener {
|
||||
int32 total_queued_to_send);
|
||||
};
|
||||
|
||||
enum SharedStorageGetStatus {
|
||||
kSuccess,
|
||||
kNotFound,
|
||||
kError,
|
||||
};
|
||||
|
||||
// Used by the shared storage worklet environment to access the shared storage,
|
||||
// log messages, etc.
|
||||
interface SharedStorageWorkletServiceClient {
|
||||
|
@ -136,6 +136,25 @@ GetSharedStorageWorkletServiceClient(ExecutionContext* execution_context) {
|
||||
->GetSharedStorageWorkletServiceClient();
|
||||
}
|
||||
|
||||
bool CanGetOutsideWorklet(ScriptState* script_state) {
|
||||
ExecutionContext* execution_context = ExecutionContext::From(script_state);
|
||||
CHECK(execution_context->IsWindow());
|
||||
|
||||
LocalFrame* frame = To<LocalDOMWindow>(execution_context)->GetFrame();
|
||||
DCHECK(frame);
|
||||
|
||||
if (!blink::features::IsFencedFramesEnabled() ||
|
||||
!base::FeatureList::IsEnabled(
|
||||
blink::features::kFencedFramesLocalUnpartitionedDataAccess)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calling get() is only allowed in fenced frame trees where network access
|
||||
// has been restricted. We can't check the network access part in the
|
||||
// renderer, so we'll defer to the browser for that.
|
||||
return frame->IsInFencedFrameTree();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class SharedStorage::IterationSource final
|
||||
@ -547,7 +566,8 @@ ScriptPromiseTyped<IDLString> SharedStorage::get(
|
||||
ExceptionState& exception_state) {
|
||||
base::TimeTicks start_time = base::TimeTicks::Now();
|
||||
ExecutionContext* execution_context = ExecutionContext::From(script_state);
|
||||
CHECK(execution_context->IsSharedStorageWorkletGlobalScope());
|
||||
CHECK(execution_context->IsWindow() ||
|
||||
execution_context->IsSharedStorageWorkletGlobalScope());
|
||||
|
||||
if (!CheckBrowsingContextIsValid(*script_state, exception_state)) {
|
||||
return ScriptPromiseTyped<IDLString>();
|
||||
@ -558,6 +578,13 @@ ScriptPromiseTyped<IDLString> SharedStorage::get(
|
||||
script_state, exception_state.GetContext());
|
||||
auto promise = resolver->Promise();
|
||||
|
||||
if (execution_context->IsWindow() && !CanGetOutsideWorklet(script_state)) {
|
||||
resolver->Reject(V8ThrowDOMException::CreateOrEmpty(
|
||||
script_state->GetIsolate(), DOMExceptionCode::kOperationError,
|
||||
"Cannot call get() outside of a fenced frame."));
|
||||
return promise;
|
||||
}
|
||||
|
||||
CHECK(CheckSharedStoragePermissionsPolicy(*script_state, *execution_context,
|
||||
*resolver));
|
||||
|
||||
@ -568,42 +595,50 @@ ScriptPromiseTyped<IDLString> SharedStorage::get(
|
||||
return promise;
|
||||
}
|
||||
|
||||
GetSharedStorageWorkletServiceClient(execution_context)
|
||||
->SharedStorageGet(
|
||||
key,
|
||||
WTF::BindOnce(
|
||||
[](ScriptPromiseResolverTyped<IDLString>* resolver,
|
||||
SharedStorage* shared_storage, base::TimeTicks start_time,
|
||||
mojom::blink::SharedStorageGetStatus status,
|
||||
const String& error_message, const String& value) {
|
||||
DCHECK(resolver);
|
||||
ScriptState* script_state = resolver->GetScriptState();
|
||||
std::string histogram_name = execution_context->IsWindow()
|
||||
? "Storage.SharedStorage.Document.Timing.Get"
|
||||
: "Storage.SharedStorage.Worklet.Timing.Get";
|
||||
auto callback = WTF::BindOnce(
|
||||
[](ScriptPromiseResolverTyped<IDLString>* resolver,
|
||||
SharedStorage* shared_storage, base::TimeTicks start_time,
|
||||
const std::string& histogram_name,
|
||||
mojom::blink::SharedStorageGetStatus status,
|
||||
const String& error_message, const String& value) {
|
||||
DCHECK(resolver);
|
||||
ScriptState* script_state = resolver->GetScriptState();
|
||||
|
||||
if (status == mojom::blink::SharedStorageGetStatus::kError) {
|
||||
if (IsInParallelAlgorithmRunnable(
|
||||
resolver->GetExecutionContext(), script_state)) {
|
||||
ScriptState::Scope scope(script_state);
|
||||
resolver->Reject(V8ThrowDOMException::CreateOrEmpty(
|
||||
script_state->GetIsolate(),
|
||||
DOMExceptionCode::kOperationError, error_message));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (status == mojom::blink::SharedStorageGetStatus::kError) {
|
||||
if (IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
|
||||
script_state)) {
|
||||
ScriptState::Scope scope(script_state);
|
||||
resolver->Reject(V8ThrowDOMException::CreateOrEmpty(
|
||||
script_state->GetIsolate(), DOMExceptionCode::kOperationError,
|
||||
error_message));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
base::UmaHistogramMediumTimes(
|
||||
"Storage.SharedStorage.Worklet.Timing.Get",
|
||||
base::TimeTicks::Now() - start_time);
|
||||
base::UmaHistogramMediumTimes(histogram_name,
|
||||
base::TimeTicks::Now() - start_time);
|
||||
|
||||
if (status == mojom::blink::SharedStorageGetStatus::kSuccess) {
|
||||
resolver->Resolve(value);
|
||||
return;
|
||||
}
|
||||
if (status == mojom::blink::SharedStorageGetStatus::kSuccess) {
|
||||
resolver->Resolve(value);
|
||||
return;
|
||||
}
|
||||
|
||||
CHECK_EQ(status,
|
||||
mojom::blink::SharedStorageGetStatus::kNotFound);
|
||||
resolver->Resolve();
|
||||
},
|
||||
WrapPersistent(resolver), WrapPersistent(this), start_time));
|
||||
CHECK_EQ(status, mojom::blink::SharedStorageGetStatus::kNotFound);
|
||||
resolver->Resolve();
|
||||
},
|
||||
WrapPersistent(resolver), WrapPersistent(this), start_time,
|
||||
histogram_name);
|
||||
|
||||
if (execution_context->IsWindow()) {
|
||||
GetSharedStorageDocumentService(execution_context)
|
||||
->SharedStorageGet(key, std::move(callback));
|
||||
} else {
|
||||
GetSharedStorageWorkletServiceClient(execution_context)
|
||||
->SharedStorageGet(key, std::move(callback));
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
@ -33,7 +33,6 @@
|
||||
] Promise<any> clear();
|
||||
|
||||
[
|
||||
Exposed=SharedStorageWorklet,
|
||||
CallWith=ScriptState,
|
||||
RaisesException
|
||||
] Promise<DOMString> get(DOMString key);
|
||||
|
141
third_party/blink/web_tests/external/wpt/fenced-frame/content-shared-storage-get.https.html
vendored
Normal file
141
third_party/blink/web_tests/external/wpt/fenced-frame/content-shared-storage-get.https.html
vendored
Normal file
@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<title>Test sharedStorage.get() in a fenced frame with network revocation.</title>
|
||||
<meta name="timeout" content="long">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/common/utils.js"></script>
|
||||
<script src="/common/dispatcher/dispatcher.js"></script>
|
||||
<script src="/common/get-host-info.sub.js"></script>
|
||||
<script src="resources/utils.js"></script>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
|
||||
promise_setup(async () => {
|
||||
// Set sharedStorage value for HTTPS_ORIGIN
|
||||
await sharedStorage.set('test', 'apple');
|
||||
|
||||
// Set sharedStorage value for HTTPS_REMOTE_ORIGIN.
|
||||
let init_iframe = await attachIFrameContext(
|
||||
{origin: get_host_info().HTTPS_REMOTE_ORIGIN});
|
||||
await init_iframe.execute(async () => {
|
||||
await sharedStorage.set('test', 'banana');
|
||||
});
|
||||
});
|
||||
|
||||
promise_test(async (t) => {
|
||||
const fencedframe = await attachFencedFrameContext(
|
||||
{origin: get_host_info().HTTPS_ORIGIN});
|
||||
|
||||
await fencedframe.execute(async () => {
|
||||
await window.fence.disableUntrustedNetwork();
|
||||
let get_result = await sharedStorage.get('test');
|
||||
assert_equals(get_result, 'apple');
|
||||
});
|
||||
}, 'Test sharedStorage.get() succeeds in a fenced frame with network revocation.');
|
||||
|
||||
promise_test(async (t) => {
|
||||
const fencedframe = await attachFencedFrameContext(
|
||||
{origin: get_host_info().HTTPS_ORIGIN});
|
||||
|
||||
await fencedframe.execute(async () => {
|
||||
try {
|
||||
await sharedStorage.get('test');
|
||||
assert_unreached('Calling get() without revoking network should throw.');
|
||||
} catch (e) {
|
||||
assert_equals(e.name, 'OperationError');
|
||||
assert_equals(e.message, 'sharedStorage.get() is not allowed in a ' +
|
||||
'fenced frame until network access for it and all descendent frames ' +
|
||||
'has been revoked with window.fence.disableUntrustedNetwork()');
|
||||
}
|
||||
});
|
||||
}, 'Test that sharedStorage.get() in a fenced frame rejects when network is not revoked.');
|
||||
|
||||
promise_test(async (t) => {
|
||||
const fencedframe = await attachFencedFrameContext(
|
||||
{origin: get_host_info().HTTPS_REMOTE_ORIGIN});
|
||||
|
||||
await fencedframe.execute(async () => {
|
||||
await window.fence.disableUntrustedNetwork();
|
||||
let get_result = await sharedStorage.get('test');
|
||||
assert_equals(get_result, 'banana');
|
||||
});
|
||||
}, 'Test that sharedStorage.get() can only fetch keys for its own origin when network is revoked.');
|
||||
|
||||
promise_test(async (t) => {
|
||||
// First, try get() in a top-level document.
|
||||
try {
|
||||
await sharedStorage.get('test');
|
||||
assert_unreached('Call to get() in top-level document should throw.');
|
||||
} catch (e) {
|
||||
assert_equals(e.name, 'OperationError');
|
||||
assert_equals(e.message, 'Cannot call get() outside of a fenced frame.')
|
||||
}
|
||||
|
||||
const samesite_iframe = await attachIFrameContext(
|
||||
{origin: get_host_info().HTTPS_ORIGIN});
|
||||
|
||||
// Then, try get() in an iframe.
|
||||
await samesite_iframe.execute(async () => {
|
||||
try {
|
||||
await sharedStorage.get('test');
|
||||
assert_unreached('Call to get() outside a fenced frame should throw.');
|
||||
} catch (e) {
|
||||
assert_equals(e.name, 'OperationError');
|
||||
assert_equals(e.message, 'Cannot call get() outside of a fenced frame.')
|
||||
}
|
||||
});
|
||||
}, 'Test that sharedStorage.get() rejects outside of a fenced frame.');
|
||||
|
||||
promise_test(async (t) => {
|
||||
const fencedframe = await attachFencedFrameContext(
|
||||
{origin: get_host_info().HTTPS_ORIGIN});
|
||||
|
||||
let get_result = await fencedframe.execute(async () => {
|
||||
const nested_frame = await attachFencedFrameContext(
|
||||
{origin: get_host_info().HTTPS_ORIGIN});
|
||||
|
||||
// Note that the parent fenced frame has not disabled network at this point.
|
||||
return nested_frame.execute(async () => {
|
||||
await window.fence.disableUntrustedNetwork();
|
||||
return sharedStorage.get('test');
|
||||
});
|
||||
});
|
||||
|
||||
assert_equals(get_result, 'apple');
|
||||
}, 'Test that sharedStorage.get() succeeds in a nested fenced frame with network disabled, ' +
|
||||
'even if the parent fenced frame has not disabled network yet.');
|
||||
|
||||
promise_test(async (t) => {
|
||||
const fencedframe = await attachFencedFrameContext(
|
||||
{origin: get_host_info().HTTPS_ORIGIN});
|
||||
|
||||
await fencedframe.execute(async () => {
|
||||
const nested_frame = await attachFencedFrameContext(
|
||||
{origin: get_host_info().HTTPS_ORIGIN})
|
||||
|
||||
// Note that we do *not* await, because we need to operate in the top frame
|
||||
// while the nested frame still hasn't disabled network access.
|
||||
// TODO(crbug.com/324440086): disableUntrustedNetwork() should not resolve
|
||||
// until all child frames also disable untrusted network. However, that
|
||||
// behavior is yet to be implemented. We should add a timeout check here
|
||||
// once the async behavior of disableUntrustedNetwork() is correct.
|
||||
let disable_network_promise = window.fence.disableUntrustedNetwork();
|
||||
|
||||
try {
|
||||
await sharedStorage.get('test');
|
||||
assert_unreached('sharedStorage.get() is not allowed in a fenced frame ' +
|
||||
'until network access for it and all descendent frames has been ' +
|
||||
'revoked with window.fence.disableUntrustedNetwork()');
|
||||
} catch (e) {
|
||||
assert_equals(e.name, 'OperationError');
|
||||
assert_equals(e.message, 'sharedStorage.get() is not allowed in a ' +
|
||||
'fenced frame until network access for it and all descendent frames ' +
|
||||
'has been revoked with window.fence.disableUntrustedNetwork()');
|
||||
}
|
||||
});
|
||||
}, 'sharedStorage.get() fails in a top-level fenced frame with network disabled, ' +
|
||||
'because a nested fenced frame has not disabled network yet.');
|
||||
|
||||
</script>
|
||||
</body>
|
@ -9461,6 +9461,7 @@ interface SharedStorage
|
||||
method clear
|
||||
method constructor
|
||||
method delete
|
||||
method get
|
||||
method run
|
||||
method selectURL
|
||||
method set
|
||||
|
@ -974,6 +974,19 @@ chromium-metrics-reviews@google.com.
|
||||
</summary>
|
||||
</histogram>
|
||||
|
||||
<histogram name="Storage.SharedStorage.Document.Timing.Get" units="ms"
|
||||
expires_after="2024-07-31">
|
||||
<owner>cammie@chromium.org</owner>
|
||||
<owner>yaoxia@chromium.org</owner>
|
||||
<owner>chrome-ads-histograms@google.com</owner>
|
||||
<summary>
|
||||
Measures the time from start of the call to `blink::SharedStorage::get()` to
|
||||
when the callback has successfully completed. Does not measure the timing of
|
||||
calls that end in an error. Recorded in the OnceCallback executed after the
|
||||
get operation completes.
|
||||
</summary>
|
||||
</histogram>
|
||||
|
||||
<histogram name="Storage.SharedStorage.Document.Timing.Run" units="ms"
|
||||
expires_after="2024-07-31">
|
||||
<owner>cammie@chromium.org</owner>
|
||||
|
Reference in New Issue
Block a user