diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs index a341cb9147..3cec491045 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs @@ -23,12 +23,14 @@ const TRUSTED_TYPE_SCRIPT_URL_PROTOTYPE_SLOT: &str = "__moliTrustedScriptURLProt const TRUSTED_TYPE_HTML_CONSTRUCTOR_SLOT: &str = "__moliTrustedHTMLConstructor"; const TRUSTED_TYPE_SCRIPT_CONSTRUCTOR_SLOT: &str = "__moliTrustedScriptConstructor"; const TRUSTED_TYPE_SCRIPT_URL_CONSTRUCTOR_SLOT: &str = "__moliTrustedScriptURLConstructor"; +const TRUSTED_TYPE_POLICY_CONSTRUCTOR_SLOT: &str = "__moliTrustedTypePolicyConstructor"; const TRUSTED_TYPE_POLICY_FACTORY_CONSTRUCTOR_SLOT: &str = "__moliTrustedTypePolicyFactoryConstructor"; const TRUSTED_TYPES_DEFAULT_POLICY_SLOT: &str = "__moliTrustedTypesDefaultPolicy"; const TRUSTED_TYPES_CREATE_HTML_SLOT: &str = "__moliTrustedTypesCreateHTML"; const TRUSTED_TYPES_CREATE_SCRIPT_SLOT: &str = "__moliTrustedTypesCreateScript"; const TRUSTED_TYPES_CREATE_SCRIPT_URL_SLOT: &str = "__moliTrustedTypesCreateScriptURL"; +const TRUSTED_TYPES_POLICY_NAME_SLOT: &str = "__moliTrustedTypesPolicyName"; const TRUSTED_TYPES_EMPTY_HTML_SLOT: &str = "__moliTrustedTypesEmptyHTML"; const TRUSTED_TYPES_EMPTY_SCRIPT_SLOT: &str = "__moliTrustedTypesEmptyScript"; const TRUSTED_TYPES_EMPTY_VALUE_SLOTS: [&str; 2] = [ @@ -87,6 +89,12 @@ struct TrustedTypePolicyFactoryInterfaceDeclaration { enumerable )] empty_script: (), + #[webapi( + accessor_property = "defaultPolicy", + getter = trusted_types_default_policy_getter_callback, + enumerable + )] + default_policy: (), } #[derive(WebApiObject)] @@ -116,40 +124,60 @@ struct TrustedTypePrototypeDeclaration { value_of: (), } -#[derive(WebApiObject)] -#[webapi(prototype = "Object", interface = web_api_interfaces::TrustedTypePolicy)] -struct TrustedTypePolicyDeclaration<'scope> { - #[webapi(data_property)] - name: v8::Local<'scope, v8::String>, - #[webapi(slot = TRUSTED_TYPES_CREATE_HTML_SLOT)] - create_html_callback: Option>, +#[derive(WebApiFunctionTemplate)] +#[webapi( + interface = web_api_interfaces::TrustedTypePolicy, + constructor = "illegal", + constructor_length = 0, + readonly_prototype, + prototype_to_string_tag = "TrustedTypePolicy" +)] +struct TrustedTypePolicyInterfaceDeclaration { + #[webapi( + accessor_property, + getter = trusted_type_policy_name_getter_callback, + enumerable + )] + name: (), #[webapi( method = "createHTML", callback = trusted_type_policy_create_callback, data = crate::util::callback_data_index_value(scope, 0), - length = 1 + length = 1, + enumerable )] create_html: (), - #[webapi(slot = TRUSTED_TYPES_CREATE_SCRIPT_SLOT)] - create_script_callback: Option>, #[webapi( method, callback = trusted_type_policy_create_callback, data = crate::util::callback_data_index_value(scope, 1), - length = 1 + length = 1, + enumerable )] create_script: (), - #[webapi(slot = TRUSTED_TYPES_CREATE_SCRIPT_URL_SLOT)] - create_script_url_callback: Option>, #[webapi( method = "createScriptURL", callback = trusted_type_policy_create_callback, data = crate::util::callback_data_index_value(scope, 2), - length = 1 + length = 1, + enumerable )] create_script_url: (), } +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::TrustedTypePolicy, require_prototype)] +struct TrustedTypePolicyObjectDeclaration<'scope> { + #[webapi(slot = TRUSTED_TYPES_POLICY_NAME_SLOT)] + name: v8::Local<'scope, v8::String>, + #[webapi(slot = TRUSTED_TYPES_CREATE_HTML_SLOT)] + create_html_callback: Option>, + #[webapi(slot = TRUSTED_TYPES_CREATE_SCRIPT_SLOT)] + create_script_callback: Option>, + #[webapi(slot = TRUSTED_TYPES_CREATE_SCRIPT_URL_SLOT)] + create_script_url_callback: Option>, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum TrustedTypeKind { Html, @@ -762,6 +790,9 @@ fn trusted_types_empty_value_getter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + return; + } let Some(slot) = crate::util::callback_data_item( scope, &args, @@ -771,17 +802,45 @@ fn trusted_types_empty_value_getter_callback<'s>( return; }; let Some(value) = get_private_value(scope, args.this(), slot) else { - throw_type_error(scope, "Illegal invocation"); return; }; rv.set(value); } +fn trusted_types_default_policy_getter_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + return; + } + let global = scope.get_current_context().global(scope); + rv.set( + get_private_value(scope, global, TRUSTED_TYPES_DEFAULT_POLICY_SLOT) + .unwrap_or_else(|| v8::null(scope).into()), + ); +} + +fn trusted_types_factory_receiver_is_valid<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, +) -> bool { + if web_api_interfaces::TrustedTypePolicyFactory::is_instance(scope, receiver.into()) { + return true; + } + throw_type_error(scope, "Illegal invocation"); + false +} + fn trusted_types_create_policy_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + return; + } let Some(name) = webidl::required_argument::( scope, &args, @@ -805,12 +864,12 @@ fn trusted_types_create_policy_callback<'s>( } let policy_name = v8_string(scope, &name).unwrap_or_else(|| v8::String::empty(scope)); - let policy = TrustedTypePolicyDeclaration::new( - policy_name, - callbacks.create_html, - callbacks.create_script, - callbacks.create_script_url, - ) + let policy = TrustedTypePolicyObjectDeclaration { + name: policy_name, + create_html_callback: callbacks.create_html, + create_script_callback: callbacks.create_script, + create_script_url_callback: callbacks.create_script_url, + } .bind(scope) .expect("TrustedTypePolicy declaration should bind"); @@ -836,6 +895,18 @@ fn trusted_types_policy_name_allowed(scope: &mut v8::PinScope<'_, '_>, name: &st unsafe { &*host_ptr }.allows_trusted_type_policy_name(scope, name) } +fn trusted_type_policy_name_getter_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(name) = get_private_value(scope, args.this(), TRUSTED_TYPES_POLICY_NAME_SLOT) else { + throw_type_error(scope, "Illegal invocation"); + return; + }; + rv.set(name); +} + fn trusted_type_policy_create_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -855,6 +926,10 @@ fn trusted_type_policy_create_callback<'s>( return; }; let policy = args.this(); + if get_private_value(scope, policy, TRUSTED_TYPES_POLICY_NAME_SLOT).is_none() { + throw_type_error(scope, "Illegal invocation"); + return; + } let Some(input) = webidl::required_argument::( scope, &args, @@ -897,6 +972,9 @@ fn trusted_types_is_html_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + return; + } rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::Html).is_some()); } @@ -905,6 +983,9 @@ fn trusted_types_is_script_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + return; + } rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::Script).is_some()); } @@ -913,6 +994,9 @@ fn trusted_types_is_script_url_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + return; + } rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::ScriptUrl).is_some()); } diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs index 74f7027fdf..b0653a6763 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs @@ -61,6 +61,25 @@ pub(super) fn install_lazy_trusted_types_runtime_state<'s>( return Ok(()); } install_trusted_script_code_like_constructor(scope, global)?; + let policy_constructor = TrustedTypePolicyInterfaceDeclaration::build(scope) + .get_function(scope) + .ok_or_else(|| anyhow!("failed to build TrustedTypePolicy interface"))?; + global + .define_own_property( + scope, + v8str(scope, "TrustedTypePolicy").into(), + policy_constructor.into(), + v8::PropertyAttribute::DONT_ENUM, + ) + .unwrap_or(false) + .then_some(()) + .ok_or_else(|| anyhow!("failed to publish TrustedTypePolicy interface"))?; + set_private_value( + scope, + global, + TRUSTED_TYPE_POLICY_CONSTRUCTOR_SLOT, + policy_constructor.into(), + ); let policy_factory_constructor = TrustedTypePolicyFactoryInterfaceDeclaration::build(scope) .get_function(scope) .ok_or_else(|| anyhow!("failed to build TrustedTypePolicyFactory interface"))?; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 84d68a8f20..204338f808 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -572,7 +572,7 @@ fn wpt_webdriver_delete_all_cookies_is_declared_on_prototype() { } #[test] -fn trusted_type_policy_declared_methods_preserve_surface() { +fn trusted_type_policy_interface_shares_methods_and_rejects_missing_callbacks() { let mut vm = new_storage_test_vm("https://trusted-type-policy-declared-methods.test/"); let result = vm @@ -587,11 +587,9 @@ fn trusted_type_policy_declared_methods_preserve_surface() { createScript: value => value, createScriptURL: value => value }); - const describe = (object, name) => { - const descriptor = Object.getOwnPropertyDescriptor(object, name); - if (!descriptor) { - return `${name}:missing`; - } + const prototype = TrustedTypePolicy.prototype; + const describe = name => { + const descriptor = Object.getOwnPropertyDescriptor(prototype, name); return [ name, descriptor.enumerable, @@ -605,36 +603,55 @@ fn trusted_type_policy_declared_methods_preserve_surface() { const html = htmlOnly.createHTML("ok"); const script = full.createScript("1 + 1"); const scriptURL = full.createScriptURL("data:text/javascript,"); - const missingCallbackErrors = ["createScript", "createScriptURL"].map(name => { + const errorName = callback => { try { - htmlOnly[name]("missing"); + callback(); return "none"; } catch (error) { - return error && error.name; + return error.constructor.name; } - }); + }; + const nameDescriptor = Object.getOwnPropertyDescriptor(prototype, "name"); return JSON.stringify({ htmlOnlyKeys: Object.keys(htmlOnly).join(","), fullKeys: Object.keys(full).join(","), - htmlOnlyMethods: ["createHTML", "createScript", "createScriptURL"].map(name => describe(htmlOnly, name)), - fullMethods: ["createHTML", "createScript", "createScriptURL"].map(name => describe(full, name)), + ownMethods: ["createHTML", "createScript", "createScriptURL"].map(name => Object.hasOwn(htmlOnly, name)), + prototypeMethods: ["createHTML", "createScript", "createScriptURL"].map(describe), + name: [ + htmlOnly.name, + full.name, + typeof nameDescriptor.get, + nameDescriptor.set === undefined, + nameDescriptor.enumerable, + nameDescriptor.configurable, + Object.hasOwn(htmlOnly, "name") + ], + brands: [ + htmlOnly instanceof TrustedTypePolicy, + full instanceof TrustedTypePolicy, + Object.getPrototypeOf(htmlOnly) === prototype, + htmlOnly.createHTML === full.createHTML + ], + missingCallbackErrors: [ + errorName(() => htmlOnly.createScript("1 + 1")), + errorName(() => htmlOnly.createScriptURL("data:text/javascript,")) + ], trusted: [ trustedTypes.isHTML(html), trustedTypes.isScript(script), trustedTypes.isScriptURL(scriptURL) ], - values: [String(html), String(script), String(scriptURL)], - missingCallbackErrors + values: [String(html), String(script), String(scriptURL)] }); })() "#, ) - .expect("TrustedTypePolicy optional method descriptors should evaluate"); + .expect("TrustedTypePolicy interface descriptors should evaluate"); assert_eq!( result, - r#"{"htmlOnlyKeys":"","fullKeys":"","htmlOnlyMethods":["createHTML:false:true:true:function:createHTML:1","createScript:false:true:true:function:createScript:1","createScriptURL:false:true:true:function:createScriptURL:1"],"fullMethods":["createHTML:false:true:true:function:createHTML:1","createScript:false:true:true:function:createScript:1","createScriptURL:false:true:true:function:createScriptURL:1"],"trusted":[true,true,true],"values":["ok","1 + 1","data:text/javascript,"],"missingCallbackErrors":["TypeError","TypeError"]}"# + r#"{"htmlOnlyKeys":"","fullKeys":"","ownMethods":[false,false,false],"prototypeMethods":["createHTML:true:true:true:function:createHTML:1","createScript:true:true:true:function:createScript:1","createScriptURL:true:true:true:function:createScriptURL:1"],"name":["html-only","full","function",true,true,true,false],"brands":[true,true,true,true],"missingCallbackErrors":["TypeError","TypeError"],"trusted":[true,true,true],"values":["ok","1 + 1","data:text/javascript,"]}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs index 2ba4e5a1d6..b03be16d29 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs @@ -98,6 +98,7 @@ fn trusted_type_policy_callbacks_follow_webidl_dictionary_and_callback_rules() { return JSON.stringify({ emptyMethods: [methodTypes(emptyFromNull), methodTypes(emptyFromOmission)], + policyBrand: nullPolicy instanceof TrustedTypePolicy, nullResults, observed, variadicResults, @@ -107,6 +108,7 @@ fn trusted_type_policy_callbacks_follow_webidl_dictionary_and_callback_rules() { errorName(() => trustedTypes.createPolicy("throwing-getter", { get createHTML() { throw new RangeError("getter"); } })), + errorName(() => emptyFromNull.createHTML("missing callback")), errorName(() => nullPolicy.createHTML()), errorName(() => emptyFromOmission.createHTML("html")), errorName(() => emptyFromOmission.createScript("script")), @@ -120,7 +122,7 @@ fn trusted_type_policy_callbacks_follow_webidl_dictionary_and_callback_rules() { assert_eq!( result, - r#"{"emptyMethods":[["function","function","function"],["function","function","function"]],"nullResults":["","",""],"observed":[true,"converted",2,true,42],"variadicResults":["abc","abundefined","a123null"],"errors":["TypeError","TypeError","RangeError","TypeError","TypeError","TypeError","TypeError"]}"# + r#"{"emptyMethods":[["function","function","function"],["function","function","function"]],"policyBrand":true,"nullResults":["","",""],"observed":[true,"converted",2,true,42],"variadicResults":["abc","abundefined","a123null"],"errors":["TypeError","TypeError","RangeError","TypeError","TypeError","TypeError","TypeError","TypeError"]}"# ); } @@ -196,6 +198,64 @@ fn trusted_type_factory_interface_exposes_stable_branded_empty_values() { ); } +#[test] +fn trusted_type_factory_default_policy_getter_tracks_the_branded_policy() { + let mut vm = new_storage_test_vm("https://trusted-type-factory-default-policy.test/"); + + let result = vm + .eval( + r#" +(() => { + const errorName = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error.constructor.name; + } + }; + const initial = trustedTypes.defaultPolicy; + const policy = trustedTypes.createPolicy("default", { createHTML: value => value }); + const other = trustedTypes.createPolicy("other", { createHTML: value => value }); + const descriptor = Object.getOwnPropertyDescriptor( + TrustedTypePolicyFactory.prototype, + "defaultPolicy" + ); + const errors = [ + errorName(() => { + "use strict"; + trustedTypes.defaultPolicy = other; + }), + errorName(() => descriptor.get.call({})), + errorName(() => new TrustedTypePolicy()) + ]; + return JSON.stringify({ + initialIsNull: initial === null, + policy: [ + policy instanceof TrustedTypePolicy, + policy.name, + trustedTypes.defaultPolicy === policy, + trustedTypes.defaultPolicy !== other + ], + descriptor: [ + typeof descriptor.get, + descriptor.set === undefined, + descriptor.enumerable, + descriptor.configurable + ], + errors + }); +})() +"#, + ) + .expect("TrustedTypePolicyFactory default policy probe should evaluate"); + + assert_eq!( + result, + r#"{"initialIsNull":true,"policy":[true,"default",true,true],"descriptor":["function",true,true,true],"errors":["TypeError","TypeError","TypeError"]}"# + ); +} + #[test] fn element_markup_sinks_enforce_trusted_html_and_standard_sink_names() { let mut vm = new_storage_test_vm("https://element-markup-trusted-types.test/");