Implement "Create a Trusted Type" algorithm (#36454)

This algorithm is quite straightforward written in the specification,
but leads to some type awkwardness in Rust. Most notably, the callbacks
have different types and cannot be unified easily. They also return
different string types. Similarly, the returning objects are all unique
types and don't have a common denominator.

Therefore, rather than implementing it in 1-to-1 fashion with the
specification text, it instead uses callbacks to instruct the type
system of what to call when.

This is further complicated by the fact that the callback can exist
or not, as well as return a value or not. This requires multiple
unwrangling, combined with the fact that the algorithm should throw
or not.

All in all, the number of lines is relatively low compared to the
specification algorithm and the Rust compiler does a lot of heavy
lifting figuring out which type is what.

Part of https://github.com/servo/servo/issues/36258

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
Co-authored-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
Tim van der Lippe 2025-04-13 05:55:23 +02:00 committed by GitHub
parent 0c045fc247
commit dcc88b53aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 197 additions and 259 deletions

View file

@ -2,11 +2,20 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc;
use dom_struct::dom_struct;
use js::jsapi::JSObject;
use js::rust::HandleValue;
use crate::dom::bindings::callback::ExceptionHandling;
use crate::dom::bindings::codegen::Bindings::TrustedTypePolicyBinding::TrustedTypePolicyMethods;
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
use crate::dom::bindings::codegen::Bindings::TrustedTypePolicyFactoryBinding::{
CreateHTMLCallback, CreateScriptCallback, CreateScriptURLCallback, TrustedTypePolicyOptions,
};
use crate::dom::bindings::error::Error::Type;
use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::reflector::{DomGlobal, DomObject, Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::globalscope::GlobalScope;
@ -20,19 +29,117 @@ pub struct TrustedTypePolicy {
reflector_: Reflector,
name: String,
#[ignore_malloc_size_of = "Rc has unclear ownership"]
create_html: Option<Rc<CreateHTMLCallback>>,
#[ignore_malloc_size_of = "Rc has unclear ownership"]
create_script: Option<Rc<CreateScriptCallback>>,
#[ignore_malloc_size_of = "Rc has unclear ownership"]
create_script_url: Option<Rc<CreateScriptURLCallback>>,
}
impl TrustedTypePolicy {
fn new_inherited(name: String) -> Self {
fn new_inherited(name: String, options: &TrustedTypePolicyOptions) -> Self {
Self {
reflector_: Reflector::new(),
name,
create_html: options.createHTML.clone(),
create_script: options.createScript.clone(),
create_script_url: options.createScriptURL.clone(),
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new(name: String, global: &GlobalScope, can_gc: CanGc) -> DomRoot<Self> {
reflect_dom_object(Box::new(Self::new_inherited(name)), global, can_gc)
pub(crate) fn new(
name: String,
options: &TrustedTypePolicyOptions,
global: &GlobalScope,
can_gc: CanGc,
) -> DomRoot<Self> {
reflect_dom_object(Box::new(Self::new_inherited(name, options)), global, can_gc)
}
/// This does not take all arguments as specified. That's because the return type of the
/// trusted type function and object are not the same. 2 of the 3 string callbacks return
/// a DOMString, while the other one returns an USVString. Additionally, all three callbacks
/// have a unique type signature in WebIDL.
///
/// To circumvent these type problems, rather than implementing the full functionality here,
/// part of the algorithm is implemented on the caller side. There, we only call the callback
/// and create the object. The rest of the machinery is ensuring the right values pass through
/// to the relevant callbacks.
///
/// <https://w3c.github.io/trusted-types/dist/spec/#get-trusted-type-policy-value-algorithm>
pub(crate) fn get_trusted_type_policy_value<S, PolicyCallback>(
&self,
policy_value_callback: PolicyCallback,
throw_if_missing: bool,
) -> Fallible<Option<S>>
where
S: AsRef<str>,
PolicyCallback: FnOnce() -> Option<Fallible<Option<S>>>,
{
// Step 1: Let functionName be a function name for the given trustedTypeName, based on the following table:
// Step 2: Let function be policys options[functionName].
let function = policy_value_callback();
match function {
// Step 3: If function is null, then:
None => {
// Step 3.1: If throwIfMissing throw a TypeError.
if throw_if_missing {
Err(Type("Cannot find type".to_owned()))
} else {
// Step 3.2: Else return null.
Ok(None)
}
},
// Step 4: Let policyValue be the result of invoking function with value as a first argument,
// items of arguments as subsequent arguments, and callback **this** value set to null,
// rethrowing any exceptions.
Some(policy_value) => policy_value,
}
}
/// This does not take all arguments as specified. That's because the return type of the
/// trusted type function and object are not the same. 2 of the 3 string callbacks return
/// a DOMString, while the other one returns an USVString. Additionally, all three callbacks
/// have a unique type signature in WebIDL.
///
/// To circumvent these type problems, rather than implementing the full functionality here,
/// part of the algorithm is implemented on the caller side. There, we only call the callback
/// and create the object. The rest of the machinery is ensuring the right values pass through
/// to the relevant callbacks.
///
/// <https://w3c.github.io/trusted-types/dist/spec/#create-a-trusted-type-algorithm>
pub(crate) fn create_trusted_type<R, S, PolicyCallback, TrustedTypeCallback>(
&self,
policy_value_callback: PolicyCallback,
trusted_type_creation_callback: TrustedTypeCallback,
) -> Fallible<DomRoot<R>>
where
R: DomObject,
S: AsRef<str>,
PolicyCallback: FnOnce() -> Option<Fallible<Option<S>>>,
TrustedTypeCallback: FnOnce(String) -> DomRoot<R>,
{
// Step 1: Let policyValue be the result of executing Get Trusted Type policy value
// with the same arguments as this algorithm and additionally true as throwIfMissing.
let policy_value = self.get_trusted_type_policy_value(policy_value_callback, true);
match policy_value {
// Step 2: If the algorithm threw an error, rethrow the error and abort the following steps.
Err(error) => Err(error),
Ok(policy_value) => {
// Step 3: Let dataString be the result of stringifying policyValue.
let data_string = match policy_value {
Some(value) => value.as_ref().into(),
// Step 4: If policyValue is null or undefined, set dataString to the empty string.
None => "".to_owned(),
};
// Step 5: Return a new instance of an interface with a type name trustedTypeName,
// with its associated data value set to dataString.
Ok(trusted_type_creation_callback(data_string))
},
}
}
}
@ -44,34 +151,82 @@ impl TrustedTypePolicyMethods<crate::DomTypeHolder> for TrustedTypePolicy {
/// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicy-createhtml>
fn CreateHTML(
&self,
_: JSContext,
data: DOMString,
_: Vec<HandleValue>,
cx: JSContext,
input: DOMString,
arguments: Vec<HandleValue>,
can_gc: CanGc,
) -> DomRoot<TrustedHTML> {
// TODO(36258): handle arguments
TrustedHTML::new(data.to_string(), &self.global(), can_gc)
) -> Fallible<DomRoot<TrustedHTML>> {
self.create_trusted_type(
|| {
self.create_html.clone().map(|callback| {
rooted!(in(*cx) let this_object: *mut JSObject);
// Step 4: Let policyValue be the result of invoking function with value as a first argument,
// items of arguments as subsequent arguments, and callback **this** value set to null,
// rethrowing any exceptions.
callback.Call_(
&this_object.handle(),
input,
arguments,
ExceptionHandling::Rethrow,
can_gc,
)
})
},
|data_string| TrustedHTML::new(data_string, &self.global(), can_gc),
)
}
/// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicy-createscript>
fn CreateScript(
&self,
_: JSContext,
data: DOMString,
_: Vec<HandleValue>,
cx: JSContext,
input: DOMString,
arguments: Vec<HandleValue>,
can_gc: CanGc,
) -> DomRoot<TrustedScript> {
// TODO(36258): handle arguments
TrustedScript::new(data.to_string(), &self.global(), can_gc)
) -> Fallible<DomRoot<TrustedScript>> {
self.create_trusted_type(
|| {
self.create_script.clone().map(|callback| {
rooted!(in(*cx) let this_object: *mut JSObject);
// Step 4: Let policyValue be the result of invoking function with value as a first argument,
// items of arguments as subsequent arguments, and callback **this** value set to null,
// rethrowing any exceptions.
callback.Call_(
&this_object.handle(),
input,
arguments,
ExceptionHandling::Rethrow,
can_gc,
)
})
},
|data_string| TrustedScript::new(data_string, &self.global(), can_gc),
)
}
/// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicy-createscripturl>
fn CreateScriptURL(
&self,
_: JSContext,
data: DOMString,
_: Vec<HandleValue>,
cx: JSContext,
input: DOMString,
arguments: Vec<HandleValue>,
can_gc: CanGc,
) -> DomRoot<TrustedScriptURL> {
// TODO(36258): handle arguments
TrustedScriptURL::new(data.to_string(), &self.global(), can_gc)
) -> Fallible<DomRoot<TrustedScriptURL>> {
self.create_trusted_type(
|| {
self.create_script_url.clone().map(|callback| {
rooted!(in(*cx) let this_object: *mut JSObject);
// Step 4: Let policyValue be the result of invoking function with value as a first argument,
// items of arguments as subsequent arguments, and callback **this** value set to null,
// rethrowing any exceptions.
callback.Call_(
&this_object.handle(),
input,
arguments,
ExceptionHandling::Rethrow,
can_gc,
)
})
},
|data_string| TrustedScriptURL::new(data_string, &self.global(), can_gc),
)
}
}

View file

@ -48,7 +48,7 @@ impl TrustedTypePolicyFactory {
fn create_trusted_type_policy(
&self,
policy_name: String,
_options: &TrustedTypePolicyOptions,
options: &TrustedTypePolicyOptions,
global: &GlobalScope,
can_gc: CanGc,
) -> Fallible<DomRoot<TrustedTypePolicy>> {
@ -72,11 +72,10 @@ impl TrustedTypePolicyFactory {
// Step 4: Let policy be a new TrustedTypePolicy object.
// Step 5: Set policys name property value to policyName.
let policy = TrustedTypePolicy::new(policy_name.clone(), global, can_gc);
// Step 6: Set policys options value to «[ "createHTML" ->
// options["createHTML", "createScript" -> options["createScript",
// "createScriptURL" -> options["createScriptURL" ]».
// TODO(36258): implement step 6
let policy = TrustedTypePolicy::new(policy_name.clone(), options, global, can_gc);
// Step 7: If the policyName is default, set the factorys default policy value to policy.
if policy_name == "default" {
self.default_policy.set(Some(&policy))

View file

@ -12,7 +12,7 @@ use std::rc::Rc;
use js::jsapi::{
AddRawValueRoot, EnterRealm, Heap, IsCallable, JSObject, LeaveRealm, Realm, RemoveRawValueRoot,
};
use js::jsval::{JSVal, ObjectValue, UndefinedValue};
use js::jsval::{JSVal, NullValue, ObjectValue, UndefinedValue};
use js::rust::wrappers::{JS_GetProperty, JS_WrapObject};
use js::rust::{HandleObject, MutableHandleValue, Runtime};
@ -237,7 +237,11 @@ pub(crate) fn wrap_call_this_value<T: ThisReflector>(
mut rval: MutableHandleValue,
) -> bool {
rooted!(in(*cx) let mut obj = p.jsobject());
assert!(!obj.is_null());
if obj.is_null() {
rval.set(NullValue());
return true;
}
unsafe {
if !JS_WrapObject(*cx, obj.handle_mut()) {

View file

@ -9,7 +9,7 @@
[Exposed=(Window,Worker), Pref="dom_trusted_types_enabled"]
interface TrustedTypePolicy {
readonly attribute DOMString name;
TrustedHTML createHTML(DOMString input, any... arguments);
TrustedScript createScript(DOMString input, any... arguments);
TrustedScriptURL createScriptURL(DOMString input, any... arguments);
[Throws] TrustedHTML createHTML(DOMString input, any... arguments);
[Throws] TrustedScript createScript(DOMString input, any... arguments);
[Throws] TrustedScriptURL createScriptURL(DOMString input, any... arguments);
};

View file

@ -815185,7 +815185,7 @@
]
],
"TrustedTypePolicy-createXXX.html": [
"4cd91aa2a2b26877e0c5cacdcaf5719f267a3cca",
"f51f51d98455ebccdee31a5b0d844a926b27fc0e",
[
null,
{}

View file

@ -1,3 +0,0 @@
[DOMParser-parseFromString.html]
[document.innerText assigned via policy (successful HTML transformation).]
expected: FAIL

View file

@ -1,16 +1,10 @@
[DedicatedWorker-importScripts.html]
[importScripts with TrustedScriptURL works in dedicated worker]
expected: FAIL
[importScripts with untrusted URLs throws in dedicated worker]
expected: FAIL
[null is not a trusted script URL throws in dedicated worker]
expected: FAIL
[importScripts with two URLs, both trusted, in dedicated worker]
expected: FAIL
[importScripts with two URLs, both strings, in dedicated worker]
expected: FAIL

View file

@ -1,6 +0,0 @@
[Document-write.html]
[document.write with html assigned via policy (successful transformation).]
expected: FAIL
[document.writeln with html assigned via policy (successful transformation).]
expected: FAIL

View file

@ -1,3 +0,0 @@
[Element-insertAdjacentHTML.html]
[insertAdjacentHTML with html assigned via policy (successful HTML transformation).]
expected: FAIL

View file

@ -1,3 +0,0 @@
[Element-outerHTML.html]
[outerHTML with html assigned via policy (successful HTML transformation).]
expected: FAIL

View file

@ -1,9 +0,0 @@
[Element-setAttribute.html]
[script.src assigned via policy (successful ScriptURL transformation)]
expected: FAIL
[iframe.srcdoc assigned via policy (successful HTML transformation)]
expected: FAIL
[script.src assigned via policy (successful script transformation)]
expected: FAIL

View file

@ -1,12 +0,0 @@
[Element-setAttributeNS.html]
[Element.setAttributeNS assigned via policy (successful HTML transformation)]
expected: FAIL
[Element.setAttributeNS assigned via policy (successful Script transformation)]
expected: FAIL
[Element.setAttributeNS assigned via policy (successful ScriptURL transformation)]
expected: FAIL
[Element.setAttributeNS accepts a URL on <svg:image xlink:href/>]
expected: FAIL

View file

@ -1,3 +0,0 @@
[Range-createContextualFragment.html]
[range.createContextualFragment assigned via policy (successful HTML transformation).]
expected: FAIL

View file

@ -1,60 +0,0 @@
[TrustedTypePolicy-createXXX.html]
[calling undefined callbacks throws]
expected: FAIL
[trustedTypes.createPolicy(.., null) creates empty policy.]
expected: FAIL
[TestPolicyTrustedHTML1 (TrustedHTML: s => null)]
expected: FAIL
[TestPolicyTrustedHTML2 (TrustedHTML: s => "well, " + s)]
expected: FAIL
[TestPolicyTrustedHTML3 (TrustedHTML: s => { throw new Error() })]
expected: FAIL
[TestPolicyTrustedHTML5 (TrustedHTML: s => aGlobalVarForSideEffectTesting + s)]
expected: FAIL
[TestPolicyTrustedHTML6 (TrustedHTML: function() {\n [native code\]\n})]
expected: FAIL
[TestPolicyTrustedHTML7 (TrustedHTML: s => aGlobalFunction(s))]
expected: FAIL
[TestPolicyTrustedScript1 (TrustedScript: s => null)]
expected: FAIL
[TestPolicyTrustedScript2 (TrustedScript: s => "well, " + s)]
expected: FAIL
[TestPolicyTrustedScript3 (TrustedScript: s => { throw new Error() })]
expected: FAIL
[TestPolicyTrustedScript5 (TrustedScript: s => aGlobalVarForSideEffectTesting + s)]
expected: FAIL
[TestPolicyTrustedScript6 (TrustedScript: function() {\n [native code\]\n})]
expected: FAIL
[TestPolicyTrustedScript7 (TrustedScript: s => aGlobalFunction(s))]
expected: FAIL
[TestPolicyTrustedScriptURL1 (TrustedScriptURL: s => null)]
expected: FAIL
[TestPolicyTrustedScriptURL2 (TrustedScriptURL: s => s + "#duck")]
expected: FAIL
[TestPolicyTrustedScriptURL3 (TrustedScriptURL: s => { throw new Error() })]
expected: FAIL
[TestPolicyTrustedScriptURL4 (TrustedScriptURL: s => s + "#" + aGlobalVarForSideEffectTesting)]
expected: FAIL
[TestPolicyTrustedScriptURL5 (TrustedScriptURL: function() {\n [native code\]\n})]
expected: FAIL
[TestPolicyTrustedScriptURL6 (TrustedScriptURL: s => anotherGlobalFunction(s))]
expected: FAIL

View file

@ -1,75 +0,0 @@
[TrustedTypePolicyFactory-createPolicy-createXYZTests.html]
[html = null]
expected: FAIL
[html = string + global string]
expected: FAIL
[html = identity function, global string changed]
expected: FAIL
[html = callback that throws]
expected: FAIL
[html = this bound to an object]
expected: FAIL
[html = this without bind]
expected: FAIL
[html - calling undefined callback throws]
expected: FAIL
[createHTML defined - calling undefined callbacks throws]
expected: FAIL
[script = null]
expected: FAIL
[script = string + global string]
expected: FAIL
[script = identity function, global string changed]
expected: FAIL
[script = callback that throws]
expected: FAIL
[script = this bound to an object]
expected: FAIL
[script = this without bind]
expected: FAIL
[script - calling undefined callback throws]
expected: FAIL
[createScript defined - calling undefined callbacks throws]
expected: FAIL
[script_url = null]
expected: FAIL
[script_url = string + global string]
expected: FAIL
[script_url = identity function, global string changed]
expected: FAIL
[script_url = callback that throws]
expected: FAIL
[script_url = this bound to an object]
expected: FAIL
[script_url = this without bind]
expected: FAIL
[script_url - calling undefined callback throws]
expected: FAIL
[createScriptURL defined - calling undefined callbacks throws]
expected: FAIL
[Arbitrary number of arguments]
expected: FAIL

View file

@ -1,7 +1,4 @@
[block-string-assignment-to-DOMParser-parseFromString.html]
[document.innerText assigned via policy (successful HTML transformation).]
expected: FAIL
[`document.innerText = string` throws.]
expected: FAIL

View file

@ -1,10 +1,4 @@
[block-string-assignment-to-Document-write.html]
[document.write with html assigned via policy (successful URL transformation).]
expected: FAIL
[document.writeln with html assigned via policy (successful URL transformation).]
expected: FAIL
[`document.write(string)` throws]
expected: FAIL

View file

@ -1,10 +1,4 @@
[block-string-assignment-to-Element-insertAdjacentHTML.html]
[insertAdjacentHTML with html assigned via policy (successful HTML transformation).]
expected: FAIL
[insertAdjacentHTML(TrustedHTML) throws SyntaxError DOMException when position invalid.]
expected: FAIL
[`insertAdjacentHTML(string)` throws.]
expected: FAIL

View file

@ -1,7 +1,4 @@
[block-string-assignment-to-Element-outerHTML.html]
[outerHTML with html assigned via policy (successful HTML transformation).]
expected: FAIL
[`outerHTML = string` throws.]
expected: FAIL

View file

@ -29,8 +29,5 @@
[div.onclick accepts string and null after default policy was created.]
expected: FAIL
[a.rel accepts a Trusted Type]
expected: FAIL
[`script.src = setAttributeNode(embed.src)` with string works.]
expected: FAIL

View file

@ -1,12 +1,3 @@
[block-string-assignment-to-Element-setAttributeNS.html]
[Element.setAttributeNS assigned via policy (successful HTML transformation)]
expected: FAIL
[Element.setAttributeNS assigned via policy (successful Script transformation)]
expected: FAIL
[Element.setAttributeNS assigned via policy (successful ScriptURL transformation)]
expected: FAIL
[Blocking non-TrustedScriptURL assignment to <svg:script xlink:href=...> works]
expected: FAIL

View file

@ -1,7 +1,4 @@
[block-string-assignment-to-Element-setHTMLUnsafe.html]
[element.setHTMLUnsafe(html) assigned via policy (successful HTML transformation).]
expected: FAIL
[`element.setHTMLUnsafe(string)` throws.]
expected: FAIL

View file

@ -1,7 +1,4 @@
[block-string-assignment-to-HTMLIFrameElement-srcdoc.html]
[iframe.srcdoc assigned via policy (successful HTML transformation).]
expected: FAIL
[`iframe.srcdoc = string` throws.]
expected: FAIL

View file

@ -1,7 +1,4 @@
[block-string-assignment-to-Range-createContextualFragment.html]
[range.createContextualFragment assigned via policy (successful HTML transformation).]
expected: FAIL
[`range.createContextualFragment(string)` throws.]
expected: FAIL

View file

@ -1,7 +1,4 @@
[block-string-assignment-to-ShadowRoot-innerHTML.html]
[shadowRoot.innerHTML = html assigned via policy (successful HTML transformation).]
expected: FAIL
[`shadowRoot.innerHTML = string` throws.]
expected: FAIL

View file

@ -13,6 +13,3 @@
[indirect eval with TrustedScript and permissive CSP works.]
expected: FAIL
[new Function with TrustedScript and permissive CSP works.]
expected: FAIL

View file

@ -1,3 +0,0 @@
[trusted-types-duplicate-names.html]
[policy - duplicate names]
expected: FAIL

View file

@ -50,11 +50,18 @@
function anotherGlobalFunction(s) { return s + "#" + this.foo; }
var foo = "a global var named foo";
class WrappingClass {
callback_to_capture_this(s) {
return String(this);
}
}
const stringTestCases = [
[ s => s, "whatever" ],
[ s => null, "" ],
[ s => "well, " + s, "well, whatever" ],
[ s => { throw new Error() }, Error ],
[ new WrappingClass().callback_to_capture_this, "null"],
[ s => { aGlobalVarForSideEffectTesting = s; return s }, "whatever" ],
[ s => aGlobalVarForSideEffectTesting + s, "whateverwhatever" ],
[ aGlobalFunction.bind(aGlobalObject), "well, whatever" ],
@ -66,6 +73,7 @@
[ s => null, "" ],
[ s => s + "#duck", INPUTS.SCRIPTURL + "#duck" ],
[ s => { throw new Error() }, Error ],
[ new WrappingClass().callback_to_capture_this, "null"],
[ s => s + "#" + aGlobalVarForSideEffectTesting,
INPUTS.SCRIPTURL + "#global" ],
[ anotherGlobalFunction.bind(aGlobalObject), INPUTS.SCRIPTURL + "#well," ],