fix(trusted-types): align WebIDL interfaces and global accessors

This commit is contained in:
ldm0
2026-10-11 18:04:10 +08:00
parent a7660d549f
commit fab598520e
7 changed files with 425 additions and 136 deletions
@@ -23,9 +23,6 @@ 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";
@@ -51,27 +48,30 @@ struct TrustedTypePolicyFactoryInterfaceDeclaration {
#[webapi(
method,
callback = trusted_types_create_policy_callback,
length = 2,
length = 1,
enumerable
)]
create_policy: (),
#[webapi(
method = "isHTML",
callback = trusted_types_is_html_callback,
callback = trusted_types_is_type_callback,
data = crate::util::callback_data_index_value(scope, 0),
length = 1,
enumerable
)]
is_html: (),
#[webapi(
method,
callback = trusted_types_is_script_callback,
callback = trusted_types_is_type_callback,
data = crate::util::callback_data_index_value(scope, 1),
length = 1,
enumerable
)]
is_script: (),
#[webapi(
method = "isScriptURL",
callback = trusted_types_is_script_url_callback,
callback = trusted_types_is_type_callback,
data = crate::util::callback_data_index_value(scope, 2),
length = 1,
enumerable
)]
@@ -123,6 +123,13 @@ struct TrustedTypesFactoryObjectDeclaration<'scope> {
created_policy_names: v8::Local<'scope, v8::Array>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "TrustedTypePolicyFactory")]
struct TrustedTypesPredicateArgs<'scope> {
#[webidl(required)]
value: v8::Local<'scope, v8::Value>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "TrustedTypePolicyFactory.getAttributeType")]
struct TrustedTypesGetAttributeTypeArgs {
@@ -154,15 +161,15 @@ struct TrustedTypeObjectDeclaration<'scope> {
value: v8::Local<'scope, v8::String>,
}
#[derive(Default, WebApiObject)]
#[webapi(plain)]
struct TrustedTypePrototypeDeclaration {
#[webapi(method = "toString", callback = trusted_type_to_string_callback, length = 0)]
#[derive(WebApiObject)]
#[webapi(fragment, enumerable)]
struct TrustedTypePrototypeDeclaration<'scope> {
kind: v8::Local<'scope, v8::String>,
#[webapi(method = "toString", callback = trusted_type_to_string_callback, data = self.kind, length = 0)]
to_string: (),
#[webapi(method = "toJSON", callback = trusted_type_to_string_callback, length = 0)]
#[webapi(method = "toJSON", callback = trusted_type_to_string_callback, data = self.kind, length = 0)]
to_json: (),
#[webapi(method, callback = trusted_type_to_string_callback, length = 0)]
value_of: (),
}
#[derive(WebApiFunctionTemplate)]
@@ -745,16 +752,17 @@ fn build_trusted_type_constructor<'s>(
kind: TrustedTypeKind,
) -> Result<TrustedTypeConstructorBinding<'s>> {
let name = kind.constructor_name();
let constructor = v8::Function::builder(trusted_type_illegal_constructor_callback)
.length(0)
.build(scope)
let template = v8::FunctionTemplate::new(scope, trusted_type_illegal_constructor_callback);
template.read_only_prototype();
let constructor = template
.get_function(scope)
.ok_or_else(|| anyhow!("failed to create {name} constructor"))?;
constructor.set_name(v8str(scope, name));
let prototype = constructor
.get(scope, v8str(scope, "prototype").into())
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
.ok_or_else(|| anyhow!("{name}.prototype missing"))?;
TrustedTypePrototypeDeclaration::default()
TrustedTypePrototypeDeclaration::new(v8str(scope, kind.constructor_name()))
.initialize(scope, prototype)
.map_err(|error| anyhow!("failed to initialize {name}.prototype declaration: {error}"))?;
prototype
@@ -762,7 +770,7 @@ fn build_trusted_type_constructor<'s>(
scope,
v8::Symbol::get_to_string_tag(scope).into(),
v8str(scope, name).into(),
v8::PropertyAttribute::DONT_ENUM,
v8::PropertyAttribute::DONT_ENUM | v8::PropertyAttribute::READ_ONLY,
)
.unwrap_or(false)
.then_some(())
@@ -787,7 +795,7 @@ fn install_trusted_script_code_like_constructor<'s>(
.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()
TrustedTypePrototypeDeclaration::new(v8str(scope, TrustedTypeKind::Script.constructor_name()))
.initialize(scope, prototype)
.map_err(|error| {
anyhow!("failed to initialize TrustedScript code-like carrier: {error}")
@@ -865,6 +873,13 @@ fn trusted_type_to_string_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let this = args.this();
if !trusted_type_kind(scope, this.into()).is_some_and(|kind| {
v8str(scope, kind.constructor_name()).strict_equals(args.data())
})
{
throw_type_error(scope, "Illegal invocation");
return;
}
let Some(value) = get_private_value(scope, this, TRUSTED_TYPE_VALUE_SLOT) else {
throw_type_error(scope, "Illegal invocation");
return;
@@ -1108,7 +1123,7 @@ fn trusted_type_policy_create_callback<'s>(
rv.set(object.into());
}
fn trusted_types_is_html_callback<'s>(
fn trusted_types_is_type_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
@@ -1116,29 +1131,18 @@ fn trusted_types_is_html_callback<'s>(
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());
}
fn trusted_types_is_script_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()) {
let Some(parsed) = webidl::parse_args::<TrustedTypesPredicateArgs<'s>>(scope, &args) else {
return;
}
rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::Script).is_some());
}
fn trusted_types_is_script_url_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()) {
};
let Some(kind) = crate::util::callback_data_item(
scope,
&args,
&TRUSTED_TYPE_KINDS,
"TrustedTypePolicyFactory type predicates",
) else {
return;
}
rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::ScriptUrl).is_some());
};
rv.set_bool(trusted_type_kind(scope, parsed.value) == Some(kind));
}
fn trusted_types_get_attribute_type_callback<'s>(
@@ -8,49 +8,18 @@ const TRUSTED_TYPES_LAZY_STATE_INSTALLED_SLOT: &str = "__moliTrustedTypesLazySta
const TRUSTED_TYPES_FACTORY_SLOT: &str = "__moliTrustedTypesFactory";
const TRUSTED_TYPES_MATERIALIZING_SLOT: &str = "__moliTrustedTypesMaterializing";
#[derive(Clone, Copy)]
enum TrustedTypesGlobalProperty {
Html,
Script,
ScriptUrl,
Factory,
}
#[derive(WebApiObject)]
#[webapi(fragment)]
struct TrustedTypesGlobalAccessorDeclaration {
window_receiver: bool,
impl TrustedTypesGlobalProperty {
const ALL: [(Self, &'static str); 4] = [
(Self::Html, "TrustedHTML"),
(Self::Script, "TrustedScript"),
(Self::ScriptUrl, "TrustedScriptURL"),
(Self::Factory, "trustedTypes"),
];
const fn callback_data(self) -> u32 {
match self {
Self::Html => 0,
Self::Script => 1,
Self::ScriptUrl => 2,
Self::Factory => 3,
}
}
fn from_callback_data(value: u32) -> Option<Self> {
match value {
0 => Some(Self::Html),
1 => Some(Self::Script),
2 => Some(Self::ScriptUrl),
3 => Some(Self::Factory),
_ => None,
}
}
const fn trusted_type_kind(self) -> Option<TrustedTypeKind> {
match self {
Self::Html => Some(TrustedTypeKind::Html),
Self::Script => Some(TrustedTypeKind::Script),
Self::ScriptUrl => Some(TrustedTypeKind::ScriptUrl),
Self::Factory => None,
}
}
#[webapi(
accessor_property = "trustedTypes",
getter = trusted_types_global_getter,
data = self.window_receiver,
enumerable
)]
trusted_types: (),
}
pub(super) fn install_lazy_trusted_types_runtime_state<'s>(
@@ -74,12 +43,6 @@ pub(super) fn install_lazy_trusted_types_runtime_state<'s>(
.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"))?;
@@ -93,28 +56,55 @@ pub(super) fn install_lazy_trusted_types_runtime_state<'s>(
.unwrap_or(false)
.then_some(())
.ok_or_else(|| anyhow!("failed to publish TrustedTypePolicyFactory interface"))?;
set_private_value(
scope,
global,
TRUSTED_TYPE_POLICY_FACTORY_CONSTRUCTOR_SLOT,
policy_factory_constructor.into(),
);
install_trusted_script_code_like_constructor(scope, global)?;
// These eager interfaces are outside the exposed-interface template table.
// Register their intrinsic prototypes before author code can replace globals;
// WebApiObject binding must never use an author-supplied constructor.
for (name, constructor) in [
("TrustedTypePolicy", policy_constructor),
("TrustedTypePolicyFactory", policy_factory_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!("{name}.prototype missing during bootstrap"))?;
if !crate::util::register_intrinsic_interface(
scope,
global,
name,
constructor.into(),
prototype,
) {
return Err(anyhow!("failed to register intrinsic {name} interface"));
}
}
install_function_constructor_brand_guards(scope, global)?;
for (property, name) in TrustedTypesGlobalProperty::ALL {
let data = v8::Integer::new_from_unsigned(scope, property.callback_data());
for (index, kind) in TRUSTED_TYPE_KINDS.into_iter().enumerate() {
let name = kind.constructor_name();
let data = crate::util::callback_data_index_value(scope, index);
global
.set_lazy_data_property_with_configuration(
scope,
v8str(scope, name).into(),
v8::LazyDataPropertyConfiguration::new(trusted_types_global_lazy_getter)
.data(data.into())
.data(data)
.property_attribute(v8::PropertyAttribute::DONT_ENUM),
)
.unwrap_or(false)
.then_some(())
.ok_or_else(|| anyhow!("failed to install lazy `{name}` global"))?;
}
// Window owns its mixin attribute; workers inherit it from WorkerGlobalScope.
let is_worker = get_private_value(scope, global, crate::worker::WORKER_STATE_SLOT).is_some();
let target = if is_worker {
crate::util::global_constructor_prototype(scope, "WorkerGlobalScope").ok_or_else(|| {
anyhow!("WorkerGlobalScope.prototype missing during Trusted Types bootstrap")
})?
} else {
global
};
TrustedTypesGlobalAccessorDeclaration::new(!is_worker)
.initialize(scope, target)
.map_err(|error| anyhow!("failed to install trustedTypes accessor: {error}"))?;
set_private_value(
scope,
global,
@@ -130,10 +120,10 @@ fn trusted_types_global_lazy_getter<'s>(
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(property) = args
let Some(kind) = args
.data()
.uint32_value(scope)
.and_then(TrustedTypesGlobalProperty::from_callback_data)
.and_then(|index| TrustedTypeKind::from_callback_index(index as usize))
else {
throw_error(
scope,
@@ -149,14 +139,64 @@ fn trusted_types_global_lazy_getter<'s>(
return;
};
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
match ensure_trusted_types_state(target_scope)
.and_then(|()| cached_public_value(target_scope, property))
{
Ok(value) => rv.set(value),
Err(error) => throw_error(
if let Err(error) = ensure_trusted_types_state(target_scope) {
throw_error(
target_scope,
&format!("Failed to materialize Trusted Types: {error}"),
),
);
return;
}
let global = relevant_context.global(target_scope);
if let Some(value) = get_private_value(target_scope, global, kind.constructor_slot()) {
rv.set(value);
} else {
throw_error(
target_scope,
"Materialized Trusted Types constructor is missing.",
);
}
}
fn trusted_types_global_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let receiver = args.this();
// Reject in the getter's realm before entering a valid receiver's realm.
// Borrowing a getter changes neither its TypeError realm nor its brand check.
let valid_receiver = if args.data().is_true() {
super::super::window_receiver::is_window_receiver(scope, receiver)
} else {
let global = scope.get_current_context().global(scope);
receiver.strict_equals(global.into())
};
if !valid_receiver {
throw_type_error(
scope,
"trustedTypes getter called on incompatible receiver.",
);
return;
}
let relevant_context = receiver
.get_creation_context(scope)
.unwrap_or_else(|| scope.get_current_context());
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
if let Err(error) = ensure_trusted_types_state(target_scope) {
throw_error(
target_scope,
&format!("Failed to materialize Trusted Types: {error}"),
);
return;
}
let global = relevant_context.global(target_scope);
if let Some(factory) = cached_object(target_scope, global, TRUSTED_TYPES_FACTORY_SLOT) {
rv.set(factory.into());
} else {
throw_error(
target_scope,
"Materialized Trusted Types factory is missing.",
);
}
}
@@ -222,24 +262,6 @@ fn build_and_cache_trusted_types_state<'s>(
Ok(())
}
fn cached_public_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
property: TrustedTypesGlobalProperty,
) -> Result<v8::Local<'s, v8::Value>> {
let global = scope.get_current_context().global(scope);
let slot = match property {
TrustedTypesGlobalProperty::Html
| TrustedTypesGlobalProperty::Script
| TrustedTypesGlobalProperty::ScriptUrl => property
.trusted_type_kind()
.expect("trusted type constructor property must have a kind")
.constructor_slot(),
TrustedTypesGlobalProperty::Factory => TRUSTED_TYPES_FACTORY_SLOT,
};
get_private_value(scope, global, slot)
.ok_or_else(|| anyhow!("materialized Trusted Types value is missing"))
}
fn cached_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -129,24 +129,24 @@ fn trusted_type_prototype_declared_methods_preserve_descriptors() {
script: internalNames(script),
scriptURL: internalNames(scriptURL)
},
htmlMethods: ["toString", "valueOf"].map(name => describe(TrustedHTML.prototype, name)),
scriptMethods: ["toString", "valueOf"].map(name => describe(TrustedScript.prototype, name)),
scriptURLMethods: ["toString", "valueOf"].map(name => describe(TrustedScriptURL.prototype, name)),
htmlMethods: ["toString", "toJSON"].map(name => describe(TrustedHTML.prototype, name)),
scriptMethods: ["toString", "toJSON"].map(name => describe(TrustedScript.prototype, name)),
scriptURLMethods: ["toString", "toJSON"].map(name => describe(TrustedScriptURL.prototype, name)),
values: [
String(html),
html.valueOf(),
html.toJSON(),
String(script),
script.valueOf(),
script.toJSON(),
String(scriptURL),
scriptURL.valueOf()
scriptURL.toJSON()
],
fakeResults: [
probe(() => TrustedHTML.prototype.toString.call(fakeHTML)),
probe(() => TrustedHTML.prototype.valueOf.call(fakeHTML)),
probe(() => TrustedHTML.prototype.toJSON.call(fakeHTML)),
probe(() => TrustedScript.prototype.toString.call(fakeScript)),
probe(() => TrustedScript.prototype.valueOf.call(fakeScript)),
probe(() => TrustedScript.prototype.toJSON.call(fakeScript)),
probe(() => TrustedScriptURL.prototype.toString.call(fakeScriptURL)),
probe(() => TrustedScriptURL.prototype.valueOf.call(fakeScriptURL))
probe(() => TrustedScriptURL.prototype.toJSON.call(fakeScriptURL))
].join("|"),
trustedChecks: [
trustedTypes.isHTML(fakeHTML),
@@ -164,7 +164,7 @@ fn trusted_type_prototype_declared_methods_preserve_descriptors() {
assert_eq!(
result,
r#"{"prototypes":[true,true,true],"initialOwnSlots":{"html":[],"script":[],"scriptURL":[]},"spoofedOwnSlots":{"html":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"script":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"scriptURL":["__moliTrustedTypeKind","__moliTrustedTypeValue"]},"htmlMethods":["false:true:true:toString:0","false:true:true:valueOf:0"],"scriptMethods":["false:true:true:toString:0","false:true:true:valueOf:0"],"scriptURLMethods":["false:true:true:toString:0","false:true:true:valueOf:0"],"values":["<em>ok</em>","<em>ok</em>","1 + 1","1 + 1","data:text/javascript,","data:text/javascript,"],"fakeResults":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError","trustedChecks":[false,false,false,true,true,true]}"#
r#"{"prototypes":[true,true,true],"initialOwnSlots":{"html":[],"script":[],"scriptURL":[]},"spoofedOwnSlots":{"html":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"script":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"scriptURL":["__moliTrustedTypeKind","__moliTrustedTypeValue"]},"htmlMethods":["true:true:true:toString:0","true:true:true:toJSON:0"],"scriptMethods":["true:true:true:toString:0","true:true:true:toJSON:0"],"scriptURLMethods":["true:true:true:toString:0","true:true:true:toJSON:0"],"values":["<em>ok</em>","<em>ok</em>","1 + 1","1 + 1","data:text/javascript,","data:text/javascript,"],"fakeResults":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError","trustedChecks":[false,false,false,true,true,true]}"#
);
}
#[test]
@@ -40,6 +40,87 @@ fn trusted_types_csp_policy_creation_matches_only_valid_name_tokens() {
}
}
#[test]
fn trusted_types_webidl_surface_checks_descriptors_arguments_and_brands() {
let mut vm = new_storage_test_vm("https://trusted-types-webidl.test/");
assert_eq!(
vm.eval(include_str!(
"../../../../tests/fixtures/trusted-types-webidl.js"
))
.expect("Trusted Types WebIDL surface should match its declarations"),
"ok"
);
}
#[tokio::test]
async fn trusted_types_webidl_getter_and_stringifiers_preserve_cross_realm_semantics() {
let mut vm = new_parsed_test_vm(
"https://trusted-types-webidl-realms.test/",
"<!doctype html><html><body></body></html>",
);
vm.eval(
r#"
const frame = document.createElement('iframe');
frame.id = 'trusted-types-webidl-child';
frame.srcdoc = '<!doctype html><p>child</p>';
document.body.appendChild(frame);
"#,
)
.expect("create child realm for Trusted Types getter checks");
run_child_navigation_commit_and_host_load_for_test(&mut vm, "Trusted Types child realm").await;
assert_eq!(
vm.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const child = document.getElementById('trusted-types-webidl-child').contentWindow;
const parentGetter = Object.getOwnPropertyDescriptor(window, 'trustedTypes').get;
const childGetter = Object.getOwnPropertyDescriptor(child, 'trustedTypes').get;
// Borrow the parent getter before the child's factory has been accessed.
const childFactory = parentGetter.call(child);
assert(childFactory === child.trustedTypes &&
childFactory instanceof child.TrustedTypePolicyFactory &&
!(childFactory instanceof TrustedTypePolicyFactory), 'factory receiver realm');
assert(childGetter.call(window) === trustedTypes, 'borrowed child getter');
assert(childGetter.call(null) === childFactory &&
childGetter.call(undefined) === childFactory, 'nullish receiver uses getter realm');
for (const [getter, ErrorType] of [[parentGetter, TypeError], [childGetter, child.TypeError]]) {
let error;
try { getter.call({}); } catch (caught) { error = caught; }
assert(error instanceof ErrorType, 'getter exception belongs to function realm');
}
const policy = childFactory.createPolicy('child', {
createHTML: value => value,
createScript: value => value,
createScriptURL: value => value,
});
const cases = [
[TrustedHTML, child.TrustedHTML, policy.createHTML('html'), 'html', 'isHTML'],
[TrustedScript, child.TrustedScript, policy.createScript('script'), 'script', 'isScript'],
[TrustedScriptURL, child.TrustedScriptURL, policy.createScriptURL('/app.js'), '/app.js', 'isScriptURL'],
];
for (const [Parent, Child, value, text, predicate] of cases) {
assert(value instanceof Child && !(value instanceof Parent), 'value realm');
assert(trustedTypes[predicate](value), 'cross-realm predicate');
for (const method of ['toString', 'toJSON']) {
assert(Parent.prototype[method].call(value) === text, 'cross-realm stringifier');
let error;
try { Child.prototype[method].call({}); } catch (caught) { error = caught; }
assert(error instanceof child.TypeError && !(error instanceof TypeError),
'stringifier exception belongs to function realm');
}
}
return 'ok';
})()
"#,
)
.expect("Trusted Types getter and stringifier realms should be preserved"),
"ok"
);
}
#[test]
fn trusted_type_policy_callbacks_follow_webidl_dictionary_and_callback_rules() {
let mut vm = new_storage_test_vm("https://trusted-type-policy-webidl.test/");
@@ -495,6 +495,18 @@ fn trusted_types_globals_share_one_lazy_native_realm_state() {
let mut vm = new_storage_test_vm("https://lazy-trusted-types.test/");
assert!(!default_trusted_types_materialized(&mut vm));
assert_eq!(
vm.eval(
"String(typeof Object.getOwnPropertyDescriptor(globalThis, 'trustedTypes').get === 'function')"
)
.expect("reading the trustedTypes getter descriptor should not invoke it"),
"true"
);
assert!(
!default_trusted_types_materialized(&mut vm),
"reflecting the accessor must not build Trusted Types prototypes or factory"
);
assert_eq!(
vm.eval(
r#"
@@ -551,6 +563,51 @@ fn trusted_types_globals_share_one_lazy_native_realm_state() {
);
}
#[test]
fn trusted_types_getter_materializes_intrinsics_without_replacing_public_overrides() {
let mut vm = new_storage_test_vm("https://lazy-trusted-types-overrides.test/");
vm.eval(
r#"
const replacement = function Replacement() {};
globalThis.TrustedHTML = replacement;
globalThis.TrustedScript = replacement;
globalThis.TrustedScriptURL = replacement;
globalThis.TrustedTypePolicy = replacement;
globalThis.TrustedTypePolicyFactory = replacement;
"#,
)
.expect("overriding public constructor names should not materialize Trusted Types");
assert!(!default_trusted_types_materialized(&mut vm));
assert_eq!(
vm.eval(
r#"
(() => {
const factory = trustedTypes;
const policy = factory.createPolicy('private-intrinsics', {
createHTML: value => value,
createScript: value => value,
createScriptURL: value => value,
});
const values = [policy.createHTML('html'), policy.createScript('script'),
policy.createScriptURL('/app.js')];
return String(
[TrustedHTML, TrustedScript, TrustedScriptURL, TrustedTypePolicy,
TrustedTypePolicyFactory].every(value => value === replacement) &&
!(factory instanceof replacement) && !(policy instanceof replacement) &&
values.every(value => !(value instanceof replacement)) &&
factory.isHTML(values[0]) && factory.isScript(values[1]) &&
factory.isScriptURL(values[2]) &&
values.map(String).join('|') === 'html|script|/app.js'
);
})()
"#,
)
.expect("lazy Trusted Types materialization should use private intrinsic identities"),
"true"
);
assert!(default_trusted_types_materialized(&mut vm));
}
#[test]
fn media_source_uses_the_shared_lazy_interface_registry() {
let mut vm = new_storage_test_vm("https://lazy-media-source.test/");
@@ -1,5 +1,22 @@
use super::*;
#[tokio::test]
async fn worker_trusted_types_webidl_surface_checks_descriptors_arguments_and_brands() {
ensure_v8();
let mut handle = spawn_worker(
format!(
"postMessage({}); close();",
include_str!("../../../../tests/fixtures/trusted-types-webidl.js")
),
"https://trusted-types-webidl.test/worker.js".into(),
);
let message = timeout(TIMEOUT, handle.recv())
.await
.expect("worker Trusted Types probe should settle")
.expect("worker Trusted Types probe should return a result");
assert_eq!(expect_post_json(message), r#""ok""#);
}
#[tokio::test]
async fn worker_compression_streams_roundtrip_all_formats() {
ensure_v8();
+108
View File
@@ -0,0 +1,108 @@
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const rejects = (callback, message) => {
let error;
try { callback(); } catch (caught) { error = caught; }
assert(error instanceof TypeError, message);
};
const isWindow = typeof window !== "undefined";
const owner = isWindow ? globalThis : WorkerGlobalScope.prototype;
const descriptor = Object.getOwnPropertyDescriptor(owner, "trustedTypes");
assert(typeof descriptor?.get === "function", "trustedTypes is an accessor");
assert(descriptor.set === undefined && !Object.hasOwn(descriptor, "value"),
"trustedTypes has no setter or data value");
assert(descriptor.enumerable && descriptor.configurable, "trustedTypes flags");
const getter = descriptor.get;
assert(getter.name === "get trustedTypes" && getter.length === 0, "getter metadata");
assert(!Object.hasOwn(getter, "prototype"), "getter is not a constructor");
rejects(() => Reflect.construct(getter, []), "new getter");
if (!isWindow) assert(!Object.hasOwn(globalThis, "trustedTypes"), "worker placement");
const factory = trustedTypes;
assert(factory instanceof TrustedTypePolicyFactory, "factory interface");
for (const receiver of [globalThis, null, undefined]) {
assert(getter.call(receiver) === factory, "getter global and nullish receivers");
}
for (const receiver of [{}, Object.create(globalThis), new Proxy(globalThis, {}), 1]) {
rejects(() => getter.call(receiver), "getter receiver brand");
}
assert(!Reflect.set(globalThis, "trustedTypes", {}), "readonly write fails");
rejects(() => { "use strict"; globalThis.trustedTypes = {}; }, "strict readonly write");
assert(trustedTypes === factory, "readonly write preserves factory");
assert(Object.getOwnPropertyDescriptor(owner, "trustedTypes").get === getter,
"getter remains an accessor after materialization");
assert(factory.createPolicy.length === 1, "optional policy options do not count in length");
const policy = factory.createPolicy("webidl-surface", {
createHTML: value => value,
createScript: value => value,
createScriptURL: value => value,
});
const entries = [
[TrustedHTML, policy.createHTML("<b>html</b>"), "<b>html</b>", "isHTML"],
[TrustedScript, policy.createScript("1 + 1"), "1 + 1", "isScript"],
[TrustedScriptURL, policy.createScriptURL("/app.js"), "/app.js", "isScriptURL"],
];
for (const [Constructor, value, text, predicate] of entries) {
const proto = Constructor.prototype;
const prototypeDescriptor = Object.getOwnPropertyDescriptor(Constructor, "prototype");
assert(!prototypeDescriptor.writable && !prototypeDescriptor.enumerable &&
!prototypeDescriptor.configurable, `${Constructor.name}.prototype flags`);
const constructorDescriptor = Object.getOwnPropertyDescriptor(proto, "constructor");
assert(constructorDescriptor.value === Constructor && constructorDescriptor.writable &&
!constructorDescriptor.enumerable && constructorDescriptor.configurable,
`${Constructor.name} constructor backlink`);
rejects(() => Constructor(), `${Constructor.name} call`);
rejects(() => new Constructor(), `${Constructor.name} construction`);
const tag = Object.getOwnPropertyDescriptor(proto, Symbol.toStringTag);
assert(tag.value === Constructor.name && !tag.writable && !tag.enumerable && tag.configurable,
`${Constructor.name} tag flags`);
assert(!Object.hasOwn(proto, "valueOf") && value.valueOf() === value,
`${Constructor.name} inherits Object valueOf`);
const fake = Object.create(proto);
const revoked = Proxy.revocable(value, {});
revoked.revoke();
const fakeValues = [fake, Object.create(value), new Proxy(value, {}), revoked.proxy,
{}, null, undefined, text];
for (const name of ["toString", "toJSON"]) {
const method = Object.getOwnPropertyDescriptor(proto, name);
assert(method.enumerable && method.configurable && method.writable,
`${Constructor.name}.${name} flags`);
assert(method.value.name === name && method.value.length === 0 &&
!Object.hasOwn(method.value, "prototype"), `${name} metadata`);
rejects(() => Reflect.construct(method.value, []), `new ${name}`);
assert(method.value.call(value) === text, `${name} data`);
for (const other of [...fakeValues, ...entries.filter(entry => entry[0] !== Constructor)
.map(entry => entry[1])]) {
rejects(() => method.value.call(other), `${Constructor.name}.${name} receiver brand`);
}
}
rejects(() => factory[predicate](), `${predicate} missing argument`);
for (const other of fakeValues) {
assert(factory[predicate](other) === false, `${predicate} non-instance`);
}
for (const [Other, other] of entries) {
assert(factory[predicate](other) === (Other === Constructor), `${predicate} kind`);
}
const trap = new Proxy({}, { get() { throw new Error("predicate must not coerce"); } });
assert(factory[predicate](trap) === false, `${predicate} does not coerce input`);
rejects(() => factory[predicate].call({}, value), `${predicate} factory brand`);
assert(String(value) === text && JSON.stringify(value) === JSON.stringify(text),
`${Constructor.name} string and JSON conversion`);
value.toString = () => "7";
assert(Number(value) === 7, `${Constructor.name} default primitive conversion`);
delete value.toString;
}
const Policy = TrustedTypePolicy;
globalThis.TrustedTypePolicy = function ReplacementPolicy() {};
globalThis.TrustedTypePolicyFactory = function ReplacementFactory() {};
const afterOverride = factory.createPolicy("after-public-overrides", {createHTML: value => value});
assert(afterOverride instanceof Policy && String(afterOverride.createHTML("safe")) === "safe",
"policy creation uses intrinsic prototypes after public constructor replacement");
Object.defineProperty(globalThis, "trustedTypes", {value: "shadowed", configurable: true});
assert(getter.call(globalThis) === factory, "captured getter ignores public replacement");
return "ok";
})()