diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index fea04b039a..925049c124 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -3645,7 +3645,6 @@ trusted-types/block-string-assignment-to-HTMLElement-generic.html trusted-types/block-string-assignment-to-HTMLIFrameElement-srcdoc.html trusted-types/block-string-assignment-to-attribute-via-attribute-node.html trusted-types/eval-function-constructor-untrusted-arguments-and-applying-default-policy.html -trusted-types/eval-function-constructor.html trusted-types/inheriting-csp-for-local-schemes.html trusted-types/legacy-trusted-script-urls.html trusted-types/modify-attributes-in-callback.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index ec12d76d5a..cbf78950fa 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -8602,6 +8602,7 @@ trusted-types/eval-csp-tt-default-policy-mutate.html trusted-types/eval-csp-tt-default-policy.html trusted-types/eval-csp-tt-no-default-policy.html trusted-types/eval-function-constructor-untrusted-arguments-and-default-policy-throwing.html +trusted-types/eval-function-constructor.html trusted-types/eval-no-csp-no-tt-default-policy.html trusted-types/eval-no-csp-no-tt.html trusted-types/eval-with-non-trusted-script-object.html diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index d4f8ef25d0..4f3d3c1447 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -428,8 +428,7 @@ pub(crate) use self::streams::{ pub(crate) use self::trusted_types::trusted_types_lazy_state_materialized; pub(crate) use self::trusted_types::{ TrustedTypesCodeGenerationCheck, check_javascript_url_trusted_types, - install_trusted_types_eval_runtime_state, install_trusted_types_runtime_state, - trusted_html_string_or_throw, trusted_html_value_string, + install_trusted_types_runtime_state, trusted_html_string_or_throw, trusted_html_value_string, trusted_script_string_for_script_element_execution, trusted_script_string_or_type_error, trusted_script_url_string_or_throw, trusted_types_code_generation_check, trusted_types_code_generation_check_callback, diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs index 2e9ed8f64c..f39d9bffe6 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs @@ -253,13 +253,6 @@ pub(crate) fn install_trusted_types_runtime_state<'s>( realm_state::install_lazy_trusted_types_runtime_state(scope, global) } -pub(crate) fn install_trusted_types_eval_runtime_state<'s>( - scope: &mut v8::PinScope<'s, '_>, - global: v8::Local<'s, v8::Object>, -) -> Result<()> { - install_function_constructor_wrappers(scope, global) -} - pub(crate) fn trusted_script_url_string_or_throw<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, @@ -726,6 +719,15 @@ fn install_trusted_script_code_like_constructor<'s>( let constructor = template .get_function(scope) .ok_or_else(|| anyhow!("failed to create TrustedScript code-like constructor"))?; + let prototype = constructor + .get(scope, v8str(scope, "prototype").into()) + .and_then(|value| v8::Local::::try_from(value).ok()) + .ok_or_else(|| anyhow!("TrustedScript code-like constructor prototype missing"))?; + TrustedTypePrototypeDeclaration::default() + .initialize(scope, prototype) + .map_err(|error| { + anyhow!("failed to initialize TrustedScript code-like carrier: {error}") + })?; set_private_value( scope, global, @@ -744,6 +746,7 @@ fn build_trusted_type_object<'s>( let prototype = get_private_value(scope, global, kind.prototype_slot()) .and_then(|value| v8::Local::::try_from(value).ok())?; let value = v8_string(scope, &value)?; + let declaration = TrustedTypeObjectDeclaration::new(value); let object = if kind == TrustedTypeKind::Script { get_private_value(scope, global, TRUSTED_SCRIPT_CODE_LIKE_CONSTRUCTOR_SLOT) .and_then(|value| v8::Local::::try_from(value).ok())? @@ -751,7 +754,7 @@ fn build_trusted_type_object<'s>( } else { v8::Object::new(scope) }; - TrustedTypeObjectDeclaration::new(value) + declaration .initialize(scope, object) .expect("TrustedType object declaration should initialize"); kind.interface().initialize(scope, object).ok()?; @@ -759,6 +762,22 @@ fn build_trusted_type_object<'s>( Some(object) } +fn build_trusted_script_code_like_carrier<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: String, +) -> Option> { + let global = scope.get_current_context().global(scope); + let object = get_private_value(scope, global, TRUSTED_SCRIPT_CODE_LIKE_CONSTRUCTOR_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())? + .new_instance(scope, &[])?; + let value = v8_string(scope, &value)?; + TrustedTypeObjectDeclaration::new(value) + .initialize(scope, object) + .expect("TrustedScript code-like carrier declaration should initialize"); + web_api_interfaces::TrustedScript::DESCRIPTOR.initialize(scope, object).ok()?; + Some(object) +} + fn trusted_type_illegal_constructor_callback<'s>( scope: &mut v8::PinScope<'s, '_>, _args: v8::FunctionCallbackArguments<'s>, @@ -1002,15 +1021,15 @@ fn trusted_types_is_script_url_callback<'s>( rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::ScriptUrl).is_some()); } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum TrustedFunctionConstructorKind { +#[derive(Clone, Copy)] +enum FunctionConstructorKind { Function, AsyncFunction, GeneratorFunction, AsyncGeneratorFunction, } -impl TrustedFunctionConstructorKind { +impl FunctionConstructorKind { fn name(self) -> &'static str { match self { Self::Function => "Function", @@ -1028,60 +1047,87 @@ impl TrustedFunctionConstructorKind { Self::AsyncGeneratorFunction => "Object.getPrototypeOf(async function*() {})", } } - - fn source_wrapper(self, params: &str, body: &str) -> String { - match self { - Self::Function => format!("(function anonymous({params}) {{\n{body}\n}})"), - Self::AsyncFunction => format!("(async function anonymous({params}) {{\n{body}\n}})"), - Self::GeneratorFunction => { - format!("(function* anonymous({params}) {{\n{body}\n}})") - } - Self::AsyncGeneratorFunction => { - format!("(async function* anonymous({params}) {{\n{body}\n}})") - } - } - } - - fn default_policy_source(self, params: &str, body: &str) -> String { - let prefix = match self { - Self::Function => "function anonymous", - Self::AsyncFunction => "async function anonymous", - Self::GeneratorFunction => "function* anonymous", - Self::AsyncGeneratorFunction => "async function* anonymous", - }; - format!("{prefix}({params}\n) {{\n{body}\n}}") - } } -const TRUSTED_FUNCTION_CONSTRUCTOR_KINDS: [TrustedFunctionConstructorKind; 4] = [ - TrustedFunctionConstructorKind::Function, - TrustedFunctionConstructorKind::AsyncFunction, - TrustedFunctionConstructorKind::GeneratorFunction, - TrustedFunctionConstructorKind::AsyncGeneratorFunction, +const FUNCTION_CONSTRUCTOR_KINDS: [FunctionConstructorKind; 4] = [ + FunctionConstructorKind::Function, + FunctionConstructorKind::AsyncFunction, + FunctionConstructorKind::GeneratorFunction, + FunctionConstructorKind::AsyncGeneratorFunction, ]; -fn install_function_constructor_wrappers<'s>( +fn install_function_constructor_brand_guards<'s>( scope: &mut v8::PinScope<'s, '_>, global: v8::Local<'s, v8::Object>, ) -> Result<()> { - for (index, kind) in TRUSTED_FUNCTION_CONSTRUCTOR_KINDS.into_iter().enumerate() { - let data = crate::util::callback_data_index_value(scope, index); - let constructor = v8::Function::builder(trusted_types_function_constructor_callback) - .data(data) - .length(1) - .build(scope) - .ok_or_else(|| anyhow!("failed to build {} Trusted Types wrapper", kind.name()))?; - constructor.set_name(v8str(scope, kind.name())); - if let Some(prototype) = eval_object(scope, kind.prototype_expression()) { - let _ = prototype.define_own_property( + // V8's Function constructors report one combined source string to the + // code-generation callback. Preserve the original argument list here so + // an overridden TrustedScript string conversion can still be downgraded + // to the default-policy path. The traps always delegate compilation to the + // intrinsic constructors; they do not compile source themselves. + let reflect_construct = global + .get(scope, v8str(scope, "Reflect").into()) + .and_then(|value| v8::Local::::try_from(value).ok()) + .and_then(|reflect| reflect.get(scope, v8str(scope, "construct").into())) + .and_then(|value| v8::Local::::try_from(value).ok()) + .ok_or_else(|| anyhow!("Reflect.construct missing during Trusted Types bootstrap"))?; + let apply = v8::Function::builder(function_constructor_apply_trap_callback) + .length(3) + .build(scope) + .ok_or_else(|| anyhow!("failed to create Function apply brand guard"))?; + let construct = v8::Function::builder(function_constructor_construct_trap_callback) + .data(reflect_construct.into()) + .length(3) + .build(scope) + .ok_or_else(|| anyhow!("failed to create Function construct brand guard"))?; + let handler = v8::Object::new(scope); + if !handler + .define_own_property( + scope, + v8str(scope, "apply").into(), + apply.into(), + v8::PropertyAttribute::DONT_ENUM, + ) + .unwrap_or(false) + || !handler + .define_own_property( + scope, + v8str(scope, "construct").into(), + construct.into(), + v8::PropertyAttribute::DONT_ENUM, + ) + .unwrap_or(false) + { + return Err(anyhow!( + "failed to initialize Function constructor brand guard" + )); + } + + for kind in FUNCTION_CONSTRUCTOR_KINDS { + let prototype = eval_object(scope, kind.prototype_expression()) + .ok_or_else(|| anyhow!("{} prototype missing", kind.name()))?; + let target = prototype + .get(scope, v8str(scope, "constructor").into()) + .and_then(|value| v8::Local::::try_from(value).ok()) + .ok_or_else(|| anyhow!("{} constructor missing", kind.name()))?; + let proxy = v8::Proxy::new(scope, target.into(), handler) + .ok_or_else(|| anyhow!("failed to proxy {} constructor", kind.name()))?; + if !prototype + .define_own_property( scope, v8str(scope, "constructor").into(), - constructor.into(), + proxy.into(), v8::PropertyAttribute::DONT_ENUM, - ); + ) + .unwrap_or(false) + { + return Err(anyhow!( + "failed to install {} constructor brand guard", + kind.name() + )); } - if kind == TrustedFunctionConstructorKind::Function { - define_global_value(scope, global, "Function", constructor.into())?; + if matches!(kind, FunctionConstructorKind::Function) { + define_global_value(scope, global, "Function", proxy.into())?; } } Ok(()) @@ -1097,100 +1143,95 @@ fn eval_object<'s>( .and_then(|value| v8::Local::::try_from(value).ok()) } -fn trusted_types_function_constructor_callback<'s>( +fn function_constructor_apply_trap_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, + mut rv: v8::ReturnValue<'s, v8::Value>, ) { - let Some(kind) = crate::util::callback_data_item( - scope, - &args, - &TRUSTED_FUNCTION_CONSTRUCTOR_KINDS, - "Trusted Types Function constructor wrappers", - ) else { + let Ok(target) = v8::Local::::try_from(args.get(0)) else { return; }; - let Some((params, body)) = function_constructor_source_parts(scope, &args, kind) else { + let Ok(arguments) = v8::Local::::try_from(args.get(2)) else { return; }; - let source = kind.source_wrapper(¶ms, &body); - let Some(source) = v8_string(scope, &source) else { + let Some(arguments) = prepare_function_constructor_arguments(scope, arguments) else { return; }; - let Some(script) = v8::Script::compile(scope, source, None) else { - return; - }; - if let Some(function) = crate::script_execution::execute_compiled_script(scope, script) { - rv.set(function); + if let Some(value) = crate::script_execution::call_function(scope, target, args.get(1), &arguments) { + rv.set(value); } } -fn function_constructor_source_parts<'s>( +fn function_constructor_construct_trap_callback<'s>( scope: &mut v8::PinScope<'s, '_>, - args: &v8::FunctionCallbackArguments<'s>, - kind: TrustedFunctionConstructorKind, -) -> Option<(String, String)> { - let argument_count = args.length(); - if argument_count == 0 { - let empty = v8::String::empty(scope); - let body = trusted_script_string_for_function_constructor(scope, empty.into(), "", kind)?; - return Some((String::new(), body)); + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Ok(arguments) = v8::Local::::try_from(args.get(1)) else { + return; + }; + let Some(arguments) = prepare_function_constructor_arguments(scope, arguments) else { + return; + }; + let Ok(reflect_construct) = v8::Local::::try_from(args.data()) else { + return; + }; + let arguments = v8::Array::new_with_elements(scope, &arguments); + let receiver = v8::undefined(scope); + let forwarded = [args.get(0), arguments.into(), args.get(2)]; + if let Some(value) = crate::script_execution::call_function(scope, reflect_construct, receiver.into(), &forwarded) { + rv.set(value); } - let mut params = Vec::new(); - for index in 0..argument_count.saturating_sub(1) { - params.push(js_value_to_string(scope, args.get(index))?); - } - let params = params.join(","); - let body = trusted_script_string_for_function_constructor( - scope, - args.get(argument_count.saturating_sub(1)), - ¶ms, - kind, - )?; - Some((params, body)) } -fn trusted_script_string_for_function_constructor<'s>( +fn prepare_function_constructor_arguments<'s>( scope: &mut v8::PinScope<'s, '_>, - value: v8::Local<'s, v8::Value>, - params: &str, - kind: TrustedFunctionConstructorKind, -) -> Option { - if let Some(value) = trusted_type_string(scope, value, TrustedTypeKind::Script) { - return Some(value); - } - let original = js_value_to_string(scope, value)?; - if trusted_types_eval_is_allowed(scope) { + arguments: v8::Local<'s, v8::Array>, +) -> Option>> { + let original = (0..arguments.length()) + .map(|index| arguments.get_index(scope, index)) + .collect::>>()?; + if !trusted_types_for_script_is_required(scope) || trusted_types_eval_is_allowed(scope) { return Some(original); } - let violation_sample = function_constructor_violation_sample(params, &original); - let default_policy_source = kind.default_policy_source(params, &original); - if let Some(default_value) = apply_default_trusted_type_policy( - scope, - &default_policy_source, - TrustedTypeKind::Script, - "Function", - TrustedTypeErrorKind::Eval, - ) { - if default_value == default_policy_source { + + let mut trusted = Vec::with_capacity(original.len()); + for value in &original { + let Some(value) = trusted_type_string(scope, *value, TrustedTypeKind::Script) else { return Some(original); - } - dispatch_trusted_types_sink_violation_event(scope, "Function", &violation_sample); - throw_eval_error( - scope, - "Trusted Types default policy must not transform strings passed to Function.", - ); - return None; + }; + trusted.push(value); } - dispatch_trusted_types_sink_violation_event(scope, "Function", &violation_sample); - throw_trusted_type_error( - scope, - TrustedTypeErrorKind::Eval, - "Function", - TrustedTypeKind::Script, - "Function", - ); - None + let stringified = original + .iter() + .map(|value| js_value_to_string(scope, *value)) + .collect::>>()?; + if stringified != trusted { + // Plain strings force V8's combined Function source through the normal + // Trusted Types/default-policy check. Reusing the already stringified + // values also avoids invoking a page-defined conversion twice. + return stringified + .into_iter() + .map(|value| v8_string(scope, &value).map(Into::into)) + .collect(); + } + // Private carriers retain V8's code-like brand while using an inaccessible + // native string conversion, so the original page objects are converted + // exactly once before the intrinsic constructor runs. + trusted + .into_iter() + .map(|value| build_trusted_script_code_like_carrier(scope, value).map(Into::into)) + .collect() +} + +fn trusted_types_for_script_is_required(scope: &mut v8::PinScope<'_, '_>) -> bool { + if let Some(required) = crate::worker::worker_requires_trusted_types_for_script(scope) { + return required; + } + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return false; + }; + unsafe { &*host_ptr }.requires_trusted_types_for_script(scope) } fn trusted_types_eval_is_allowed(scope: &mut v8::PinScope<'_, '_>) -> bool { @@ -1203,10 +1244,6 @@ fn trusted_types_eval_is_allowed(scope: &mut v8::PinScope<'_, '_>) -> bool { unsafe { &*host_ptr }.allows_trusted_types_eval(scope) } -fn function_constructor_violation_sample(params: &str, body: &str) -> String { - format!("({params}\n) {{\n{body}\n}}") -} - fn dispatch_trusted_types_sink_violation_event( scope: &mut v8::PinScope<'_, '_>, sink: &str, 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 b0653a6763..76eb298a22 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 @@ -99,6 +99,8 @@ pub(super) fn install_lazy_trusted_types_runtime_state<'s>( TRUSTED_TYPE_POLICY_FACTORY_CONSTRUCTOR_SLOT, policy_factory_constructor.into(), ); + install_trusted_script_code_like_constructor(scope, global)?; + install_function_constructor_brand_guards(scope, global)?; for (property, name) in TrustedTypesGlobalProperty::ALL { let data = v8::Integer::new_from_unsigned(scope, property.callback_data()); global 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 d116bd5333..3210813cf0 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 @@ -970,6 +970,67 @@ fn trusted_script_eval_is_unwrapped_without_trusted_types_enforcement() { ); } +#[test] +fn trusted_script_code_like_brand_drives_function_constructors() { + let mut vm = new_storage_test_vm("https://trusted-script-function-brand.test/"); + vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]); + + let result = vm + .eval( + r#" +(() => { + const policy = trustedTypes.createPolicy("function-brand", { createScript: value => value }); + const source = ["a", "b", "c = 5", "return (a + b) * c;"]; + const constructors = [ + Function, + (async function() {}).constructor, + (function*() {}).constructor, + (async function*() {}).constructor + ]; + const mixedBlocked = constructors.map(Constructor => { + let blocked = 0; + for (let mask = 0; mask < 15; mask++) { + const args = source.map((value, index) => + mask & (2 ** index) ? policy.createScript(value) : value); + try { + new Constructor(...args); + } catch (error) { + blocked += error instanceof EvalError; + } + } + return blocked; + }); + const trusted = source.map(value => policy.createScript(value)); + const functions = constructors.map(Constructor => new Constructor(...trusted)); + let forgedBlocked = 0; + for (let index = 0; index < source.length; index++) { + const forged = trusted.slice(); + forged[index] = Object.assign(policy.createScript(source[index]), { + toString: () => ` ${source[index]} ` + }); + try { + new Function(...forged); + } catch (error) { + forgedBlocked += error instanceof EvalError; + } + } + return JSON.stringify({ + mixedBlocked, + trustedConstructorTypes: functions.map(value => typeof value), + functionValues: [functions[0](1, 2, 3), functions[0](1, 2)], + forgedBlocked + }); +})() +"#, + ) + .expect("TrustedScript function constructor brand probe should evaluate"); + + assert_eq!( + result, + r#"{"mixedBlocked":[15,15,15,15],"trustedConstructorTypes":["function","function","function","function"],"functionValues":[9,15],"forgedBlocked":4}"# + ); +} + #[test] fn trusted_types_default_policy_receives_type_and_eval_sink_arguments() { let mut vm = new_storage_test_vm("https://trusted-types-default-callback-args.test/"); 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 d6c8dbd3d4..f5db3f1b0a 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -179,6 +179,11 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[ 1, DirectCallOwner::BrowserAlgorithm, ), + allowed( + "context_bootstrap/trusted_types.rs", + 2, + DirectCallOwner::NativeForwardingOrScript, + ), allowed( "context_bootstrap/web_storage/interceptors.rs", 1, diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index e45c330b32..4e88d73e07 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -3017,7 +3017,6 @@ pub(super) fn install_worker_global_scope<'s>( scope .get_current_context() .set_allow_generation_from_strings(false); - crate::context_bootstrap::install_trusted_types_eval_runtime_state(scope, global)?; } crate::context_bootstrap::install_webassembly_runtime_state(scope, global)?; if matches!(global_kind, super::thread::WorkerGlobalKind::Service { .. }) { @@ -6245,6 +6244,16 @@ pub(crate) fn worker_allows_trusted_types_eval(scope: &mut v8::PinScope<'_, '_>) ) } +pub(crate) fn worker_requires_trusted_types_for_script( + scope: &mut v8::PinScope<'_, '_>, +) -> Option { + Some( + crate::content_security_policy::content_security_policy_requires_trusted_types_for_script( + &get_worker_state(scope)?.borrow().content_security_policies, + ), + ) +} + pub(crate) fn dispatch_worker_trusted_types_sink_violation_event( scope: &mut v8::PinScope<'_, '_>, sink: &str, diff --git a/moli-renderer-v8/src/worker/mod.rs b/moli-renderer-v8/src/worker/mod.rs index cf707a769a..1c92bc176f 100644 --- a/moli-renderer-v8/src/worker/mod.rs +++ b/moli-renderer-v8/src/worker/mod.rs @@ -44,9 +44,9 @@ pub(crate) use global_scope::{ worker_message_port_event_listener_snapshots, worker_message_port_registry, worker_message_port_wake_sender, worker_message_port_wrapper, worker_notification_permission_state, worker_opfs_directory_iterator_registry, - worker_opfs_handle_registry, worker_service_worker_control_state, worker_storage_key, - worker_storage_partition_identity, worker_termination_requested, - worker_uses_shared_worker_agent_cluster, + worker_opfs_handle_registry, worker_requires_trusted_types_for_script, + worker_service_worker_control_state, worker_storage_key, worker_storage_partition_identity, + worker_termination_requested, worker_uses_shared_worker_agent_cluster, }; pub(crate) use handle::WorkerMessage; pub(crate) use handle::{ diff --git a/moli-renderer-v8/src/worker/thread/tests/modules.rs b/moli-renderer-v8/src/worker/thread/tests/modules.rs index e0509390ac..331c817710 100644 --- a/moli-renderer-v8/src/worker/thread/tests/modules.rs +++ b/moli-renderer-v8/src/worker/thread/tests/modules.rs @@ -882,6 +882,39 @@ async fn worker_trusted_script_eval_is_unwrapped_with_trusted_types_eval_keyword assert_eq!(expect_post_json(msg), r#"{"trusted":7,"string":9}"#); } +#[tokio::test] +async fn worker_trusted_script_code_like_brand_drives_function_constructor() { + ensure_v8(); + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new( + r#" + const policy = trustedTypes.createPolicy("function-brand", { + createScript: value => value + }); + const source = ["a", "b", "return a + b;"]; + const trusted = source.map(value => policy.createScript(value)); + let mixedBlocked = false; + try { + new Function(trusted[0], "b", trusted[2]); + } catch (error) { + mixedBlocked = error instanceof EvalError; + } + const constructed = new Function(...trusted); + postMessage({ mixedBlocked, value: constructed(20, 22) }); + "# + .to_owned(), + "https://app.test/worker/main.js".to_owned(), + ) + .with_content_security_policies(vec!["require-trusted-types-for 'script'".to_owned()]), + ); + + let msg = timeout(TIMEOUT, handle.recv()) + .await + .expect("timed out") + .expect("channel closed"); + assert_eq!(expect_post_json(msg), r#"{"mixedBlocked":true,"value":42}"#); +} + #[tokio::test] async fn worker_module_static_imports_resolve_http_dependencies_against_module_url() { ensure_v8();