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