script/dom/
csp.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/. */
4
5use std::borrow::Cow;
6
7use constellation_traits::{LoadData, LoadOrigin};
8/// Used to determine which inline check to run
9pub use content_security_policy::InlineCheckType;
10/// Used to report CSP violations in Fetch handlers
11pub use content_security_policy::Violation;
12use content_security_policy::{
13    CheckResult, CspList, Destination, Element as CspElement, Initiator, NavigationCheckType,
14    Origin, ParserMetadata, PolicyDisposition, PolicySource, Request, ViolationResource,
15};
16use http::header::{HeaderMap, HeaderValue, ValueIter};
17use hyper_serde::Serde;
18use js::rust::describe_scripted_caller;
19use log::warn;
20
21use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
22use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::refcounted::Trusted;
25use crate::dom::csppolicyviolationreport::CSPViolationReportBuilder;
26use crate::dom::element::Element;
27use crate::dom::globalscope::GlobalScope;
28use crate::dom::node::{Node, NodeTraits};
29use crate::dom::trustedscript::TrustedScript;
30use crate::dom::window::Window;
31use crate::script_runtime::CanGc;
32use crate::security_manager::CSPViolationReportTask;
33
34pub(crate) trait CspReporting {
35    fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool;
36    fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool;
37    fn should_navigation_request_be_blocked(
38        &self,
39        global: &GlobalScope,
40        load_data: &mut LoadData,
41        element: Option<&Element>,
42        can_gc: CanGc,
43    ) -> bool;
44    fn should_elements_inline_type_behavior_be_blocked(
45        &self,
46        global: &GlobalScope,
47        el: &Element,
48        type_: InlineCheckType,
49        source: &str,
50    ) -> bool;
51    fn is_trusted_type_policy_creation_allowed(
52        &self,
53        global: &GlobalScope,
54        policy_name: &str,
55        created_policy_names: &[&str],
56    ) -> bool;
57    fn does_sink_type_require_trusted_types(
58        &self,
59        sink_group: &str,
60        include_report_only_policies: bool,
61    ) -> bool;
62    fn should_sink_type_mismatch_violation_be_blocked_by_csp(
63        &self,
64        global: &GlobalScope,
65        sink: &str,
66        sink_group: &str,
67        source: &str,
68    ) -> bool;
69    fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList>;
70}
71
72impl CspReporting for Option<CspList> {
73    /// <https://www.w3.org/TR/CSP/#can-compile-strings>
74    fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool {
75        let Some(csp_list) = self else {
76            return true;
77        };
78
79        let (is_js_evaluation_allowed, violations) = csp_list.is_js_evaluation_allowed(source);
80
81        global.report_csp_violations(violations, None, None);
82
83        is_js_evaluation_allowed == CheckResult::Allowed
84    }
85
86    /// <https://www.w3.org/TR/CSP/#can-compile-wasm-bytes>
87    fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool {
88        let Some(csp_list) = self else {
89            return true;
90        };
91
92        let (is_wasm_evaluation_allowed, violations) = csp_list.is_wasm_evaluation_allowed();
93
94        global.report_csp_violations(violations, None, None);
95
96        is_wasm_evaluation_allowed == CheckResult::Allowed
97    }
98
99    /// <https://www.w3.org/TR/CSP/#should-block-navigation-request>
100    fn should_navigation_request_be_blocked(
101        &self,
102        global: &GlobalScope,
103        load_data: &mut LoadData,
104        element: Option<&Element>,
105        can_gc: CanGc,
106    ) -> bool {
107        let Some(csp_list) = self else {
108            return false;
109        };
110        let mut request = Request {
111            url: load_data.url.clone().into_url(),
112            origin: match &load_data.load_origin {
113                LoadOrigin::Script(immutable_origin) => immutable_origin.clone().into_url_origin(),
114                _ => Origin::new_opaque(),
115            },
116            // TODO: populate this field correctly
117            redirect_count: 0,
118            destination: Destination::None,
119            initiator: Initiator::None,
120            nonce: "".to_owned(),
121            integrity_metadata: "".to_owned(),
122            parser_metadata: ParserMetadata::None,
123        };
124        // TODO: set correct navigation check type for form submission if applicable
125        let (result, violations) = csp_list.should_navigation_request_be_blocked(
126            &mut request,
127            NavigationCheckType::Other,
128            |script_source| {
129                // Step 4. Let convertedScriptSource be the result of executing
130                // Process value with a default policy algorithm, with the following arguments:
131                TrustedScript::get_trusted_script_compliant_string(
132                    global,
133                    TrustedScriptOrString::String(script_source.into()),
134                    "Location href",
135                    can_gc,
136                )
137                .ok()
138                .map(|s| s.into())
139            },
140        );
141
142        // In case trusted types processing has changed the Javascript contents
143        load_data.url = request.url.into();
144
145        global.report_csp_violations(violations, element, None);
146
147        result == CheckResult::Blocked
148    }
149
150    /// <https://www.w3.org/TR/CSP/#should-block-inline>
151    fn should_elements_inline_type_behavior_be_blocked(
152        &self,
153        global: &GlobalScope,
154        el: &Element,
155        type_: InlineCheckType,
156        source: &str,
157    ) -> bool {
158        let Some(csp_list) = self else {
159            return false;
160        };
161        let element = CspElement {
162            nonce: el.nonce_value_if_nonceable().map(Cow::Owned),
163        };
164        let (result, violations) =
165            csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
166
167        global.report_csp_violations(violations, Some(el), None);
168
169        result == CheckResult::Blocked
170    }
171
172    /// <https://w3c.github.io/trusted-types/dist/spec/#should-block-create-policy>
173    fn is_trusted_type_policy_creation_allowed(
174        &self,
175        global: &GlobalScope,
176        policy_name: &str,
177        created_policy_names: &[&str],
178    ) -> bool {
179        let Some(csp_list) = self else {
180            return true;
181        };
182
183        let (allowed_by_csp, violations) =
184            csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
185
186        global.report_csp_violations(violations, None, None);
187
188        allowed_by_csp == CheckResult::Allowed
189    }
190
191    /// <https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-does-sink-type-require-trusted-types>
192    fn does_sink_type_require_trusted_types(
193        &self,
194        sink_group: &str,
195        include_report_only_policies: bool,
196    ) -> bool {
197        let Some(csp_list) = self else {
198            return false;
199        };
200
201        csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
202    }
203
204    /// <https://w3c.github.io/trusted-types/dist/spec/#should-block-sink-type-mismatch>
205    fn should_sink_type_mismatch_violation_be_blocked_by_csp(
206        &self,
207        global: &GlobalScope,
208        sink: &str,
209        sink_group: &str,
210        source: &str,
211    ) -> bool {
212        let Some(csp_list) = self else {
213            return false;
214        };
215
216        let (allowed_by_csp, violations) = csp_list
217            .should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
218
219        global.report_csp_violations(violations, None, None);
220
221        allowed_by_csp == CheckResult::Blocked
222    }
223
224    fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
225        let Some(new_csp_list) = new_csp_list else {
226            return self;
227        };
228
229        match self {
230            None => Some(new_csp_list),
231            Some(mut old_csp_list) => {
232                old_csp_list.append(new_csp_list);
233                Some(old_csp_list)
234            },
235        }
236    }
237}
238
239pub(crate) struct SourcePosition {
240    pub(crate) source_file: String,
241    pub(crate) line_number: u32,
242    pub(crate) column_number: u32,
243}
244
245pub(crate) trait GlobalCspReporting {
246    fn report_csp_violations(
247        &self,
248        violations: Vec<Violation>,
249        element: Option<&Element>,
250        source_position: Option<SourcePosition>,
251    );
252}
253
254#[allow(unsafe_code)]
255fn compute_scripted_caller_source_position() -> SourcePosition {
256    let scripted_caller =
257        unsafe { describe_scripted_caller(*GlobalScope::get_cx()) }.unwrap_or_default();
258
259    SourcePosition {
260        source_file: scripted_caller.filename,
261        line_number: scripted_caller.line,
262        column_number: scripted_caller.col + 1,
263    }
264}
265
266impl GlobalCspReporting for GlobalScope {
267    /// <https://www.w3.org/TR/CSP/#report-violation>
268    fn report_csp_violations(
269        &self,
270        violations: Vec<Violation>,
271        element: Option<&Element>,
272        source_position: Option<SourcePosition>,
273    ) {
274        if violations.is_empty() {
275            return;
276        }
277        warn!("Reporting CSP violations: {:?}", violations);
278        let source_position =
279            source_position.unwrap_or_else(compute_scripted_caller_source_position);
280        for violation in violations {
281            let (sample, resource) = match violation.resource {
282                ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
283                ViolationResource::Url(url) => (Some(String::new()), url.into()),
284                ViolationResource::TrustedTypePolicy { sample } => {
285                    (Some(sample), "trusted-types-policy".to_owned())
286                },
287                ViolationResource::TrustedTypeSink { sample } => {
288                    (Some(sample), "trusted-types-sink".to_owned())
289                },
290                ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
291                ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
292            };
293            let report = CSPViolationReportBuilder::default()
294                .resource(resource)
295                .sample(sample)
296                .effective_directive(violation.directive.name)
297                .original_policy(violation.policy.to_string())
298                .report_only(violation.policy.disposition == PolicyDisposition::Report)
299                .source_file(source_position.source_file.clone())
300                .line_number(source_position.line_number)
301                .column_number(source_position.column_number)
302                .build(self);
303            // Step 1: Let global be violation’s global object.
304            // We use `self` as `global`;
305            // Step 2: Let target be violation’s element.
306            let target = element.and_then(|event_target| {
307                // Step 3.1: If target is not null, and global is a Window,
308                // and target’s shadow-including root is not global’s associated Document, set target to null.
309                if let Some(window) = self.downcast::<Window>() {
310                    // If a node is connected, its owner document is always the shadow-including root.
311                    // If it isn't connected, then it also doesn't have a corresponding document, hence
312                    // it can't be this document.
313                    if event_target.upcast::<Node>().owner_document() != window.Document() {
314                        return None;
315                    }
316                }
317                Some(event_target)
318            });
319            let target = match target {
320                // Step 3.2: If target is null:
321                None => {
322                    // Step 3.2.2: If target is a Window, set target to target’s associated Document.
323                    if let Some(window) = self.downcast::<Window>() {
324                        Trusted::new(window.Document().upcast())
325                    } else {
326                        // Step 3.2.1: Set target to violation’s global object.
327                        Trusted::new(self.upcast())
328                    }
329                },
330                Some(event_target) => Trusted::new(event_target.upcast()),
331            };
332            // Step 3: Queue a task to run the following steps:
333            let task =
334                CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy);
335            self.task_manager()
336                .dom_manipulation_task_source()
337                .queue(task);
338        }
339    }
340}
341
342fn parse_and_potentially_append_to_csp_list(
343    old_csp_list: Option<CspList>,
344    csp_header_iter: ValueIter<HeaderValue>,
345    disposition: PolicyDisposition,
346) -> Option<CspList> {
347    let mut csp_list = old_csp_list;
348    for header in csp_header_iter {
349        // This silently ignores the CSP if it contains invalid Unicode.
350        // We should probably report an error somewhere.
351        let new_csp_list = header
352            .to_str()
353            .ok()
354            .map(|value| CspList::parse(value, PolicySource::Header, disposition));
355        csp_list = csp_list.concatenate(new_csp_list);
356    }
357    csp_list
358}
359
360/// <https://www.w3.org/TR/CSP/#parse-response-csp>
361pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
362    let headers = headers.as_ref()?;
363    let csp_enforce_list = parse_and_potentially_append_to_csp_list(
364        None,
365        headers.get_all("content-security-policy").iter(),
366        PolicyDisposition::Enforce,
367    );
368
369    parse_and_potentially_append_to_csp_list(
370        csp_enforce_list,
371        headers
372            .get_all("content-security-policy-report-only")
373            .iter(),
374        PolicyDisposition::Report,
375    )
376}