Skip to main content

script/dom/trustedtypes/
trustedtypepolicyfactory.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4use std::cell::RefCell;
5
6use dom_struct::dom_struct;
7use html5ever::{LocalName, Namespace, QualName, local_name, ns};
8use js::jsval::NullValue;
9use js::rust::HandleValue;
10use script_bindings::conversions::SafeToJSValConvertible;
11use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
12
13use crate::conversions::Convert;
14use crate::dom::bindings::codegen::Bindings::TrustedTypePolicyFactoryBinding::{
15    TrustedTypePolicyFactoryMethods, TrustedTypePolicyOptions,
16};
17use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
18use crate::dom::bindings::codegen::Bindings::WorkerGlobalScopeBinding::WorkerGlobalScopeMethods;
19use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrTrustedScriptOrTrustedScriptURLOrString as TrustedTypeOrString;
20use crate::dom::bindings::conversions::root_from_handlevalue;
21use crate::dom::bindings::error::{Error, Fallible};
22use crate::dom::bindings::inheritance::Castable;
23use crate::dom::bindings::reflector::DomGlobal;
24use crate::dom::bindings::root::{DomRoot, MutNullableDom};
25use crate::dom::bindings::str::DOMString;
26use crate::dom::csp::CspReporting;
27use crate::dom::eventtarget::EventTarget;
28use crate::dom::globalscope::GlobalScope;
29use crate::dom::trustedtypes::trustedhtml::TrustedHTML;
30use crate::dom::trustedtypes::trustedscript::TrustedScript;
31use crate::dom::trustedtypes::trustedscripturl::TrustedScriptURL;
32use crate::dom::trustedtypes::trustedtypepolicy::{TrustedType, TrustedTypePolicy};
33use crate::dom::types::WorkerGlobalScope;
34use crate::dom::window::Window;
35use crate::script_runtime::{CanGc, JSContext};
36
37#[dom_struct]
38pub struct TrustedTypePolicyFactory {
39    reflector_: Reflector,
40
41    default_policy: MutNullableDom<TrustedTypePolicy>,
42    policy_names: RefCell<Vec<String>>,
43}
44
45pub(crate) static DEFAULT_SCRIPT_SINK_GROUP: &str = "'script'";
46
47// We currently always clone the result, so keep the `clone()` in the trait
48// for now to keep the caller side clean
49impl Convert<DOMString> for TrustedTypeOrString {
50    fn convert(self) -> DOMString {
51        match self {
52            TrustedTypeOrString::TrustedHTML(trusted_html) => trusted_html.data().clone(),
53            TrustedTypeOrString::TrustedScript(trusted_script) => trusted_script.data().clone(),
54            TrustedTypeOrString::TrustedScriptURL(trusted_script_url) => {
55                trusted_script_url.data().clone()
56            },
57            TrustedTypeOrString::String(str_) => str_,
58        }
59    }
60}
61
62impl TrustedTypePolicyFactory {
63    fn new_inherited() -> Self {
64        Self {
65            reflector_: Reflector::new(),
66            default_policy: Default::default(),
67            policy_names: RefCell::new(vec![]),
68        }
69    }
70
71    pub(crate) fn new(cx: &mut js::context::JSContext, global: &GlobalScope) -> DomRoot<Self> {
72        reflect_dom_object_with_cx(Box::new(Self::new_inherited()), global, cx)
73    }
74
75    /// <https://www.w3.org/TR/trusted-types/#create-trusted-type-policy-algorithm>
76    fn create_trusted_type_policy(
77        &self,
78        cx: &mut js::context::JSContext,
79        policy_name: String,
80        options: &TrustedTypePolicyOptions,
81        global: &GlobalScope,
82    ) -> Fallible<DomRoot<TrustedTypePolicy>> {
83        // Avoid double borrow on policy_names
84        {
85            // Step 1: Let allowedByCSP be the result of executing Should Trusted Type policy creation be blocked by
86            // Content Security Policy? algorithm with global, policyName and factory’s created policy names value.
87            let policy_names = self.policy_names.borrow();
88            let policy_names: Vec<&str> = policy_names.iter().map(String::as_ref).collect();
89            let allowed_by_csp = global
90                .get_csp_list()
91                .is_trusted_type_policy_creation_allowed(global, &policy_name, &policy_names);
92
93            // Step 2: If allowedByCSP is "Blocked", throw a TypeError and abort further steps.
94            if !allowed_by_csp {
95                return Err(Error::Type(c"Not allowed by CSP".to_owned()));
96            }
97        }
98
99        // Step 3: If policyName is default and the factory’s default policy value is not null, throw a TypeError
100        // and abort further steps.
101        if policy_name == "default" && self.default_policy.get().is_some() {
102            return Err(Error::Type(
103                c"Already set default policy for factory".to_owned(),
104            ));
105        }
106
107        // Step 4: Let policy be a new TrustedTypePolicy object.
108        // Step 5: Set policy’s name property value to policyName.
109        // Step 6: Set policy’s options value to «[ "createHTML" ->
110        // options["createHTML", "createScript" -> options["createScript",
111        // "createScriptURL" -> options["createScriptURL" ]».
112        let policy = TrustedTypePolicy::new(cx, policy_name.clone(), options, global);
113        // Step 7: If the policyName is default, set the factory’s default policy value to policy.
114        if policy_name == "default" {
115            self.default_policy.set(Some(&policy))
116        }
117        // Step 8: Append policyName to factory’s created policy names.
118        self.policy_names.borrow_mut().push(policy_name);
119        // Step 9: Return policy.
120        Ok(policy)
121    }
122
123    /// <https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-get-trusted-type-data-for-attribute>
124    #[expect(clippy::if_same_then_else)]
125    fn get_trusted_type_data_for_attribute(
126        element_namespace: &Namespace,
127        element_name: &LocalName,
128        attribute: &str,
129        attribute_namespace: Option<&Namespace>,
130    ) -> Option<(TrustedType, String)> {
131        // Step 1: Let data be null.
132        //
133        // We return the if directly
134        // Step 2: If attributeNs is null, « HTML namespace, SVG namespace, MathML namespace » contains
135        // element’s namespace, and attribute is the name of an event handler content attribute:
136        if attribute_namespace.is_none() &&
137            matches!(*element_namespace, ns!(html) | ns!(svg) | ns!(mathml)) &&
138            EventTarget::is_content_event_handler(attribute)
139        {
140            // Step 2.1. Return (Element, null, attribute, TrustedScript, "Element " + attribute).
141            return Some((
142                TrustedType::TrustedScript,
143                "Element ".to_owned() + attribute,
144            ));
145        }
146        // Step 3: Find the row in the following table, where element is in the first column,
147        // attributeNs is in the second column, and attribute is in the third column.
148        // If a matching row is found, set data to that row.
149        // Step 4: Return data.
150        if *element_namespace == ns!(html) &&
151            *element_name == local_name!("iframe") &&
152            attribute_namespace.is_none() &&
153            attribute == "srcdoc"
154        {
155            Some((
156                TrustedType::TrustedHTML,
157                "HTMLIFrameElement srcdoc".to_owned(),
158            ))
159        } else if *element_namespace == ns!(html) &&
160            *element_name == local_name!("script") &&
161            attribute_namespace.is_none() &&
162            attribute == "src"
163        {
164            Some((
165                TrustedType::TrustedScriptURL,
166                "HTMLScriptElement src".to_owned(),
167            ))
168        } else if *element_namespace == ns!(svg) &&
169            *element_name == local_name!("script") &&
170            attribute_namespace.is_none() &&
171            attribute == "href"
172        {
173            Some((
174                TrustedType::TrustedScriptURL,
175                "SVGScriptElement href".to_owned(),
176            ))
177        } else if *element_namespace == ns!(svg) &&
178            *element_name == local_name!("script") &&
179            attribute_namespace == Some(&ns!(xlink)) &&
180            attribute == "href"
181        {
182            Some((
183                TrustedType::TrustedScriptURL,
184                "SVGScriptElement href".to_owned(),
185            ))
186        } else {
187            None
188        }
189    }
190
191    /// <https://w3c.github.io/trusted-types/dist/spec/#validate-attribute-mutation>
192    pub(crate) fn get_trusted_types_compliant_attribute_value(
193        cx: &mut js::context::JSContext,
194        element_namespace: &Namespace,
195        element_name: &LocalName,
196        attribute: &str,
197        attribute_namespace: Option<&Namespace>,
198        new_value: TrustedTypeOrString,
199        global: &GlobalScope,
200    ) -> Fallible<DOMString> {
201        // Step 1. If attributeNs is the empty string, set attributeNs to null.
202        let attribute_namespace =
203            attribute_namespace.and_then(|a| if *a == ns!() { None } else { Some(a) });
204        // Step 2. Set attributeData to the result of Get Trusted Type data for attribute algorithm,
205        // with the following arguments:
206        let Some(attribute_data) = Self::get_trusted_type_data_for_attribute(
207            element_namespace,
208            element_name,
209            attribute,
210            attribute_namespace,
211        ) else {
212            // Step 3. If attributeData is null, then:
213            // Step 3.1. If newValue is a string, return newValue.
214            // Step 3.2. Assert: newValue is TrustedHTML or TrustedScript or TrustedScriptURL.
215            // Step 3.3. Return value’s associated data.
216            return Ok(new_value.convert());
217        };
218        // Step 4. Let expectedType be the value of the fourth member of attributeData.
219        // Step 5. Let sink be the value of the fifth member of attributeData.
220        let (expected_type, sink) = attribute_data;
221        let new_value = if let TrustedTypeOrString::String(str_) = new_value {
222            str_
223        } else {
224            // If the type was already trusted, we should return immediately as
225            // all callers of `get_trusted_type_compliant_string` implement this
226            // check themselves. However, we should only do this if it matches
227            // the expected type.
228            if expected_type.matches_idl_trusted_type(&new_value) {
229                return Ok(new_value.convert());
230            }
231            new_value.convert()
232        };
233        // Step 6. Return the result of executing Get Trusted Type compliant string with the following arguments:
234        // If the algorithm threw an error, rethrow the error.
235        Self::get_trusted_type_compliant_string(
236            cx,
237            expected_type,
238            global,
239            new_value,
240            &sink,
241            DEFAULT_SCRIPT_SINK_GROUP,
242        )
243    }
244
245    /// <https://w3c.github.io/trusted-types/dist/spec/#process-value-with-a-default-policy-algorithm>
246    pub(crate) fn process_value_with_default_policy(
247        cx: &mut js::context::JSContext,
248        expected_type: TrustedType,
249        global: &GlobalScope,
250        input: DOMString,
251        sink: &str,
252    ) -> Fallible<Option<DOMString>> {
253        // Step 1: Let defaultPolicy be the value of global’s trusted type policy factory's default policy.
254        let global_policy_factory = global.trusted_types(cx);
255        let default_policy = match global_policy_factory.default_policy.get() {
256            None => return Ok(None),
257            Some(default_policy) => default_policy,
258        };
259        // Step 2: Let policyValue be the result of executing Get Trusted Type policy value,
260        // with the following arguments:
261        rooted!(&in(cx) let mut trusted_type_name_value = NullValue());
262        expected_type.as_ref().safe_to_jsval(
263            cx.into(),
264            trusted_type_name_value.handle_mut(),
265            CanGc::from_cx(cx),
266        );
267
268        rooted!(&in(cx) let mut sink_value = NullValue());
269        sink.safe_to_jsval(cx.into(), sink_value.handle_mut(), CanGc::from_cx(cx));
270
271        let arguments = vec![trusted_type_name_value.handle(), sink_value.handle()];
272        let policy_value = default_policy.get_trusted_type_policy_value(
273            cx,
274            expected_type,
275            input,
276            arguments,
277            false,
278        );
279        let data_string = match policy_value {
280            // Step 3: If the algorithm threw an error, rethrow the error and abort the following steps.
281            Err(error) => return Err(error),
282            Ok(policy_value) => match policy_value {
283                // Step 4: If policyValue is null or undefined, return policyValue.
284                None => return Ok(None),
285                // Step 5: Let dataString be the result of stringifying policyValue.
286                Some(policy_value) => policy_value,
287            },
288        };
289        Ok(Some(data_string))
290    }
291    /// Step 1 is implemented by the caller
292    /// <https://w3c.github.io/trusted-types/dist/spec/#get-trusted-type-compliant-string-algorithm>
293    pub(crate) fn get_trusted_type_compliant_string(
294        cx: &mut js::context::JSContext,
295        expected_type: TrustedType,
296        global: &GlobalScope,
297        input: DOMString,
298        sink: &str,
299        sink_group: &str,
300    ) -> Fallible<DOMString> {
301        // Step 2: Let requireTrustedTypes be the result of executing Does sink type require trusted types?
302        // algorithm, passing global, sinkGroup, and true.
303        let require_trusted_types = global
304            .get_csp_list()
305            .does_sink_type_require_trusted_types(sink_group, true);
306        // Step 3: If requireTrustedTypes is false, return stringified input and abort these steps.
307        if !require_trusted_types {
308            return Ok(input);
309        }
310        // Step 4: Let convertedInput be the result of executing Process value with a default policy
311        // with the same arguments as this algorithm.
312        let converted_input = TrustedTypePolicyFactory::process_value_with_default_policy(
313            cx,
314            expected_type,
315            global,
316            input.clone(),
317            sink,
318        );
319        // Step 5: If the algorithm threw an error, rethrow the error and abort the following steps.
320        match converted_input? {
321            // Step 6: If convertedInput is null or undefined, execute the following steps:
322            None => {
323                // Step 6.1: Let disposition be the result of executing Should sink type mismatch violation
324                // be blocked by Content Security Policy? algorithm, passing global,
325                // stringified input as source, sinkGroup and sink.
326                let is_blocked = global
327                    .get_csp_list()
328                    .should_sink_type_mismatch_violation_be_blocked_by_csp(
329                        global,
330                        sink,
331                        sink_group,
332                        &input.str(),
333                    );
334                // Step 6.2: If disposition is “Allowed”, return stringified input and abort further steps.
335                if !is_blocked {
336                    Ok(input)
337                } else {
338                    // Step 6.3: Throw a TypeError and abort further steps.
339                    Err(Error::Type(
340                        c"Cannot set value, expected trusted type".to_owned(),
341                    ))
342                }
343            },
344            // Step 8: Return stringified convertedInput.
345            Some(converted_input) => Ok(converted_input),
346        }
347        // Step 7: Assert: convertedInput is an instance of expectedType.
348        // TODO(https://github.com/w3c/trusted-types/issues/566): Implement when spec is resolved
349    }
350
351    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-isscript>
352    pub(crate) fn is_trusted_script(
353        cx: JSContext,
354        value: HandleValue,
355    ) -> Result<DomRoot<TrustedScript>, ()> {
356        root_from_handlevalue::<TrustedScript>(value, cx)
357    }
358}
359
360impl TrustedTypePolicyFactoryMethods<crate::DomTypeHolder> for TrustedTypePolicyFactory {
361    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-createpolicy>
362    fn CreatePolicy(
363        &self,
364        cx: &mut js::context::JSContext,
365        policy_name: DOMString,
366        options: &TrustedTypePolicyOptions,
367    ) -> Fallible<DomRoot<TrustedTypePolicy>> {
368        self.create_trusted_type_policy(cx, policy_name.to_string(), options, &self.global())
369    }
370    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-ishtml>
371    fn IsHTML(&self, cx: JSContext, value: HandleValue) -> bool {
372        root_from_handlevalue::<TrustedHTML>(value, cx).is_ok()
373    }
374    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-isscript>
375    fn IsScript(&self, cx: JSContext, value: HandleValue) -> bool {
376        TrustedTypePolicyFactory::is_trusted_script(cx, value).is_ok()
377    }
378    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-isscripturl>
379    fn IsScriptURL(&self, cx: JSContext, value: HandleValue) -> bool {
380        root_from_handlevalue::<TrustedScriptURL>(value, cx).is_ok()
381    }
382    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-emptyhtml>
383    fn EmptyHTML(&self, cx: &mut js::context::JSContext) -> DomRoot<TrustedHTML> {
384        TrustedHTML::new(cx, DOMString::new(), &self.global())
385    }
386    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-emptyscript>
387    fn EmptyScript(&self, cx: &mut js::context::JSContext) -> DomRoot<TrustedScript> {
388        TrustedScript::new(cx, DOMString::new(), &self.global())
389    }
390    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-getattributetype>
391    fn GetAttributeType(
392        &self,
393        tag_name: DOMString,
394        attribute: DOMString,
395        element_namespace: Option<DOMString>,
396        attribute_namespace: Option<DOMString>,
397    ) -> Option<DOMString> {
398        // Step 1: Set localName to tagName in ASCII lowercase.
399        let local_name = tag_name.to_ascii_lowercase();
400        // Step 2: Set attribute to attribute in ASCII lowercase.
401        let attribute = attribute.to_ascii_lowercase();
402        // Step 3: If elementNs is null or an empty string, set elementNs to HTML namespace.
403        let element_namespace = match element_namespace {
404            Some(namespace) if !namespace.is_empty() => Namespace::from(namespace),
405            Some(_) | None => ns!(html),
406        };
407        // Step 4: If attrNs is an empty string, set attrNs to null.
408        let attribute_namespace = match attribute_namespace {
409            Some(namespace) if !namespace.is_empty() => Some(Namespace::from(namespace)),
410            Some(_) | None => None,
411        };
412        // Step 5: Let interface be the element interface for localName and elementNs.
413        // Step 6: Let expectedType be null.
414        // Step 7: Set attributeData to the result of Get Trusted Type data for attribute algorithm,
415        // with the following arguments: interface as element, attribute, attrNs
416        // Step 8: If attributeData is not null, then set expectedType to the interface’s name of
417        // the value of the fourth member of attributeData.
418        // Step 9: Return expectedType.
419        TrustedTypePolicyFactory::get_trusted_type_data_for_attribute(
420            &element_namespace,
421            &LocalName::from(local_name),
422            &attribute,
423            attribute_namespace.as_ref(),
424        )
425        .map(|tuple| DOMString::from(tuple.0.as_ref()))
426    }
427    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-getpropertytype>
428    #[expect(clippy::if_same_then_else)]
429    fn GetPropertyType(
430        &self,
431        tag_name: DOMString,
432        property: DOMString,
433        element_namespace: Option<DOMString>,
434    ) -> Option<DOMString> {
435        // Step 1: Set localName to tagName in ASCII lowercase.
436        let local_name = tag_name.to_ascii_lowercase();
437        // Step 2: If elementNs is null or an empty string, set elementNs to HTML namespace.
438        let element_namespace = match element_namespace {
439            Some(namespace) if !namespace.is_empty() => Namespace::from(namespace),
440            Some(_) | None => ns!(html),
441        };
442        // Step 3: Let interface be the element interface for localName and elementNs.
443        let interface = QualName::new(None, element_namespace, LocalName::from(local_name));
444        // Step 4: Let expectedType be null.
445        let mut expected_type = None;
446        // Step 5: Find the row in the following table, where the first column is "*" or interface’s name,
447        // and property is in the second column. If a matching row is found, set expectedType to
448        // the interface’s name of the value of the third column.
449        let property = property.str();
450        if interface.ns == ns!(html) &&
451            interface.local == local_name!("iframe") &&
452            property == "srcdoc"
453        {
454            expected_type = Some(DOMString::from("TrustedHTML"))
455        } else if interface.ns == ns!(html) &&
456            interface.local == local_name!("script") &&
457            property == "innerText"
458        {
459            expected_type = Some(DOMString::from("TrustedScript"))
460        } else if interface.ns == ns!(html) &&
461            interface.local == local_name!("script") &&
462            property == "src"
463        {
464            expected_type = Some(DOMString::from("TrustedScriptURL"))
465        } else if interface.ns == ns!(html) &&
466            interface.local == local_name!("script") &&
467            property == "text"
468        {
469            expected_type = Some(DOMString::from("TrustedScript"))
470        } else if interface.ns == ns!(html) &&
471            interface.local == local_name!("script") &&
472            property == "textContent"
473        {
474            expected_type = Some(DOMString::from("TrustedScript"))
475        } else if property == "innerHTML" {
476            expected_type = Some(DOMString::from("TrustedHTML"))
477        } else if property == "outerHTML" {
478            expected_type = Some(DOMString::from("TrustedHTML"))
479        }
480        // Step 6: Return expectedType.
481        expected_type
482    }
483    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-defaultpolicy>
484    fn GetDefaultPolicy(&self) -> Option<DomRoot<TrustedTypePolicy>> {
485        self.default_policy.get()
486    }
487}
488
489impl GlobalScope {
490    fn trusted_types(&self, cx: &mut js::context::JSContext) -> DomRoot<TrustedTypePolicyFactory> {
491        if let Some(window) = self.downcast::<Window>() {
492            return window.TrustedTypes(cx);
493        }
494        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
495            return worker.TrustedTypes(cx);
496        }
497        unreachable!();
498    }
499}