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