mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
fix(runtime): recheck retained Location origin-domain access
Keep each live window's Location identity while rechecking protected access and cached members against Window origin policy. Use native receiver brands and caller-realm cross-origin descriptors while preserving href and replace navigation. Cover both document.domain transition directions, cross-origin reflection and native Proxy identity. Record five newly passing WPT pages. Validation: cargo fmt, strict workspace clippy, and full nextest (19,506 passed; 13 skipped). Related WPT: 48/50 passed, with the remaining failure and timeout unchanged from baseline.
This commit is contained in:
@@ -2733,7 +2733,6 @@ html/browsers/history/the-location-interface/assign_before_load.html
|
||||
html/browsers/history/the-location-interface/location_hash.html
|
||||
html/browsers/history/the-location-interface/reload_document_write_onload.html
|
||||
html/browsers/history/the-location-interface/same-hash.html
|
||||
html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html
|
||||
html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html
|
||||
html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
|
||||
html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-02.html
|
||||
|
||||
@@ -6398,6 +6398,7 @@ html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/
|
||||
html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate.html
|
||||
html/browsers/history/the-history-interface/traverse-during-beforeunload.html
|
||||
html/browsers/history/the-history-interface/traverse-during-unload.html
|
||||
html/browsers/history/the-location-interface/allow_prototype_cycle_through_location.sub.html
|
||||
html/browsers/history/the-location-interface/assign-replace-from-iframe.html
|
||||
html/browsers/history/the-location-interface/assign-replace-from-top-to-nested-iframe.html
|
||||
html/browsers/history/the-location-interface/assign-with-nested-iframe.html
|
||||
@@ -6413,6 +6414,9 @@ html/browsers/history/the-location-interface/location-protocol-setter-non-broken
|
||||
html/browsers/history/the-location-interface/location-protocol-setter-sameish.html
|
||||
html/browsers/history/the-location-interface/location-protocol-setter.html
|
||||
html/browsers/history/the-location-interface/location-prototype-no-toString-valueOf.html
|
||||
html/browsers/history/the-location-interface/location-prototype-setting-cross-origin-domain.sub.html
|
||||
html/browsers/history/the-location-interface/location-prototype-setting-cross-origin.sub.html
|
||||
html/browsers/history/the-location-interface/location-prototype-setting-goes-cross-origin-domain.sub.html
|
||||
html/browsers/history/the-location-interface/location-prototype-setting-same-origin.html
|
||||
html/browsers/history/the-location-interface/location-stringifier.html
|
||||
html/browsers/history/the-location-interface/location-symbol-toprimitive.html
|
||||
@@ -6436,6 +6440,7 @@ html/browsers/history/the-location-interface/location_search.html
|
||||
html/browsers/history/the-location-interface/reload_document_write.html
|
||||
html/browsers/history/the-location-interface/replace-with-nested-iframe.html
|
||||
html/browsers/history/the-location-interface/security_location_0.htm
|
||||
html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html
|
||||
html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html
|
||||
html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html
|
||||
html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -76,6 +76,7 @@ mod navigation_traversal;
|
||||
mod navigation_traversal_execution;
|
||||
mod navigation_traversal_plan;
|
||||
mod navigation_window;
|
||||
pub(crate) use navigation_window::window_location_for_holder;
|
||||
mod navigator_runtime;
|
||||
#[cfg(test)]
|
||||
pub(crate) use navigator_runtime::{
|
||||
|
||||
@@ -5,6 +5,7 @@ use super::location_navigation::{
|
||||
use super::navigation_callbacks::{document_location_getter, document_location_setter};
|
||||
use super::*;
|
||||
|
||||
mod access;
|
||||
mod helpers;
|
||||
mod install;
|
||||
mod methods;
|
||||
@@ -12,6 +13,8 @@ mod navigation;
|
||||
mod slots;
|
||||
mod surface;
|
||||
|
||||
pub(in crate::context_bootstrap) use access::{location_target, wrap_location_object};
|
||||
|
||||
pub(super) use install::{
|
||||
build_location_constructor_template, build_location_runtime_object,
|
||||
install_location_runtime_state, location_belongs_to_current_local_window,
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
use super::*;
|
||||
use crate::native_bridge::window_contexts_allow_access;
|
||||
use crate::util::{get_private_value, new_null_prototype_object, set_private_value};
|
||||
use crate::web_api_interfaces;
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
const SURFACE_TARGET_SLOT: &str = "__moliLocationCrossOriginTarget";
|
||||
const REFLECT_SET_SLOT: &str = "__moliLocationReflectSet";
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain)]
|
||||
struct LocationProxyHandler<'s> {
|
||||
reflect_set: v8::Local<'s, v8::Function>,
|
||||
#[webapi(method, callback = location_proxy_set, data = self.reflect_set, length = 4)]
|
||||
set: (),
|
||||
#[webapi(method, callback = location_proxy_set_prototype, length = 2)]
|
||||
set_prototype_of: (),
|
||||
#[webapi(method, callback = location_proxy_prevent_extensions, length = 1)]
|
||||
prevent_extensions: (),
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn location_target<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
moli_webapi_declare::web_api_object_target(scope, object).unwrap_or(object)
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn wrap_location_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
target: v8::Local<'s, v8::Object>,
|
||||
) -> anyhow::Result<v8::Local<'s, v8::Object>> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let reflect_set = if let Some(function) = get_private_value(scope, global, REFLECT_SET_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
|
||||
{
|
||||
function
|
||||
} else {
|
||||
// The first Location is installed before author script. Reuse this
|
||||
// intrinsic when navigation subsequently creates another Location.
|
||||
let reflect = global
|
||||
.get(scope, v8str(scope, "Reflect").into())
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.ok_or_else(|| anyhow!("missing Reflect during Location bootstrap"))?;
|
||||
let function = reflect
|
||||
.get(scope, v8str(scope, "set").into())
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
|
||||
.ok_or_else(|| anyhow!("missing Reflect.set during Location bootstrap"))?;
|
||||
set_private_value(scope, global, REFLECT_SET_SLOT, function.into());
|
||||
function
|
||||
};
|
||||
let handler = LocationProxyHandler {
|
||||
reflect_set,
|
||||
set: (),
|
||||
set_prototype_of: (),
|
||||
prevent_extensions: (),
|
||||
}
|
||||
.bind(scope)?;
|
||||
if handler.set_prototype(scope, v8::null(scope).into()) != Some(true) {
|
||||
return Err(anyhow!("failed to initialize Location proxy handler"));
|
||||
}
|
||||
// Property access, descriptors and own keys go directly to V8's checked
|
||||
// target. A shadow target cannot hide non-configurable author expandos
|
||||
// after an origin change without violating JavaScript Proxy invariants.
|
||||
let proxy = v8::Proxy::new(scope, target, handler)
|
||||
.ok_or_else(|| anyhow!("failed to create Location proxy"))?;
|
||||
moli_webapi_declare::register_web_api_proxy(scope, proxy)?;
|
||||
Ok(proxy.into())
|
||||
}
|
||||
|
||||
fn location_proxy_set<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Ok(target) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
|
||||
return;
|
||||
};
|
||||
let caller = scope
|
||||
.get_incumbent_context()
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let scope = &mut v8::ContextScope::new(scope, caller);
|
||||
if target
|
||||
.get_creation_context(scope)
|
||||
.is_some_and(|owner| window_contexts_allow_access(caller, owner))
|
||||
{
|
||||
let Ok(reflect_set) = v8::Local::<v8::Function>::try_from(args.data()) else {
|
||||
return;
|
||||
};
|
||||
if let Some(value) = reflect_set.call(
|
||||
scope,
|
||||
v8::undefined(scope).into(),
|
||||
&[target.into(), args.get(1), args.get(2), args.get(3)],
|
||||
) {
|
||||
rv.set(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Ok(key) = v8::Local::<v8::Name>::try_from(args.get(1)) else {
|
||||
return;
|
||||
};
|
||||
if key != v8str(scope, "href") {
|
||||
crate::native_bridge::throw_cross_origin_location_security_error(scope);
|
||||
return;
|
||||
}
|
||||
let Some(surface) = surface_for(scope, target) else {
|
||||
return;
|
||||
};
|
||||
let Some(descriptor) = surface
|
||||
.get_own_property_descriptor(scope, key)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(setter) = descriptor
|
||||
.get(scope, v8str(scope, "set").into())
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Preserve Reflect.set's receiver. Interceptor callbacks only expose the
|
||||
// holder, and must not substitute it for a forged or author Proxy receiver.
|
||||
if setter.call(scope, args.get(3), &[args.get(2)]).is_some() {
|
||||
rv.set_bool(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn location_proxy_set_prototype<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Ok(target) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
|
||||
return;
|
||||
};
|
||||
let caller = scope
|
||||
.get_incumbent_context()
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let scope = &mut v8::ContextScope::new(scope, caller);
|
||||
if !target
|
||||
.get_creation_context(scope)
|
||||
.is_some_and(|owner| window_contexts_allow_access(caller, owner))
|
||||
{
|
||||
rv.set_bool(args.get(1).is_null());
|
||||
return;
|
||||
}
|
||||
if let Some(prototype) = target.get_prototype(scope) {
|
||||
rv.set_bool(prototype.strict_equals(args.get(1)));
|
||||
}
|
||||
}
|
||||
|
||||
fn location_proxy_prevent_extensions<'s>(
|
||||
_scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
rv.set_bool(false);
|
||||
}
|
||||
|
||||
// Weak entries do not retain removed frames. Each cached function keeps its
|
||||
// surface alive through callback data, so keeping either function preserves
|
||||
// both descriptor identities, as required by CrossOriginPropertyDescriptorMap.
|
||||
#[derive(Default, Clone)]
|
||||
struct CrossOriginSurfaces(Rc<RefCell<Vec<CrossOriginSurface>>>);
|
||||
|
||||
struct CrossOriginSurface {
|
||||
context: v8::Weak<v8::Context>,
|
||||
target: v8::Weak<v8::Object>,
|
||||
surface: v8::Weak<v8::Object>,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain, receiver = web_api_interfaces::Location::is_instance)]
|
||||
struct CrossOriginSurfaceDeclaration<'s> {
|
||||
surface: v8::Local<'s, v8::Object>,
|
||||
#[webapi(method = "set href", callback = cross_origin_href_setter, data = self.surface, length = 1)]
|
||||
href: (),
|
||||
#[webapi(method, callback = super::methods::location_replace_callback, data = self.surface, length = 1, readonly)]
|
||||
replace: (),
|
||||
}
|
||||
|
||||
pub(super) fn install_access_check(template: v8::Local<'_, v8::ObjectTemplate>) {
|
||||
template.set_security_token_access_check_and_handlers(
|
||||
location_access_check,
|
||||
v8::NamedPropertyHandlerConfiguration::new()
|
||||
.getter(cross_origin_getter)
|
||||
.query(cross_origin_query)
|
||||
.descriptor(cross_origin_descriptor)
|
||||
.enumerator(cross_origin_enumerator),
|
||||
v8::IndexedPropertyHandlerConfiguration::new()
|
||||
.getter(cross_origin_indexed_getter)
|
||||
.descriptor(cross_origin_indexed_descriptor)
|
||||
.enumerator(cross_origin_indexed_enumerator),
|
||||
);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn location_access_check(
|
||||
accessing_context: v8::Local<'_, v8::Context>,
|
||||
object: v8::Local<'_, v8::Object>,
|
||||
_data: v8::Local<'_, v8::Value>,
|
||||
) -> bool {
|
||||
let scope = std::pin::pin!(unsafe { v8::CallbackScope::new(accessing_context) });
|
||||
let scope = &mut scope.init();
|
||||
object
|
||||
.get_creation_context(scope)
|
||||
.is_some_and(|owner| window_contexts_allow_access(accessing_context, owner))
|
||||
}
|
||||
|
||||
pub(super) fn require_same_origin(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
object: v8::Local<'_, v8::Object>,
|
||||
) -> bool {
|
||||
let current = scope.get_current_context();
|
||||
if object
|
||||
.get_creation_context(scope)
|
||||
.is_some_and(|owner| window_contexts_allow_access(current, owner))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
crate::native_bridge::throw_dom_exception(
|
||||
scope,
|
||||
"SecurityError",
|
||||
18,
|
||||
"Blocked access to a cross-origin Location.",
|
||||
);
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) fn require_entry_origin<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
if !super::install::location_belongs_to_current_local_window(scope, object) {
|
||||
return true;
|
||||
}
|
||||
let entry = scope.get_entered_or_microtask_context();
|
||||
if object
|
||||
.get_creation_context(scope)
|
||||
.is_some_and(|owner| window_contexts_allow_access(entry, owner))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
crate::native_bridge::throw_dom_exception(
|
||||
scope,
|
||||
"SecurityError",
|
||||
18,
|
||||
"Blocked access to a cross-origin Location.",
|
||||
);
|
||||
false
|
||||
}
|
||||
|
||||
fn surface_for<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
target: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let cache = scope
|
||||
.get_slot::<CrossOriginSurfaces>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
let cache = CrossOriginSurfaces::default();
|
||||
scope.set_slot(cache.clone());
|
||||
cache
|
||||
});
|
||||
let context = scope.get_current_context();
|
||||
{
|
||||
let mut entries = cache.0.borrow_mut();
|
||||
entries.retain(|entry| {
|
||||
!entry.context.is_empty() && !entry.target.is_empty() && !entry.surface.is_empty()
|
||||
});
|
||||
for entry in entries.iter() {
|
||||
if entry.context.to_local(scope) == Some(context)
|
||||
&& entry.target.to_local(scope) == Some(target)
|
||||
{
|
||||
return entry.surface.to_local(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
let surface = new_null_prototype_object(scope);
|
||||
set_private_value(scope, surface, SURFACE_TARGET_SLOT, target.into());
|
||||
CrossOriginSurfaceDeclaration {
|
||||
surface,
|
||||
href: (),
|
||||
replace: (),
|
||||
}
|
||||
.initialize(scope, surface)
|
||||
.ok()?;
|
||||
let setter_name = v8str(scope, "set href");
|
||||
let setter = surface.get(scope, setter_name.into())?;
|
||||
surface.delete(scope, setter_name.into())?;
|
||||
crate::definitions::define_get_set_property(
|
||||
scope,
|
||||
surface,
|
||||
v8str(scope, "href").into(),
|
||||
v8::undefined(scope).into(),
|
||||
setter,
|
||||
v8::PropertyAttribute::DONT_ENUM,
|
||||
"href",
|
||||
)
|
||||
.ok()?;
|
||||
// initialize() binds the declaration prototype; the cache object must not
|
||||
// inherit page-defined getters or property descriptor fields.
|
||||
surface.set_prototype(scope, v8::null(scope).into())?;
|
||||
for key in fallback_keys(scope) {
|
||||
surface.define_own_property(
|
||||
scope,
|
||||
key,
|
||||
v8::undefined(scope).into(),
|
||||
v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_ENUM,
|
||||
)?;
|
||||
}
|
||||
cache.0.borrow_mut().push(CrossOriginSurface {
|
||||
context: v8::Weak::new(scope, context),
|
||||
target: v8::Weak::new(scope, target),
|
||||
surface: v8::Weak::new(scope, surface),
|
||||
});
|
||||
Some(surface)
|
||||
}
|
||||
|
||||
fn fallback_keys<'s>(scope: &mut v8::PinScope<'s, '_>) -> [v8::Local<'s, v8::Name>; 4] {
|
||||
[
|
||||
v8str(scope, "then").into(),
|
||||
v8::Symbol::get_to_string_tag(scope).into(),
|
||||
v8::Symbol::get_has_instance(scope).into(),
|
||||
v8::Symbol::get_is_concat_spreadable(scope).into(),
|
||||
]
|
||||
}
|
||||
|
||||
fn cross_origin_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
key: v8::Local<'s, v8::Name>,
|
||||
args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) -> v8::Intercepted {
|
||||
if key == v8str(scope, "href") {
|
||||
return v8::Intercepted::kNo;
|
||||
}
|
||||
let Some(surface) = surface_for(scope, args.holder()) else {
|
||||
return v8::Intercepted::kNo;
|
||||
};
|
||||
if surface.has_own_property(scope, key) == Some(true)
|
||||
&& let Some(value) = surface.get(scope, key.into())
|
||||
{
|
||||
rv.set(value);
|
||||
return v8::Intercepted::kYes;
|
||||
}
|
||||
v8::Intercepted::kNo
|
||||
}
|
||||
|
||||
fn cross_origin_href_setter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let value = match crate::webidl::convert::<crate::webidl::UsvString>(
|
||||
scope,
|
||||
args.get(0),
|
||||
crate::webidl::Context::member("Location", "href"),
|
||||
) {
|
||||
Ok(value) => value.0,
|
||||
Err(error) => {
|
||||
crate::webidl::throw_error(scope, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
navigate_location_object(
|
||||
scope,
|
||||
args.this(),
|
||||
LocationNavigationKind::Assign,
|
||||
Some(value),
|
||||
);
|
||||
}
|
||||
|
||||
fn cross_origin_query<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
key: v8::Local<'s, v8::Name>,
|
||||
args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Integer>,
|
||||
) -> v8::Intercepted {
|
||||
let Some(surface) = surface_for(scope, args.holder()) else {
|
||||
return v8::Intercepted::kNo;
|
||||
};
|
||||
if surface.has_own_property(scope, key) == Some(true)
|
||||
&& let Some(attributes) = surface.get_property_attributes(scope, key.into())
|
||||
{
|
||||
rv.set_int32(attributes.as_u32() as i32);
|
||||
return v8::Intercepted::kYes;
|
||||
}
|
||||
v8::Intercepted::kNo
|
||||
}
|
||||
|
||||
fn cross_origin_descriptor<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
key: v8::Local<'s, v8::Name>,
|
||||
args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) -> v8::Intercepted {
|
||||
let Some(surface) = surface_for(scope, args.holder()) else {
|
||||
return v8::Intercepted::kNo;
|
||||
};
|
||||
if surface.has_own_property(scope, key) == Some(true)
|
||||
&& let Some(descriptor) = surface.get_own_property_descriptor(scope, key)
|
||||
{
|
||||
rv.set(descriptor);
|
||||
return v8::Intercepted::kYes;
|
||||
}
|
||||
// Declining here lets V8 expose an ordinary own descriptor on the target.
|
||||
crate::native_bridge::throw_cross_origin_location_security_error(scope);
|
||||
v8::Intercepted::kYes
|
||||
}
|
||||
|
||||
fn cross_origin_enumerator<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Array>,
|
||||
) {
|
||||
let mut keys = vec![v8str(scope, "href").into(), v8str(scope, "replace").into()];
|
||||
keys.extend(fallback_keys(scope).map(v8::Local::<v8::Value>::from));
|
||||
rv.set(v8::Array::new_with_elements(scope, &keys));
|
||||
}
|
||||
|
||||
fn cross_origin_indexed_getter<'s>(
|
||||
_scope: &mut v8::PinScope<'s, '_>,
|
||||
_index: u32,
|
||||
_args: v8::PropertyCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) -> v8::Intercepted {
|
||||
v8::Intercepted::kNo
|
||||
}
|
||||
|
||||
fn cross_origin_indexed_enumerator<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::PropertyCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Array>,
|
||||
) {
|
||||
rv.set(v8::Array::new(scope, 0));
|
||||
}
|
||||
|
||||
fn cross_origin_indexed_descriptor<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_index: u32,
|
||||
_args: v8::PropertyCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) -> v8::Intercepted {
|
||||
crate::native_bridge::throw_cross_origin_location_security_error(scope);
|
||||
v8::Intercepted::kYes
|
||||
}
|
||||
@@ -40,7 +40,7 @@ enum LocationAttribute {
|
||||
}
|
||||
|
||||
#[derive(Default, WebApiObject)]
|
||||
#[webapi(interface = web_api_interfaces::Location)]
|
||||
#[webapi(interface = web_api_interfaces::Location, receiver)]
|
||||
struct LocationOwnSurfaceDeclaration {
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
@@ -209,6 +209,7 @@ fn configure_location_instance_template(
|
||||
.flags(v8::PropertyHandlerFlags::ONLY_INTERCEPT_STRINGS),
|
||||
);
|
||||
template.set_immutable_proto();
|
||||
super::access::install_access_check(template);
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn build_location_runtime_object<'s>(
|
||||
@@ -231,6 +232,7 @@ pub(in crate::context_bootstrap) fn install_location_runtime_state<'s>(
|
||||
location: v8::Local<'s, v8::Object>,
|
||||
href: &str,
|
||||
) -> Result<()> {
|
||||
let location = super::access::location_target(scope, location);
|
||||
sync_location_object_fields(scope, location, href);
|
||||
// Location's legacy-unforgeable own properties are non-configurable.
|
||||
// Window resets refresh the backing slots on the existing object without
|
||||
@@ -439,6 +441,11 @@ fn location_attribute_getter<'s>(
|
||||
attribute: LocationAttribute,
|
||||
rv: &mut v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if !super::access::require_same_origin(scope, holder)
|
||||
|| !super::access::require_entry_origin(scope, holder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(current_href) = require_location_href_slot(scope, holder) else {
|
||||
return;
|
||||
};
|
||||
@@ -522,10 +529,20 @@ fn location_writable_attribute_setter_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let holder = args.this();
|
||||
if !matches!(attribute, LocationAttribute::Href)
|
||||
&& !super::access::require_same_origin(scope, holder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(value) = v8_value_to_string(scope, args.get(0)) else {
|
||||
return;
|
||||
};
|
||||
let holder = args.this();
|
||||
if !matches!(attribute, LocationAttribute::Href)
|
||||
&& !super::access::require_entry_origin(scope, holder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if require_location_href_slot(scope, holder).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,15 @@ pub(super) fn location_assign_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if !super::access::require_same_origin(scope, args.this()) {
|
||||
return;
|
||||
}
|
||||
let Some(parsed) = webidl::parse_args::<LocationAssignArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
if require_location_href_slot(scope, args.this()).is_none() {
|
||||
if !super::access::require_entry_origin(scope, args.this())
|
||||
|| require_location_href_slot(scope, args.this()).is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
navigate_location_object(
|
||||
@@ -59,7 +64,10 @@ pub(super) fn location_reload_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if require_location_href_slot(scope, args.this()).is_none() {
|
||||
if !super::access::require_same_origin(scope, args.this())
|
||||
|| !super::access::require_entry_origin(scope, args.this())
|
||||
|| require_location_href_slot(scope, args.this()).is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
navigate_location_object_with_child_navigate_event(
|
||||
@@ -75,6 +83,11 @@ pub(super) fn location_to_string_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if !super::access::require_same_origin(scope, args.this())
|
||||
|| !super::access::require_entry_origin(scope, args.this())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(href) = require_location_href_slot(scope, args.this()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(super) fn location_ancestor_origins_slot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let object = super::access::location_target(scope, object);
|
||||
get_private_object(scope, object, LOCATION_ANCESTOR_ORIGINS_SLOT)
|
||||
}
|
||||
|
||||
@@ -18,6 +19,7 @@ pub(super) fn set_location_ancestor_origins_slot<'s>(
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
value: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
let object = super::access::location_target(scope, object);
|
||||
set_private_value(scope, object, LOCATION_ANCESTOR_ORIGINS_SLOT, value.into());
|
||||
}
|
||||
|
||||
@@ -25,6 +27,7 @@ pub(super) fn clear_location_ancestor_origins_slot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
let object = super::access::location_target(scope, object);
|
||||
let undefined = v8::undefined(scope);
|
||||
set_private_value(
|
||||
scope,
|
||||
@@ -38,6 +41,7 @@ pub(super) fn location_empty_ancestor_origins_slot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let object = super::access::location_target(scope, object);
|
||||
get_private_object(scope, object, LOCATION_EMPTY_ANCESTOR_ORIGINS_SLOT)
|
||||
}
|
||||
|
||||
@@ -46,6 +50,7 @@ pub(super) fn set_location_empty_ancestor_origins_slot<'s>(
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
value: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
let object = super::access::location_target(scope, object);
|
||||
set_private_value(
|
||||
scope,
|
||||
object,
|
||||
@@ -58,6 +63,7 @@ pub(super) fn location_relevant_document_id_slot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<u64> {
|
||||
let object = super::access::location_target(scope, object);
|
||||
let value = get_private_value(scope, object, LOCATION_RELEVANT_DOCUMENT_ID_SLOT)?;
|
||||
let value = v8::Local::<v8::BigInt>::try_from(value).ok()?;
|
||||
let (document_id, lossless) = value.u64_value();
|
||||
@@ -69,6 +75,7 @@ pub(super) fn set_location_relevant_document_id_slot<'s>(
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
document_id: u64,
|
||||
) {
|
||||
let object = super::access::location_target(scope, object);
|
||||
let value = v8::BigInt::new_from_u64(scope, document_id);
|
||||
set_private_value(
|
||||
scope,
|
||||
@@ -82,6 +89,7 @@ pub(super) fn location_relevant_local_window_id_slot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<u64> {
|
||||
let object = super::access::location_target(scope, object);
|
||||
let value = get_private_value(scope, object, LOCATION_RELEVANT_LOCAL_WINDOW_ID_SLOT)?;
|
||||
let value = v8::Local::<v8::BigInt>::try_from(value).ok()?;
|
||||
let (local_window_id, lossless) = value.u64_value();
|
||||
@@ -93,6 +101,7 @@ pub(super) fn set_location_relevant_local_window_id_slot<'s>(
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
local_window_id: u64,
|
||||
) {
|
||||
let object = super::access::location_target(scope, object);
|
||||
let value = v8::BigInt::new_from_u64(scope, local_window_id);
|
||||
set_private_value(
|
||||
scope,
|
||||
@@ -107,6 +116,7 @@ pub(super) fn set_location_href_slot<'s>(
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
href: &str,
|
||||
) {
|
||||
let object = super::access::location_target(scope, object);
|
||||
if let Some(href) = v8_string(scope, href) {
|
||||
set_private_value(scope, object, WINDOW_LOCATION_HREF_SLOT, href.into());
|
||||
}
|
||||
@@ -116,6 +126,7 @@ pub(in crate::context_bootstrap) fn location_href_slot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<String> {
|
||||
let object = super::access::location_target(scope, object);
|
||||
get_private_value(scope, object, WINDOW_LOCATION_HREF_SLOT)
|
||||
.and_then(|value| value.to_string(scope))
|
||||
.map(|value| value.to_rust_string_lossy(scope))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::location_history_storage::WINDOW_RUNTIME_OWNER_SLOT;
|
||||
use super::location_runtime::{
|
||||
build_location_runtime_object, install_location_runtime_state,
|
||||
location_belongs_to_current_local_window, location_owner_has_current_realm,
|
||||
sync_window_location_history_navigation_runtime_surface,
|
||||
location_belongs_to_current_local_window, location_owner_has_current_realm, location_target,
|
||||
sync_window_location_history_navigation_runtime_surface, wrap_location_object,
|
||||
};
|
||||
use super::navigation_activation::{
|
||||
install_navigation_activation_runtime_state, set_navigation_current_entry,
|
||||
@@ -43,7 +43,7 @@ fn new_location_runtime_object<'s>(
|
||||
LocationRuntimeObjectDeclaration::new(window, href.to_owned())
|
||||
.initialize(scope, location)
|
||||
.map_err(|error| anyhow::anyhow!("failed to initialize Location object: {error}"))?;
|
||||
Ok(location)
|
||||
wrap_location_object(scope, location)
|
||||
}
|
||||
|
||||
pub(crate) fn install_window_location_history_navigation_runtime_state<'s>(
|
||||
@@ -87,6 +87,7 @@ pub(crate) fn reset_window_location_history_navigation_runtime_state<'s>(
|
||||
Some(_) | None => None,
|
||||
};
|
||||
if let Some(location) = location {
|
||||
let location = location_target(scope, location);
|
||||
LocationRuntimeObjectDeclaration::new(window, href.to_owned())
|
||||
.initialize(scope, location)
|
||||
.map_err(|error| anyhow::anyhow!("failed to initialize Location object: {error}"))?;
|
||||
|
||||
@@ -8,6 +8,7 @@ pub(super) fn runtime_window_owner<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let object = moli_webapi_declare::web_api_object_target(scope, object).unwrap_or(object);
|
||||
get_private_value(
|
||||
scope,
|
||||
object,
|
||||
@@ -22,6 +23,7 @@ pub(super) fn set_runtime_window_owner<'s>(
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
let object = moli_webapi_declare::web_api_object_target(scope, object).unwrap_or(object);
|
||||
set_private_value(
|
||||
scope,
|
||||
object,
|
||||
@@ -61,7 +63,7 @@ pub(super) fn runtime_top_window_owner<'s>(
|
||||
.unwrap_or_else(|| runtime_window_owner(scope, window))
|
||||
}
|
||||
|
||||
pub(super) fn window_location_for_holder<'s>(
|
||||
pub(crate) fn window_location_for_holder<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
|
||||
@@ -648,36 +648,7 @@ unsafe extern "C" fn window_access_check_callback(
|
||||
let Some(accessed_context) = accessed_object.get_creation_context(scope) else {
|
||||
return false;
|
||||
};
|
||||
if accessing_context == accessed_context {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(accessing_host_ptr) =
|
||||
crate::util::context_host_ptr_from_context_slot(accessing_context)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(accessed_host_ptr) = crate::util::context_host_ptr_from_context_slot(accessed_context)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if accessing_host_ptr != accessed_host_ptr {
|
||||
return false;
|
||||
}
|
||||
|
||||
let host = unsafe { &*accessing_host_ptr };
|
||||
if let (Some(accessing), Some(accessed)) = (
|
||||
host.window_execution_context_identity_for_access_check(accessing_context),
|
||||
host.window_execution_context_identity_for_access_check(accessed_context),
|
||||
) && host.window_execution_context_identity_is_current(accessing)
|
||||
&& host.window_execution_context_identity_is_current(accessed)
|
||||
{
|
||||
return host.window_execution_context_can_access(accessing, accessed);
|
||||
}
|
||||
|
||||
// Execution registrations retire before script-held globals do. Their
|
||||
// origin-domain and access policy still govern synchronous Window access.
|
||||
host.window_context_origins_allow_access(accessing_context, accessed_context)
|
||||
super::super::window_contexts_allow_access(accessing_context, accessed_context)
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
@@ -2273,7 +2244,10 @@ fn build_detached_cross_origin_location_proxy<'s>(
|
||||
fn new_cross_origin_location_proxy_target<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
new_null_prototype_object(scope)
|
||||
let target = new_null_prototype_object(scope);
|
||||
moli_webapi_declare::initialize_web_api_object(scope, target, "Location")
|
||||
.expect("native Location target should accept its brand");
|
||||
target
|
||||
}
|
||||
|
||||
fn wrap_cross_origin_location_proxy<'s>(
|
||||
@@ -2288,7 +2262,9 @@ fn wrap_cross_origin_location_proxy<'s>(
|
||||
}
|
||||
.bind(scope)
|
||||
.ok()?;
|
||||
set_null_prototype(scope, handler);
|
||||
let proxy = v8::Proxy::new(scope, target, handler)?;
|
||||
moli_webapi_declare::register_web_api_proxy(scope, proxy).ok()?;
|
||||
let proxy: v8::Local<'s, v8::Value> = proxy.into();
|
||||
v8::Local::<v8::Object>::try_from(proxy).ok()
|
||||
}
|
||||
@@ -2357,7 +2333,16 @@ fn child_window_cross_origin_access_surface<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
holder: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
child_window_cross_origin_handler_data(scope, holder).map(|(surface, _)| surface)
|
||||
let (surface, _) = child_window_cross_origin_handler_data(scope, holder)?;
|
||||
if let Some(location) = crate::context_bootstrap::window_location_for_holder(scope, holder) {
|
||||
set_private_value(
|
||||
scope,
|
||||
surface,
|
||||
CROSS_ORIGIN_WINDOW_LOCATION_SLOT,
|
||||
location.into(),
|
||||
);
|
||||
}
|
||||
Some(surface)
|
||||
}
|
||||
|
||||
fn child_window_cross_origin_handler_data<'s>(
|
||||
@@ -2913,12 +2898,37 @@ fn cross_origin_window_length_getter_callback<'s>(
|
||||
rv.set_uint32(count as u32);
|
||||
}
|
||||
|
||||
fn live_location_for_cross_origin_window<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if crate::web_api_interfaces::Window::is_instance(scope, receiver) {
|
||||
return crate::context_bootstrap::window_location_for_holder(scope, receiver);
|
||||
}
|
||||
get_cross_origin_proxy_private_value(scope, receiver, CROSS_ORIGIN_WINDOW_LOCATION_SLOT)?;
|
||||
let dispatch_scope = if let Some(popup_id) = cross_origin_lightweight_popup_id(scope, receiver)
|
||||
{
|
||||
super::super::OwnerDispatchScope::LightweightPopup(popup_id)
|
||||
} else if is_cross_origin_top_window_proxy(scope, receiver) {
|
||||
super::super::OwnerDispatchScope::Top
|
||||
} else {
|
||||
super::super::OwnerDispatchScope::Child(child_handle_from_object(scope, receiver)?)
|
||||
};
|
||||
let host_ptr = context_host_ptr_from_global_bridge(scope)?;
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
let owner = host.current_window_execution_context_owner(dispatch_scope)?;
|
||||
let (_, context) = host.window_execution_context(scope, owner, dispatch_scope)?;
|
||||
crate::context_bootstrap::window_location_for_holder(scope, context.global(scope))
|
||||
}
|
||||
|
||||
fn cross_origin_window_location_getter_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if let Some(location) =
|
||||
if let Some(location) = live_location_for_cross_origin_window(scope, args.this()) {
|
||||
rv.set(location.into());
|
||||
} else if let Some(location) =
|
||||
get_cross_origin_proxy_private_value(scope, args.this(), CROSS_ORIGIN_WINDOW_LOCATION_SLOT)
|
||||
{
|
||||
rv.set(location);
|
||||
|
||||
@@ -165,6 +165,7 @@ mod websockets;
|
||||
mod window_document_tasks;
|
||||
mod window_execution_context;
|
||||
mod window_security_tokens;
|
||||
pub(crate) use window_security_tokens::window_contexts_allow_access;
|
||||
mod workers;
|
||||
use window_security_tokens::DocumentDomainState;
|
||||
pub(crate) use window_security_tokens::set_window_security_token;
|
||||
|
||||
@@ -9,6 +9,38 @@ use std::{cell::RefCell, rc::Rc};
|
||||
const WINDOW_SECURITY_TOKEN_PREFIX: &str = "moli-window-origin-v1:";
|
||||
const WINDOW_ISOLATED_WORLD_SECURITY_TOKEN_PREFIX: &str = "moli-window-isolated-origin-v1:";
|
||||
|
||||
pub(crate) fn window_contexts_allow_access(
|
||||
accessing_context: v8::Local<'_, v8::Context>,
|
||||
accessed_context: v8::Local<'_, v8::Context>,
|
||||
) -> bool {
|
||||
if accessing_context == accessed_context {
|
||||
return true;
|
||||
}
|
||||
let Some(accessing_host_ptr) =
|
||||
crate::util::context_host_ptr_from_context_slot(accessing_context)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(accessed_host_ptr) = crate::util::context_host_ptr_from_context_slot(accessed_context)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if accessing_host_ptr != accessed_host_ptr {
|
||||
return false;
|
||||
}
|
||||
let host = unsafe { &*accessing_host_ptr };
|
||||
if let (Some(accessing), Some(accessed)) = (
|
||||
host.window_execution_context_identity_for_access_check(accessing_context),
|
||||
host.window_execution_context_identity_for_access_check(accessed_context),
|
||||
) && host.window_execution_context_identity_is_current(accessing)
|
||||
&& host.window_execution_context_identity_is_current(accessed)
|
||||
{
|
||||
return host.window_execution_context_can_access(accessing, accessed);
|
||||
}
|
||||
// Script can retain objects after their execution registrations retire.
|
||||
host.window_context_origins_allow_access(accessing_context, accessed_context)
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(in crate::native_bridge::context_host) struct DocumentDomainState(Rc<DocumentDomainStateData>);
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ pub(crate) use context_host::{
|
||||
lightweight_popup_id_from_window, restore_active_lightweight_popup_scope,
|
||||
restore_deferred_active_lightweight_popup_scope_if_present,
|
||||
throw_cross_origin_location_security_error, throw_cross_origin_type_error,
|
||||
window_contexts_allow_access,
|
||||
};
|
||||
|
||||
pub(crate) const ACTIVE_CHILD_WINDOW_HANDLE_SLOT: &str = "__moliActiveChildWindowHandle";
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn retained_location_rechecks_origin_domain_access() {
|
||||
for parent_first in [false, true] {
|
||||
let server = StaticHttpServer::spawn_with_bodies(vec![
|
||||
"<!doctype html><body>Location target</body>".to_owned(); 2
|
||||
])
|
||||
.await;
|
||||
let loader = static_http_loader([server.resolve_entry("www.example.test")]);
|
||||
let parent_url = server.url_for_host("www.example.test", "/page.html");
|
||||
let mut vm =
|
||||
new_storage_page_task_executor_test_vm_with_loader(parent_url.as_str(), &loader);
|
||||
let script = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/retained-location-origin.js"
|
||||
));
|
||||
vm.exec(
|
||||
&format!(
|
||||
r#"
|
||||
if (!document.documentElement) document.appendChild(document.createElement('html'));
|
||||
if (!document.body) document.documentElement.appendChild(document.createElement('body'));
|
||||
globalThis.__retainedLocationResult = null;
|
||||
({script})({{parentFirst: {parent_first}}}).then(
|
||||
result => {{ globalThis.__retainedLocationResult = result; }},
|
||||
error => {{ globalThis.__retainedLocationResult = {{error: String(error)}}; }}
|
||||
);
|
||||
"#,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.expect("retained Location probe should start");
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(__retainedLocationResult !== null)",
|
||||
"true",
|
||||
"retained Location probe should finish",
|
||||
)
|
||||
.await;
|
||||
let result: serde_json::Value = serde_json::from_str(
|
||||
&vm.eval("JSON.stringify(__retainedLocationResult)")
|
||||
.expect("retained Location observations"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result["checks"], 143,
|
||||
"parent_first={parent_first}: {result}"
|
||||
);
|
||||
assert_eq!(
|
||||
result["failures"],
|
||||
serde_json::json!([]),
|
||||
"parent_first={parent_first}: {result}"
|
||||
);
|
||||
assert_eq!(
|
||||
server.finish_targets().await,
|
||||
vec!["/child.html", "/peer.html"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn retained_child_window_origin_rebinds_default_and_isolated_realms() {
|
||||
let server = StaticHttpServer::spawn_with_bodies(vec![
|
||||
|
||||
@@ -182,6 +182,13 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[
|
||||
1,
|
||||
DirectCallOwner::NativeForwardingOrScript,
|
||||
),
|
||||
// Location's native Proxy forwards to a captured Reflect.set intrinsic or
|
||||
// its generated, receiver-checked href setter. Neither is an author callback.
|
||||
allowed(
|
||||
"context_bootstrap/location_runtime/access.rs",
|
||||
2,
|
||||
DirectCallOwner::NativeForwardingOrScript,
|
||||
),
|
||||
allowed(
|
||||
"context_bootstrap/runtime_state.rs",
|
||||
1,
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
async ({parentFirst = false} = {}) => {
|
||||
const result = {checks: 0, failures: [], observations: []};
|
||||
const check = (name, action, expected) => {
|
||||
let actual;
|
||||
try { actual = action(); } catch (error) { actual = error.name; }
|
||||
result.checks++;
|
||||
if (actual !== expected) result.failures.push({name, actual, expected});
|
||||
};
|
||||
const load = async path => {
|
||||
const frame = document.createElement('iframe');
|
||||
const loaded = new Promise(resolve => frame.onload = resolve);
|
||||
frame.src = path;
|
||||
document.body.append(frame);
|
||||
await loaded;
|
||||
return frame;
|
||||
};
|
||||
const frame = await load('/child.html#seed');
|
||||
const peerFrame = await load('/peer.html');
|
||||
const child = frame.contentWindow;
|
||||
const childDocument = child.document;
|
||||
const parentLocationIdentity = child.Function('expected', 'return parent.location === expected').bind(null, location);
|
||||
const parentLocationRead = child.Function('held', 'try { return held.host; } catch (error) { return error.name; }').bind(null, location);
|
||||
const peer = peerFrame.contentWindow;
|
||||
const peerDocument = peer.document;
|
||||
const held = child.location;
|
||||
const href = held.href;
|
||||
const proto = Object.getPrototypeOf(held);
|
||||
const descriptors = Object.getOwnPropertyDescriptors(held);
|
||||
const peerRead = peer.Function('target', 'return target.replace');
|
||||
const peerFunctionPrototype = peer.Function.prototype;
|
||||
const parentExceptionPrototype = DOMException.prototype;
|
||||
const childExceptionPrototype = child.DOMException.prototype;
|
||||
const childTypeErrorPrototype = child.TypeError.prototype;
|
||||
const ownReplace = held.replace;
|
||||
const childObjectPrototype = child.Object.prototype;
|
||||
childObjectPrototype.get = () => 'polluted';
|
||||
check('handler-does-not-inherit-get', () => held.href, href);
|
||||
delete childObjectPrototype.get;
|
||||
childObjectPrototype.getPrototypeOf = () => null;
|
||||
check('handler-does-not-inherit-get-prototype', () => Object.getPrototypeOf(held) === proto, true);
|
||||
delete childObjectPrototype.getPrototypeOf;
|
||||
const symbol = Symbol('retained-location');
|
||||
held.marker = 42;
|
||||
held[0] = 'index';
|
||||
held[4294967295] = 'non-index';
|
||||
held[symbol] = 'symbol';
|
||||
held.then = 'author then';
|
||||
Object.defineProperty(held, 'locked', {value: 123});
|
||||
let expandoReads = 0;
|
||||
Object.defineProperty(held, 'accessor', {
|
||||
get() { expandoReads++; return 1; }, configurable: true
|
||||
});
|
||||
check('initial-identity', () => held === child.location, true);
|
||||
check('initial-marker', () => held.marker, 42);
|
||||
if (parentFirst) {
|
||||
document.domain = 'example.test';
|
||||
peerDocument.domain = 'example.test';
|
||||
} else {
|
||||
childDocument.domain = 'example.test';
|
||||
}
|
||||
check('window-access-denied', () => child.status, 'SecurityError');
|
||||
check('parent-location-identity', () => parentLocationIdentity(), true);
|
||||
check('retained-parent-location-security', () => parentLocationRead(), 'SecurityError');
|
||||
check('location-identity-survives-origin-change', () => child.location === held, true);
|
||||
const crossWindowLocationGetter = Object.getOwnPropertyDescriptor(child, 'location').get;
|
||||
check('cross-window-location-getter', () => crossWindowLocationGetter.call(child) === held, true);
|
||||
check('cross-window-location-rejects-document', () => crossWindowLocationGetter.call(document), 'TypeError');
|
||||
for (const key of ['marker', 'locked', 'accessor', 'missing', 0, 4294967295, symbol,
|
||||
'href', 'host', 'hostname', 'port', 'protocol', 'hash', 'search', 'pathname',
|
||||
'origin', 'ancestorOrigins', 'assign', 'reload', 'toString', 'valueOf', Symbol.toPrimitive]) {
|
||||
check(`read:${String(key)}`, () => held[key], 'SecurityError');
|
||||
}
|
||||
check('blocked-expando-getter-not-called', () => expandoReads, 0);
|
||||
for (const key of ['marker', 0, symbol, 'then', 'replace']) {
|
||||
check(`set:${String(key)}`, () => Reflect.set(held, key, 7), 'SecurityError');
|
||||
check(`define:${String(key)}`, () => Reflect.defineProperty(held, key, {value: 7}), 'SecurityError');
|
||||
check(`delete:${String(key)}`, () => Reflect.deleteProperty(held, key), 'SecurityError');
|
||||
}
|
||||
check('has-expando', () => 'marker' in held, 'SecurityError');
|
||||
check('has-href', () => 'href' in held, true);
|
||||
check('has-fallback', () => 'then' in held, true);
|
||||
check('descriptor-expando', () => Object.getOwnPropertyDescriptor(held, 'marker'), 'SecurityError');
|
||||
for (const key of ['locked', 'missing', 0, symbol]) {
|
||||
check(`descriptor:${String(key)}`, () => Object.getOwnPropertyDescriptor(held, key), 'SecurityError');
|
||||
}
|
||||
check('cross-origin-prototype', () => Object.getPrototypeOf(held), null);
|
||||
check('set-null-prototype', () => Reflect.setPrototypeOf(held, null), true);
|
||||
check('set-original-prototype', () => Reflect.setPrototypeOf(held, proto), false);
|
||||
check('extensible', () => Reflect.isExtensible(held), true);
|
||||
check('prevent-extensions', () => Reflect.preventExtensions(held), false);
|
||||
check('own-keys', () => Reflect.ownKeys(held).map(String).join('|'),
|
||||
'href|replace|then|Symbol(Symbol.toStringTag)|Symbol(Symbol.hasInstance)|Symbol(Symbol.isConcatSpreadable)');
|
||||
check('enumerable-keys', () => Object.keys(held).join('|'), '');
|
||||
for (const key of ['then', Symbol.toStringTag, Symbol.hasInstance, Symbol.isConcatSpreadable]) {
|
||||
check(`fallback:${String(key)}`, () => held[key] === undefined, true);
|
||||
}
|
||||
check('href-descriptor', () => {
|
||||
const d = Object.getOwnPropertyDescriptor(held, 'href');
|
||||
return [typeof d.get, typeof d.set, d.enumerable, d.configurable,
|
||||
Object.getPrototypeOf(d.set) === Function.prototype, d.set.name, d.set.length].join('|');
|
||||
}, 'undefined|function|false|true|true|set href|1');
|
||||
check('replace-descriptor', () => {
|
||||
const d = Object.getOwnPropertyDescriptor(held, 'replace');
|
||||
return [typeof d.value, d.enumerable, d.configurable, d.writable,
|
||||
Object.getPrototypeOf(d.value) === Function.prototype, d.value.name, d.value.length].join('|');
|
||||
}, 'function|false|true|false|true|replace|1');
|
||||
check('cross-replace-cached', () => held.replace === held.replace, true);
|
||||
check('cross-replace-new-function', () => held.replace !== ownReplace, true);
|
||||
check('cross-replace-descriptor-cached', () => held.replace === Object.getOwnPropertyDescriptor(held, 'replace').value, true);
|
||||
check('cross-setter-cached', () => Object.getOwnPropertyDescriptor(held, 'href').set === Object.getOwnPropertyDescriptor(held, 'href').set, true);
|
||||
check('peer-cross-replace-realm', () => Object.getPrototypeOf(peerRead(held)) === peerFunctionPrototype, true);
|
||||
check('peer-cross-replace-cached', () => peerRead(held) === peerRead(held), true);
|
||||
check('distinct-cross-caller-functions', () => peerRead(held) !== held.replace, true);
|
||||
for (const [key, d] of Object.entries(descriptors)) {
|
||||
if (d.get) check(`cached-getter:${key}`, () => d.get.call(held), 'SecurityError');
|
||||
}
|
||||
check('cached-stringifier', () => descriptors.toString.value.call(held), 'SecurityError');
|
||||
check('cached-hash-setter', () => { descriptors.hash.set.call(held, '#seed'); return 'accepted'; }, 'SecurityError');
|
||||
for (const [label, action, expectedPrototype] of [
|
||||
['property-security-realm', () => held.host, parentExceptionPrototype],
|
||||
['cached-getter-security-realm', () => descriptors.host.get.call(held), childExceptionPrototype]
|
||||
]) {
|
||||
check(label, () => {
|
||||
try { action(); return false; }
|
||||
catch (error) { return error.name === 'SecurityError' && Object.getPrototypeOf(error) === expectedPrototype; }
|
||||
}, true);
|
||||
}
|
||||
let conversions = 0;
|
||||
const sentinel = {name: 'ConversionSentinel'};
|
||||
const poison = {toString() { conversions++; throw sentinel; }};
|
||||
for (const [label, action, expectedConversions] of [
|
||||
['direct-hash', () => { held.hash = poison; }, 0],
|
||||
['direct-href', () => { held.href = poison; }, 1],
|
||||
['direct-replace', () => held.replace(poison), 1],
|
||||
['cached-assign', () => descriptors.assign.value.call(held, poison), 1],
|
||||
['cached-hash', () => descriptors.hash.set.call(held, poison), 1]
|
||||
]) {
|
||||
const before = conversions;
|
||||
check(`conversion:${label}`, () => { action(); return 'accepted'; }, expectedConversions ? 'ConversionSentinel' : 'SecurityError');
|
||||
check(`conversion-count:${label}`, () => conversions - before, expectedConversions);
|
||||
}
|
||||
for (const receiver of [{}, Object.create(held), new Proxy(held, {}), 1, null]) {
|
||||
const before = conversions;
|
||||
check('reflect-cross-setter-brand', () => Reflect.set(held, 'href', poison, receiver), 'TypeError');
|
||||
check('reflect-cross-setter-conversion', () => conversions, before);
|
||||
}
|
||||
const revoked = Proxy.revocable(held, {});
|
||||
revoked.revoke();
|
||||
for (const receiver of [{}, Object.create(held), new Proxy(held, {}), revoked.proxy]) {
|
||||
const before = conversions;
|
||||
check('cached-getter-brand', () => {
|
||||
try { descriptors.href.get.call(receiver); return false; }
|
||||
catch (error) { return Object.getPrototypeOf(error) === childTypeErrorPrototype; }
|
||||
}, true);
|
||||
check('cached-setter-brand', () => descriptors.href.set.call(receiver, poison), 'TypeError');
|
||||
check('cached-method-brand', () => descriptors.replace.value.call(receiver, poison), 'TypeError');
|
||||
check('brand-before-conversion', () => conversions, before);
|
||||
}
|
||||
const waitForHref = async target => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (childDocument.URL === target) return true;
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const [label, navigate] of [
|
||||
['href', url => { held.href = url; }],
|
||||
['replace', url => held.replace(url)],
|
||||
['cached-href', url => descriptors.href.set.call(held, url)],
|
||||
['cached-replace', url => descriptors.replace.value.call(held, url)]
|
||||
]) {
|
||||
const target = new URL(href);
|
||||
target.hash = label;
|
||||
check(`allowed-navigation:${label}`, () => { navigate(target.href); return true; }, true);
|
||||
const reached = await waitForHref(target.href);
|
||||
check(`navigation-completed:${label}`, () => reached, true);
|
||||
}
|
||||
check('cached-assign', () => { descriptors.assign.value.call(held, href); return 'accepted'; }, 'SecurityError');
|
||||
check('cached-reload', () => { descriptors.reload.value.call(held); return 'accepted'; }, 'SecurityError');
|
||||
check('cached-method-security-realm', () => {
|
||||
try { descriptors.reload.value.call(held); return false; }
|
||||
catch (error) { return error.name === 'SecurityError' && Object.getPrototypeOf(error) === childExceptionPrototype; }
|
||||
}, true);
|
||||
if (parentFirst) childDocument.domain = 'example.test';
|
||||
else document.domain = 'example.test';
|
||||
check('same-origin-restored-identity', () => child.location === held, true);
|
||||
check('same-origin-restored-marker', () => held.marker, 42);
|
||||
check('same-origin-restored-locked', () => held.locked, 123);
|
||||
check('same-origin-restored-prototype', () => Object.getPrototypeOf(held) === proto, true);
|
||||
check('same-origin-restored-method', () => held.replace === ownReplace, true);
|
||||
check('same-origin-restored-getter', () => descriptors.href.get.call(held) === childDocument.URL, true);
|
||||
frame.remove();
|
||||
peerFrame.remove();
|
||||
return result;
|
||||
}
|
||||
@@ -174,6 +174,18 @@ pub fn web_api_object_type<'s>(
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the native backing object for a branded instance. Only explicitly
|
||||
/// registered native Proxies share their target; author and revoked Proxies
|
||||
/// are rejected without invoking traps.
|
||||
pub fn web_api_object_target<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let registry = scope.get_slot::<Rc<RefCell<TypeRegistry>>>().cloned()?;
|
||||
object_type_id(scope, object, ®istry)?;
|
||||
native_identity_target(scope, object, ®istry)
|
||||
}
|
||||
|
||||
pub fn implements_interface<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -279,7 +279,7 @@ macro_rules! web_api_constructor {
|
||||
|
||||
pub use brand::{
|
||||
WebApiType, implements_interface, initialize_web_api_object, register_web_api_interfaces,
|
||||
register_web_api_proxy, web_api_object_type,
|
||||
register_web_api_proxy, web_api_object_target, web_api_object_type,
|
||||
};
|
||||
|
||||
pub use declaration::{
|
||||
|
||||
@@ -206,17 +206,41 @@ fn only_explicitly_registered_native_proxies_share_target_identity() {
|
||||
let native = v8::Proxy::new(scope, target, handler).unwrap();
|
||||
let native_object = v8::Local::<v8::Object>::from(native);
|
||||
assert_eq!(web_api_object_type(scope, native_object), None);
|
||||
assert_eq!(
|
||||
moli_webapi_declare::web_api_object_target(scope, native_object),
|
||||
None
|
||||
);
|
||||
moli_webapi_declare::register_web_api_proxy(scope, native).unwrap();
|
||||
assert!(implements_interface(scope, native_object, "TestBase"));
|
||||
assert_eq!(
|
||||
moli_webapi_declare::web_api_object_target(scope, native_object),
|
||||
Some(target)
|
||||
);
|
||||
assert_eq!(
|
||||
moli_webapi_declare::web_api_object_target(scope, target),
|
||||
Some(target)
|
||||
);
|
||||
initialize_web_api_object(scope, native_object, "TestBase").unwrap();
|
||||
let impostor = v8::Proxy::new(scope, target, handler).unwrap();
|
||||
assert_eq!(web_api_object_type(scope, impostor.into()), None);
|
||||
assert_eq!(
|
||||
moli_webapi_declare::web_api_object_target(scope, impostor.into()),
|
||||
None
|
||||
);
|
||||
let outer_handler = v8::Object::new(scope);
|
||||
let outer = v8::Proxy::new(scope, native_object, outer_handler).unwrap();
|
||||
assert_eq!(web_api_object_type(scope, outer.into()), None);
|
||||
assert_eq!(
|
||||
moli_webapi_declare::web_api_object_target(scope, outer.into()),
|
||||
None
|
||||
);
|
||||
assert!(moli_webapi_declare::register_web_api_proxy(scope, outer).is_err());
|
||||
native.revoke();
|
||||
assert_eq!(web_api_object_type(scope, native_object), None);
|
||||
assert_eq!(
|
||||
moli_webapi_declare::web_api_object_target(scope, native_object),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(moli_webapi_declare::WebApiFunctionTemplate)]
|
||||
|
||||
Reference in New Issue
Block a user