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