fix(trusted-types): expose policy interface and default

This commit is contained in:
ldm0
2026-09-23 00:11:50 +08:00
parent 8b21c49eca
commit dea63cd916
4 changed files with 218 additions and 38 deletions
@@ -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<v8::Local<'scope, v8::Object>>,
#[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<v8::Local<'scope, v8::Object>>,
#[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<v8::Local<'scope, v8::Object>>,
#[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<v8::Local<'scope, v8::Object>>,
#[webapi(slot = TRUSTED_TYPES_CREATE_SCRIPT_SLOT)]
create_script_callback: Option<v8::Local<'scope, v8::Object>>,
#[webapi(slot = TRUSTED_TYPES_CREATE_SCRIPT_URL_SLOT)]
create_script_url_callback: Option<v8::Local<'scope, v8::Object>>,
}
#[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::<webidl::DomString>(
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::<webidl::DomString>(
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());
}
@@ -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"))?;
@@ -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":["<b>ok</b>","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":["<b>ok</b>","1 + 1","data:text/javascript,"]}"#
);
}
@@ -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/");