fix(trusted-types): honor Function constructor brands

This commit is contained in:
ldm0
2026-09-23 00:11:50 +08:00
parent ad9656a088
commit 39eecc30a0
10 changed files with 284 additions and 138 deletions
@@ -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
@@ -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
+1 -2
View File
@@ -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,
@@ -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::<v8::Object>::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::<v8::Object>::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::<v8::Function>::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<v8::Local<'s, v8::Object>> {
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::<v8::Function>::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::<v8::Object>::try_from(value).ok())
.and_then(|reflect| reflect.get(scope, v8str(scope, "construct").into()))
.and_then(|value| v8::Local::<v8::Function>::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::<v8::Function>::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::<v8::Object>::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::<v8::Function>::try_from(args.get(0)) else {
return;
};
let Some((params, body)) = function_constructor_source_parts(scope, &args, kind) else {
let Ok(arguments) = v8::Local::<v8::Array>::try_from(args.get(2)) else {
return;
};
let source = kind.source_wrapper(&params, &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::<v8::Array>::try_from(args.get(1)) else {
return;
};
let Some(arguments) = prepare_function_constructor_arguments(scope, arguments) else {
return;
};
let Ok(reflect_construct) = v8::Local::<v8::Function>::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)),
&params,
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<String> {
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<Vec<v8::Local<'s, v8::Value>>> {
let original = (0..arguments.length())
.map(|index| arguments.get_index(scope, index))
.collect::<Option<Vec<_>>>()?;
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::<Option<Vec<_>>>()?;
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,
@@ -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
@@ -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/");
@@ -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,
@@ -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<bool> {
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,
+3 -3
View File
@@ -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::{
@@ -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();