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