diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 5343409d3a..ec9014ba07 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -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 diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index ea5c32f1c0..7e7c53cd45 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -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 diff --git a/moli-core/tests/history_child.rs b/moli-core/tests/history_child.rs index 4e791b75d2..50fb1fe73b 100644 --- a/moli-core/tests/history_child.rs +++ b/moli-core/tests/history_child.rs @@ -5127,7 +5127,14 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> { 'replace',\ '__moliChildBrowsingContextHandle',\ 'unknownCrossOriginProbe'\ - ].map((name) => `${name}:${name in win.location}:${Object.prototype.hasOwnProperty.call(win.location, name)}`);\ + ].map((name) => {\ + const observe = operation => {\ + try { return operation(); }\ + catch (error) { return `${error && error.name}:${error instanceof DOMException}`; }\ + };\ + return [name, observe(() => name in win.location),\ + observe(() => Object.prototype.hasOwnProperty.call(win.location, name))];\ + });\ const calls = ['blur', 'focus', 'close'].map((name) => {\ try {\ return `${name}:${String(win[name]())}`;\ @@ -5185,7 +5192,13 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> { });\ const locationDescriptor = Object.getOwnPropertyDescriptor(win, 'location');\ const locationHrefDescriptor = Object.getOwnPropertyDescriptor(win.location, 'href');\ - const locationHashDescriptor = Object.getOwnPropertyDescriptor(win.location, 'hash');\ + let locationHashDescriptor;\ + try {\ + Object.getOwnPropertyDescriptor(win.location, 'hash');\ + locationHashDescriptor = 'read';\ + } catch (error) {\ + locationHashDescriptor = `${error && error.name}:${error instanceof DOMException}`;\ + }\ const postMessageDescriptor = Object.getOwnPropertyDescriptor(win, 'postMessage');\ const noopDescriptors = ['blur', 'focus', 'close'].map((name) => {\ const descriptor = Object.getOwnPropertyDescriptor(win, name);\ @@ -5272,12 +5285,7 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> { getterType: typeof locationHrefDescriptor?.get,\ setterType: typeof locationHrefDescriptor?.set\ },\ - locationHashDescriptor: {\ - enumerable: locationHashDescriptor?.enumerable,\ - configurable: locationHashDescriptor?.configurable,\ - getterType: typeof locationHashDescriptor?.get,\ - setterType: typeof locationHashDescriptor?.set\ - },\ + locationHashDescriptor,\ postMessageDescriptor: {\ enumerable: postMessageDescriptor?.enumerable,\ configurable: postMessageDescriptor?.configurable,\ @@ -5322,7 +5330,7 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> { assert_eq!( result, Some( - r#"{"self":true,"window":true,"frames":true,"parent":true,"top":true,"opener":true,"thenType":"undefined","length":3,"closed":false,"blurType":"function","focusType":"function","closeType":"function","postMessageType":"function","restrictedMutationProbe":["deleteDocument:SecurityError","deleteSetTimeout:SecurityError","defineDocument:SecurityError","definePostMessage:SecurityError","deleteLocationHref:SecurityError","defineLocationHref:SecurityError"],"hasProbe":["document:true:true","setTimeout:true:true","postMessage:true:true","location:true:true","self:true:true","window:true:true","frames:true:true","parent:true:true","top:true:true","closed:true:true","opener:true:true","then:true:true","__moliChildBrowsingContextHandle:SecurityError:true","__moliCrossOriginWindowLocation:SecurityError:true","unknownCrossOriginProbe:SecurityError:true"],"locationHasProbe":["href:true:true","hash:true:true","replace:true:true","__moliChildBrowsingContextHandle:false:false","unknownCrossOriginProbe:false:false"],"calls":["blur:undefined","focus:undefined","close:undefined"],"invalidNoopReceivers":["blur:TypeError:true","focus:TypeError:true","close:TypeError:true"],"postMessageWindowReceiver":"ok","postMessageInvalidReceiver":"TypeError:true","ownNamesLeakInternal":false,"ownKeysLeakInternal":false,"locationOwnNamesLeakInternal":false,"accessorDescriptors":[["window",false,true,["function","get window",0],["undefined",null,null],true,true],["self",false,true,["function","get self",0],["undefined",null,null],true,true],["location",false,true,["function","get location",0],["function","set location",1],true,true],["closed",false,true,["function","get closed",0],["undefined",null,null],true,true],["frames",false,true,["function","get frames",0],["undefined",null,null],true,true],["length",false,true,["function","get length",0],["undefined",null,null],true,true],["top",false,true,["function","get top",0],["undefined",null,null],true,true],["opener",false,true,["function","get opener",0],["undefined",null,null],true,true],["parent",false,true,["function","get parent",0],["undefined",null,null],true,true]],"locationHrefDescriptor":{"enumerable":false,"configurable":false,"getterType":"undefined","setterType":"function"},"locationHashDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"postMessageDescriptor":{"enumerable":false,"configurable":false,"writable":false,"valueType":"function","valueName":"postMessage","valueLength":1},"noopDescriptors":["blur:false:false:false:function:blur:0","focus:false:false:false:function:focus:0","close:false:false:false:function:close:0"],"locationReplaceDescriptor":{"enumerable":false,"configurable":false,"writable":false,"valueType":"function","valueName":"replace","valueLength":1},"locationReplaceInvalidReceiver":"TypeError:true","locationReplaceForgedReceiver":"TypeError:true","locationHrefSetterInvalidReceiver":"TypeError:true","locationGetterInvalidReceiver":"TypeError:true","setTimeoutDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"documentDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"windowLocationAssignResult":"ok","locationStableAfterWindowAssign":true,"deniedWindowProbe":["document:SecurityError:true","frameElement:SecurityError:true","history:SecurityError:true","navigation:SecurityError:true","localStorage:SecurityError:true","sessionStorage:SecurityError:true","indexedDB:SecurityError:true","customElements:SecurityError:true","navigator:SecurityError:true","performance:SecurityError:true","console:SecurityError:true","screen:SecurityError:true","visualViewport:SecurityError:true","crypto:SecurityError:true","caches:SecurityError:true","clientInformation:SecurityError:true","cookieStore:SecurityError:true","credentialless:SecurityError:true","crossOriginIsolated:SecurityError:true","globalThis:SecurityError:true","documentPictureInPicture:SecurityError:true","fetch:SecurityError:true","isSecureContext:SecurityError:true","origin:SecurityError:true","originAgentCluster:SecurityError:true","scheduler:SecurityError:true","speechSynthesis:SecurityError:true","structuredClone:SecurityError:true","trustedTypes:SecurityError:true","setTimeout:SecurityError:true","clearImmediate:SecurityError:true","addEventListener:SecurityError:true","dispatchEvent:SecurityError:true","queueMicrotask:SecurityError:true","requestAnimationFrame:SecurityError:true","getComputedStyle:SecurityError:true","getSelection:SecurityError:true","matchMedia:SecurityError:true","event:SecurityError:true","onerror:SecurityError:true","innerWidth:SecurityError:true","innerHeight:SecurityError:true","devicePixelRatio:SecurityError:true","scrollX:SecurityError:true","pageYOffset:SecurityError:true","scrollTo:SecurityError:true","open:SecurityError:true","stop:SecurityError:true","print:SecurityError:true","find:SecurityError:true","alert:SecurityError:true","confirm:SecurityError:true","prompt:SecurityError:true","reportError:SecurityError:true","btoa:SecurityError:true","atob:SecurityError:true"],"documentAccess":"SecurityError:true"}"#.to_owned(), + r#"{"self":true,"window":true,"frames":true,"parent":true,"top":true,"opener":true,"thenType":"undefined","length":3,"closed":false,"blurType":"function","focusType":"function","closeType":"function","postMessageType":"function","restrictedMutationProbe":["deleteDocument:SecurityError","deleteSetTimeout:SecurityError","defineDocument:SecurityError","definePostMessage:SecurityError","deleteLocationHref:SecurityError","defineLocationHref:SecurityError"],"hasProbe":["document:true:true","setTimeout:true:true","postMessage:true:true","location:true:true","self:true:true","window:true:true","frames:true:true","parent:true:true","top:true:true","closed:true:true","opener:true:true","then:true:true","__moliChildBrowsingContextHandle:SecurityError:true","__moliCrossOriginWindowLocation:SecurityError:true","unknownCrossOriginProbe:SecurityError:true"],"locationHasProbe":[["href",true,true],["hash","SecurityError:true","SecurityError:true"],["replace",true,true],["__moliChildBrowsingContextHandle","SecurityError:true","SecurityError:true"],["unknownCrossOriginProbe","SecurityError:true","SecurityError:true"]],"calls":["blur:undefined","focus:undefined","close:undefined"],"invalidNoopReceivers":["blur:TypeError:true","focus:TypeError:true","close:TypeError:true"],"postMessageWindowReceiver":"ok","postMessageInvalidReceiver":"TypeError:true","ownNamesLeakInternal":false,"ownKeysLeakInternal":false,"locationOwnNamesLeakInternal":false,"accessorDescriptors":[["window",false,true,["function","get window",0],["undefined",null,null],true,true],["self",false,true,["function","get self",0],["undefined",null,null],true,true],["location",false,true,["function","get location",0],["function","set location",1],true,true],["closed",false,true,["function","get closed",0],["undefined",null,null],true,true],["frames",false,true,["function","get frames",0],["undefined",null,null],true,true],["length",false,true,["function","get length",0],["undefined",null,null],true,true],["top",false,true,["function","get top",0],["undefined",null,null],true,true],["opener",false,true,["function","get opener",0],["undefined",null,null],true,true],["parent",false,true,["function","get parent",0],["undefined",null,null],true,true]],"locationHrefDescriptor":{"enumerable":false,"configurable":true,"getterType":"undefined","setterType":"function"},"locationHashDescriptor":"SecurityError:true","postMessageDescriptor":{"enumerable":false,"configurable":false,"writable":false,"valueType":"function","valueName":"postMessage","valueLength":1},"noopDescriptors":["blur:false:false:false:function:blur:0","focus:false:false:false:function:focus:0","close:false:false:false:function:close:0"],"locationReplaceDescriptor":{"enumerable":false,"configurable":true,"writable":false,"valueType":"function","valueName":"replace","valueLength":1},"locationReplaceInvalidReceiver":"TypeError:true","locationReplaceForgedReceiver":"TypeError:true","locationHrefSetterInvalidReceiver":"TypeError:true","locationGetterInvalidReceiver":"TypeError:true","setTimeoutDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"documentDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"windowLocationAssignResult":"ok","locationStableAfterWindowAssign":true,"deniedWindowProbe":["document:SecurityError:true","frameElement:SecurityError:true","history:SecurityError:true","navigation:SecurityError:true","localStorage:SecurityError:true","sessionStorage:SecurityError:true","indexedDB:SecurityError:true","customElements:SecurityError:true","navigator:SecurityError:true","performance:SecurityError:true","console:SecurityError:true","screen:SecurityError:true","visualViewport:SecurityError:true","crypto:SecurityError:true","caches:SecurityError:true","clientInformation:SecurityError:true","cookieStore:SecurityError:true","credentialless:SecurityError:true","crossOriginIsolated:SecurityError:true","globalThis:SecurityError:true","documentPictureInPicture:SecurityError:true","fetch:SecurityError:true","isSecureContext:SecurityError:true","origin:SecurityError:true","originAgentCluster:SecurityError:true","scheduler:SecurityError:true","speechSynthesis:SecurityError:true","structuredClone:SecurityError:true","trustedTypes:SecurityError:true","setTimeout:SecurityError:true","clearImmediate:SecurityError:true","addEventListener:SecurityError:true","dispatchEvent:SecurityError:true","queueMicrotask:SecurityError:true","requestAnimationFrame:SecurityError:true","getComputedStyle:SecurityError:true","getSelection:SecurityError:true","matchMedia:SecurityError:true","event:SecurityError:true","onerror:SecurityError:true","innerWidth:SecurityError:true","innerHeight:SecurityError:true","devicePixelRatio:SecurityError:true","scrollX:SecurityError:true","pageYOffset:SecurityError:true","scrollTo:SecurityError:true","open:SecurityError:true","stop:SecurityError:true","print:SecurityError:true","find:SecurityError:true","alert:SecurityError:true","confirm:SecurityError:true","prompt:SecurityError:true","reportError:SecurityError:true","btoa:SecurityError:true","atob:SecurityError:true"],"documentAccess":"SecurityError:true"}"#.to_owned(), ), "{}", page.serialize_html_async().await.unwrap() diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index d22f4fd435..b7eda2b564 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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::{ diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime.rs index ac414863a7..63a6e192d5 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime/access.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime/access.rs new file mode 100644 index 0000000000..8dc5d0a096 --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime/access.rs @@ -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> { + 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::::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::::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::::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::::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::::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::::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::::try_from(value).ok()) + else { + return; + }; + let Some(setter) = descriptor + .get(scope, v8str(scope, "set").into()) + .and_then(|value| v8::Local::::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::::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>>); + +struct CrossOriginSurface { + context: v8::Weak, + target: v8::Weak, + surface: v8::Weak, +} + +#[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> { + let cache = scope + .get_slot::() + .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::( + 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::::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 +} diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs index b574700469..0cea411d9c 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs @@ -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; } diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime/methods.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime/methods.rs index a3db07e973..2e72ab101c 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_runtime/methods.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime/methods.rs @@ -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::(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; }; diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs index c0bc8abb73..046c7845fd 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs @@ -10,6 +10,7 @@ pub(super) fn location_ancestor_origins_slot<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, ) -> Option> { + 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> { + 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 { + let object = super::access::location_target(scope, object); let value = get_private_value(scope, object, LOCATION_RELEVANT_DOCUMENT_ID_SLOT)?; let value = v8::Local::::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 { + 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::::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 { + 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)) diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs index 0a389a797c..a031a3f8c2 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs @@ -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}"))?; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs index e284286351..eb71ed3a6a 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs @@ -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> { diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs index 1859393c27..ceedfff863 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs @@ -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::::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> { - 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> { + 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); diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index e4fd4bd328..05f9d5b424 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -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; diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs index 0bda4b9327..e9711756ac 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs @@ -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); diff --git a/moli-renderer-v8/src/native_bridge/mod.rs b/moli-renderer-v8/src/native_bridge/mod.rs index 3ca36e11ba..fd36d706b5 100644 --- a/moli-renderer-v8/src/native_bridge/mod.rs +++ b/moli-renderer-v8/src/native_bridge/mod.rs @@ -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"; diff --git a/moli-renderer-v8/src/script_vm/tests/retained_child_window.rs b/moli-renderer-v8/src/script_vm/tests/retained_child_window.rs index 29a00ebad7..4b2decabcc 100644 --- a/moli-renderer-v8/src/script_vm/tests/retained_child_window.rs +++ b/moli-renderer-v8/src/script_vm/tests/retained_child_window.rs @@ -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![ + "Location target".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![ diff --git a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs index 7239f21422..667e1197f7 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -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, diff --git a/moli-renderer-v8/tests/fixtures/retained-location-origin.js b/moli-renderer-v8/tests/fixtures/retained-location-origin.js new file mode 100644 index 0000000000..384b51a930 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/retained-location-origin.js @@ -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; +} diff --git a/moli-webapi-declare/src/brand.rs b/moli-webapi-declare/src/brand.rs index e971bf5d58..f70636358f 100644 --- a/moli-webapi-declare/src/brand.rs +++ b/moli-webapi-declare/src/brand.rs @@ -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> { + let registry = scope.get_slot::>>().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>, diff --git a/moli-webapi-declare/src/lib.rs b/moli-webapi-declare/src/lib.rs index 62d10709be..a1c8a09c31 100644 --- a/moli-webapi-declare/src/lib.rs +++ b/moli-webapi-declare/src/lib.rs @@ -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::{ diff --git a/moli-webapi-declare/tests/brand.rs b/moli-webapi-declare/tests/brand.rs index 9d8ef98c3c..fa39666c2f 100644 --- a/moli-webapi-declare/tests/brand.rs +++ b/moli-webapi-declare/tests/brand.rs @@ -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::::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)]