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