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::reporting::reportingobserver::ReportingObserver;
30use crate::dom::security::cspviolationreporttask::CSPViolationReportTask;
31use crate::dom::trustedtypes::trustedscript::TrustedScript;
32use crate::dom::window::Window;
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        cx: &mut js::context::JSContext,
40        global: &GlobalScope,
41        load_data: &mut LoadData,
42        element: Option<&Element>,
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        cx: &mut js::context::JSContext,
110        global: &GlobalScope,
111        load_data: &mut LoadData,
112        element: Option<&Element>,
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_type_compliant_string(
139                    cx,
140                    global,
141                    TrustedScriptOrString::String(script_source.into()),
142                    "Location href",
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: if el.is_nonceable() {
171                Some(Cow::Owned(el.nonce_value().trim().to_owned()))
172            } else {
173                None
174            },
175        };
176        let (result, violations) =
177            csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
178
179        let source_position = el.compute_source_position(current_line.saturating_sub(2).max(1));
180
181        global.report_csp_violations(violations, Some(el), Some(source_position));
182
183        result == CheckResult::Blocked
184    }
185
186    /// <https://w3c.github.io/trusted-types/dist/spec/#should-block-create-policy>
187    fn is_trusted_type_policy_creation_allowed(
188        &self,
189        global: &GlobalScope,
190        policy_name: &str,
191        created_policy_names: &[&str],
192    ) -> bool {
193        let Some(csp_list) = self else {
194            return true;
195        };
196
197        let (allowed_by_csp, violations) =
198            csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
199
200        global.report_csp_violations(violations, None, None);
201
202        allowed_by_csp == CheckResult::Allowed
203    }
204
205    /// <https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-does-sink-type-require-trusted-types>
206    fn does_sink_type_require_trusted_types(
207        &self,
208        sink_group: &str,
209        include_report_only_policies: bool,
210    ) -> bool {
211        let Some(csp_list) = self else {
212            return false;
213        };
214
215        csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
216    }
217
218    /// <https://w3c.github.io/trusted-types/dist/spec/#should-block-sink-type-mismatch>
219    fn should_sink_type_mismatch_violation_be_blocked_by_csp(
220        &self,
221        global: &GlobalScope,
222        sink: &str,
223        sink_group: &str,
224        source: &str,
225    ) -> bool {
226        let Some(csp_list) = self else {
227            return false;
228        };
229
230        let (allowed_by_csp, violations) = csp_list
231            .should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
232
233        global.report_csp_violations(violations, None, None);
234
235        allowed_by_csp == CheckResult::Blocked
236    }
237
238    /// <https://www.w3.org/TR/CSP3/#allow-base-for-document>
239    fn is_base_allowed_for_document(
240        &self,
241        global: &GlobalScope,
242        base: &url::Url,
243        self_origin: &url::Origin,
244    ) -> bool {
245        let Some(csp_list) = self else {
246            return true;
247        };
248
249        let (is_base_allowed, violations) =
250            csp_list.is_base_allowed_for_document(base, self_origin);
251
252        global.report_csp_violations(violations, None, None);
253
254        is_base_allowed == CheckResult::Allowed
255    }
256
257    fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
258        let Some(new_csp_list) = new_csp_list else {
259            return self;
260        };
261
262        match self {
263            None => Some(new_csp_list),
264            Some(mut old_csp_list) => {
265                old_csp_list.append(new_csp_list);
266                Some(old_csp_list)
267            },
268        }
269    }
270}
271
272pub(crate) struct SourcePosition {
273    pub(crate) source_file: String,
274    pub(crate) line_number: u32,
275    pub(crate) column_number: u32,
276}
277
278pub(crate) trait GlobalCspReporting {
279    fn report_csp_violations(
280        &self,
281        violations: Vec<Violation>,
282        element: Option<&Element>,
283        source_position: Option<SourcePosition>,
284    );
285}
286
287#[expect(unsafe_code)]
288fn compute_scripted_caller_source_position() -> SourcePosition {
289    match unsafe { describe_scripted_caller(*GlobalScope::get_cx()) } {
290        Ok(scripted_caller) => SourcePosition {
291            source_file: scripted_caller.filename,
292            line_number: scripted_caller.line,
293            column_number: scripted_caller.col + 1,
294        },
295        Err(()) => SourcePosition {
296            source_file: String::new(),
297            line_number: 0,
298            column_number: 0,
299        },
300    }
301}
302
303/// <https://www.w3.org/TR/CSP3/#obtain-violation-blocked-uri>
304fn obtain_blocked_uri_for_violation_resource_with_sample(
305    resource: ViolationResource,
306) -> (Option<String>, String) {
307    // Step 1. Assert: resource is a URL or a string.
308    //
309    // Already done since we destructure the relevant enum value
310
311    // Step 3. Return resource.
312    match resource {
313        ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
314        // Step 2. If resource is a URL, return the result of executing § 5.4 Strip URL for use in reports on resource.
315        ViolationResource::Url(url) => (
316            Some(String::new()),
317            ReportingObserver::strip_url_for_reports(url.into()),
318        ),
319        ViolationResource::TrustedTypePolicy { sample } => {
320            (Some(sample), "trusted-types-policy".to_owned())
321        },
322        ViolationResource::TrustedTypeSink { sample } => {
323            (Some(sample), "trusted-types-sink".to_owned())
324        },
325        ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
326        ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
327    }
328}
329
330impl GlobalCspReporting for GlobalScope {
331    /// <https://www.w3.org/TR/CSP/#report-violation>
332    fn report_csp_violations(
333        &self,
334        violations: Vec<Violation>,
335        element: Option<&Element>,
336        source_position: Option<SourcePosition>,
337    ) {
338        if violations.is_empty() {
339            return;
340        }
341        warn!("Reporting CSP violations: {:?}", violations);
342        let source_position =
343            source_position.unwrap_or_else(compute_scripted_caller_source_position);
344        for violation in violations {
345            let (sample, resource) =
346                obtain_blocked_uri_for_violation_resource_with_sample(violation.resource);
347            let report = CSPViolationReportBuilder::default()
348                .resource(resource)
349                .sample(sample)
350                .effective_directive(violation.directive.name)
351                .original_policy(violation.policy.to_string())
352                .report_only(violation.policy.disposition == PolicyDisposition::Report)
353                .source_file(source_position.source_file.clone())
354                .line_number(source_position.line_number)
355                .column_number(source_position.column_number)
356                .build(self);
357            // Step 1: Let global be violation’s global object.
358            // We use `self` as `global`;
359            // Step 2: Let target be violation’s element.
360            let target = element.and_then(|event_target| {
361                // Step 3.1: If target is not null, and global is a Window,
362                // and target’s shadow-including root is not global’s associated Document, set target to null.
363                if let Some(window) = self.downcast::<Window>() {
364                    // If a node is connected, its owner document is always the shadow-including root.
365                    // If it isn't connected, then it also doesn't have a corresponding document, hence
366                    // it can't be this document.
367                    if event_target.upcast::<Node>().owner_document() != window.Document() {
368                        return None;
369                    }
370                }
371                Some(event_target)
372            });
373            let target = match target {
374                // Step 3.2: If target is null:
375                None => {
376                    // Step 3.2.2: If target is a Window, set target to target’s associated Document.
377                    if let Some(window) = self.downcast::<Window>() {
378                        Trusted::new(window.Document().upcast())
379                    } else {
380                        // Step 3.2.1: Set target to violation’s global object.
381                        Trusted::new(self.upcast())
382                    }
383                },
384                Some(event_target) => Trusted::new(event_target.upcast()),
385            };
386            // Step 3: Queue a task to run the following steps:
387            let task =
388                CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy);
389            self.task_manager()
390                .dom_manipulation_task_source()
391                .queue(task);
392        }
393    }
394}
395
396fn parse_and_potentially_append_to_csp_list(
397    old_csp_list: Option<CspList>,
398    csp_header_iter: ValueIter<HeaderValue>,
399    disposition: PolicyDisposition,
400) -> Option<CspList> {
401    let mut csp_list = old_csp_list;
402    for header in csp_header_iter {
403        // This silently ignores the CSP if it contains invalid Unicode.
404        // We should probably report an error somewhere.
405        let new_csp_list = header
406            .to_str()
407            .ok()
408            .map(|value| CspList::parse(value, PolicySource::Header, disposition));
409        csp_list = csp_list.concatenate(new_csp_list);
410    }
411    csp_list
412}
413
414/// <https://www.w3.org/TR/CSP/#parse-response-csp>
415pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
416    let headers = headers.as_ref()?;
417    let csp_enforce_list = parse_and_potentially_append_to_csp_list(
418        None,
419        headers.get_all("content-security-policy").iter(),
420        PolicyDisposition::Enforce,
421    );
422
423    parse_and_potentially_append_to_csp_list(
424        csp_enforce_list,
425        headers
426            .get_all("content-security-policy-report-only")
427            .iter(),
428        PolicyDisposition::Report,
429    )
430}