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
7/// Used to determine which inline check to run
8pub use content_security_policy::InlineCheckType;
9/// Used to report CSP violations in Fetch handlers
10pub use content_security_policy::Violation;
11use content_security_policy::{
12    CheckResult, CspList, Destination, Element as CspElement, Initiator, NavigationCheckType,
13    Origin, ParserMetadata, PolicyDisposition, PolicySource, Request, Response as CspResponse,
14    ViolationResource,
15};
16use http::header::{HeaderMap, HeaderValue, ValueIter};
17use hyper_serde::Serde;
18use js::rust::describe_scripted_caller;
19use log::warn;
20use servo_constellation_traits::{LoadData, LoadOrigin};
21use url::Url;
22
23use super::csppolicyviolationreport::CSPViolationReportBuilder;
24use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
25use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
26use crate::dom::bindings::inheritance::Castable;
27use crate::dom::bindings::refcounted::Trusted;
28use crate::dom::bindings::root::DomRoot;
29use crate::dom::element::Element;
30use crate::dom::globalscope::GlobalScope;
31use crate::dom::node::{Node, NodeTraits};
32use crate::dom::reporting::reportingobserver::ReportingObserver;
33use crate::dom::security::cspviolationreporttask::CSPViolationReportTask;
34use crate::dom::trustedtypes::trustedscript::TrustedScript;
35use crate::dom::window::Window;
36
37pub(crate) trait CspReporting {
38    fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool;
39    fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool;
40    fn should_navigation_request_be_blocked(
41        &self,
42        cx: &mut js::context::JSContext,
43        global: &GlobalScope,
44        load_data: &mut LoadData,
45        element: Option<&Element>,
46    ) -> bool;
47    fn should_navigation_response_to_navigation_request_be_blocked(
48        &self,
49        window: &Window,
50        url: Url,
51        self_origin: &url::Origin,
52    ) -> bool;
53    fn should_elements_inline_type_behavior_be_blocked(
54        &self,
55        global: &GlobalScope,
56        el: &Element,
57        type_: InlineCheckType,
58        source: &str,
59        current_line: u32,
60    ) -> bool;
61    fn is_trusted_type_policy_creation_allowed(
62        &self,
63        global: &GlobalScope,
64        policy_name: &str,
65        created_policy_names: &[&str],
66    ) -> bool;
67    fn does_sink_type_require_trusted_types(
68        &self,
69        sink_group: &str,
70        include_report_only_policies: bool,
71    ) -> bool;
72    fn should_sink_type_mismatch_violation_be_blocked_by_csp(
73        &self,
74        global: &GlobalScope,
75        sink: &str,
76        sink_group: &str,
77        source: &str,
78    ) -> bool;
79    fn is_base_allowed_for_document(
80        &self,
81        global: &GlobalScope,
82        base: &url::Url,
83        self_origin: &url::Origin,
84    ) -> bool;
85    fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList>;
86}
87
88impl CspReporting for Option<CspList> {
89    /// <https://www.w3.org/TR/CSP/#can-compile-strings>
90    fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool {
91        let Some(csp_list) = self else {
92            return true;
93        };
94
95        let (is_js_evaluation_allowed, violations) = csp_list.is_js_evaluation_allowed(source);
96
97        global.report_csp_violations(violations, None, None);
98
99        is_js_evaluation_allowed == CheckResult::Allowed
100    }
101
102    /// <https://www.w3.org/TR/CSP/#can-compile-wasm-bytes>
103    fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool {
104        let Some(csp_list) = self else {
105            return true;
106        };
107
108        let (is_wasm_evaluation_allowed, violations) = csp_list.is_wasm_evaluation_allowed();
109
110        global.report_csp_violations(violations, None, None);
111
112        is_wasm_evaluation_allowed == CheckResult::Allowed
113    }
114
115    /// <https://www.w3.org/TR/CSP/#should-block-navigation-request>
116    fn should_navigation_request_be_blocked(
117        &self,
118        cx: &mut js::context::JSContext,
119        global: &GlobalScope,
120        load_data: &mut LoadData,
121        element: Option<&Element>,
122    ) -> bool {
123        let Some(csp_list) = self else {
124            return false;
125        };
126        let mut request = Request {
127            url: load_data.url.clone().into_url(),
128            // TODO: Figure out how to propagate redirect data from LoadData into here
129            current_url: load_data.url.clone().into_url(),
130            origin: match &load_data.load_origin {
131                LoadOrigin::Script(origin) => origin.immutable().clone().into_url_origin(),
132                _ => Origin::new_opaque(),
133            },
134            // TODO: populate this field correctly
135            redirect_count: 0,
136            destination: Destination::None,
137            initiator: Initiator::None,
138            nonce: "".to_owned(),
139            integrity_metadata: "".to_owned(),
140            parser_metadata: ParserMetadata::None,
141        };
142        // TODO: set correct navigation check type for form submission if applicable
143        let (result, violations) = csp_list.should_navigation_request_be_blocked(
144            &mut request,
145            NavigationCheckType::Other,
146            |script_source| {
147                // Step 4. Let convertedScriptSource be the result of executing
148                // Process value with a default policy algorithm, with the following arguments:
149                TrustedScript::get_trusted_type_compliant_string(
150                    cx,
151                    global,
152                    TrustedScriptOrString::String(script_source.into()),
153                    "Location href",
154                )
155                .ok()
156                .map(|s| s.into())
157            },
158        );
159
160        // In case trusted types processing has changed the Javascript contents
161        load_data.url = request.url.into();
162
163        global.report_csp_violations(violations, element, None);
164
165        result == CheckResult::Blocked
166    }
167
168    /// <https://w3c.github.io/webappsec-csp/#should-block-navigation-response>
169    fn should_navigation_response_to_navigation_request_be_blocked(
170        &self,
171        window: &Window,
172        url: Url,
173        self_origin: &url::Origin,
174    ) -> bool {
175        let Some(csp_list) = self else {
176            return false;
177        };
178
179        let mut window_proxy = window.window_proxy();
180        let mut parent_navigable_origins = vec![];
181        loop {
182            // Same-origin parents can go via their own script-thread (fast-path)
183            if let Some(container_element) = window_proxy.frame_element() {
184                let container_document = container_element.owner_document();
185                let parent_origin = Url::parse(
186                    &container_document
187                        .origin()
188                        .immutable()
189                        .ascii_serialization(),
190                )
191                .expect("Must always be able to parse document origin");
192                parent_navigable_origins.push(parent_origin);
193                window_proxy = container_document.window().window_proxy();
194                continue;
195            }
196            // Cross-origin parents go via the constellation (slower)
197            if let Some(parent_proxy) = window_proxy.parent() {
198                let Some(parent_origin) = parent_proxy.document_origin() else {
199                    break;
200                };
201                let parent_origin = Url::parse(&parent_origin)
202                    .expect("Must always be able to parse document origin");
203                parent_navigable_origins.push(parent_origin);
204                window_proxy = DomRoot::from_ref(parent_proxy);
205                continue;
206            }
207            // We don't have a parent, hence we stop traversing
208            break;
209        }
210
211        let (is_navigation_response_blocked, violations) = csp_list
212            .should_navigation_response_to_navigation_request_be_blocked(
213                &CspResponse {
214                    url,
215                    redirect_count: 0,
216                },
217                self_origin,
218                &parent_navigable_origins,
219            );
220
221        window
222            .as_global_scope()
223            .report_csp_violations(violations, None, None);
224
225        is_navigation_response_blocked == CheckResult::Blocked
226    }
227
228    /// <https://www.w3.org/TR/CSP/#should-block-inline>
229    fn should_elements_inline_type_behavior_be_blocked(
230        &self,
231        global: &GlobalScope,
232        el: &Element,
233        type_: InlineCheckType,
234        source: &str,
235        current_line: u32,
236    ) -> bool {
237        let Some(csp_list) = self else {
238            return false;
239        };
240        let element = CspElement {
241            nonce: if el.is_nonceable() {
242                Some(Cow::Owned(el.nonce_value().trim().to_owned()))
243            } else {
244                None
245            },
246        };
247        let (result, violations) =
248            csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
249
250        let source_position = el.compute_source_position(current_line.saturating_sub(2).max(1));
251
252        global.report_csp_violations(violations, Some(el), Some(source_position));
253
254        result == CheckResult::Blocked
255    }
256
257    /// <https://w3c.github.io/trusted-types/dist/spec/#should-block-create-policy>
258    fn is_trusted_type_policy_creation_allowed(
259        &self,
260        global: &GlobalScope,
261        policy_name: &str,
262        created_policy_names: &[&str],
263    ) -> bool {
264        let Some(csp_list) = self else {
265            return true;
266        };
267
268        let (allowed_by_csp, violations) =
269            csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
270
271        global.report_csp_violations(violations, None, None);
272
273        allowed_by_csp == CheckResult::Allowed
274    }
275
276    /// <https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-does-sink-type-require-trusted-types>
277    fn does_sink_type_require_trusted_types(
278        &self,
279        sink_group: &str,
280        include_report_only_policies: bool,
281    ) -> bool {
282        let Some(csp_list) = self else {
283            return false;
284        };
285
286        csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
287    }
288
289    /// <https://w3c.github.io/trusted-types/dist/spec/#should-block-sink-type-mismatch>
290    fn should_sink_type_mismatch_violation_be_blocked_by_csp(
291        &self,
292        global: &GlobalScope,
293        sink: &str,
294        sink_group: &str,
295        source: &str,
296    ) -> bool {
297        let Some(csp_list) = self else {
298            return false;
299        };
300
301        let (allowed_by_csp, violations) = csp_list
302            .should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
303
304        global.report_csp_violations(violations, None, None);
305
306        allowed_by_csp == CheckResult::Blocked
307    }
308
309    /// <https://www.w3.org/TR/CSP3/#allow-base-for-document>
310    fn is_base_allowed_for_document(
311        &self,
312        global: &GlobalScope,
313        base: &url::Url,
314        self_origin: &url::Origin,
315    ) -> bool {
316        let Some(csp_list) = self else {
317            return true;
318        };
319
320        let (is_base_allowed, violations) =
321            csp_list.is_base_allowed_for_document(base, self_origin);
322
323        global.report_csp_violations(violations, None, None);
324
325        is_base_allowed == CheckResult::Allowed
326    }
327
328    fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
329        let Some(new_csp_list) = new_csp_list else {
330            return self;
331        };
332
333        match self {
334            None => Some(new_csp_list),
335            Some(mut old_csp_list) => {
336                old_csp_list.append(new_csp_list);
337                Some(old_csp_list)
338            },
339        }
340    }
341}
342
343pub(crate) struct SourcePosition {
344    pub(crate) source_file: String,
345    pub(crate) line_number: u32,
346    pub(crate) column_number: u32,
347}
348
349pub(crate) trait GlobalCspReporting {
350    fn report_csp_violations(
351        &self,
352        violations: Vec<Violation>,
353        element: Option<&Element>,
354        source_position: Option<SourcePosition>,
355    );
356}
357
358#[expect(unsafe_code)]
359fn compute_scripted_caller_source_position() -> SourcePosition {
360    match unsafe { describe_scripted_caller(*GlobalScope::get_cx()) } {
361        Ok(scripted_caller) => SourcePosition {
362            source_file: scripted_caller.filename,
363            line_number: scripted_caller.line,
364            column_number: scripted_caller.col + 1,
365        },
366        Err(()) => SourcePosition {
367            source_file: String::new(),
368            line_number: 0,
369            column_number: 0,
370        },
371    }
372}
373
374/// <https://www.w3.org/TR/CSP3/#obtain-violation-blocked-uri>
375fn obtain_blocked_uri_for_violation_resource_with_sample(
376    resource: ViolationResource,
377) -> (Option<String>, String) {
378    // Step 1. Assert: resource is a URL or a string.
379    //
380    // Already done since we destructure the relevant enum value
381
382    // Step 3. Return resource.
383    match resource {
384        ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
385        // Step 2. If resource is a URL, return the result of executing § 5.4 Strip URL for use in reports on resource.
386        ViolationResource::Url(url) => (
387            Some(String::new()),
388            ReportingObserver::strip_url_for_reports(url.into()),
389        ),
390        ViolationResource::TrustedTypePolicy { sample } => {
391            (Some(sample), "trusted-types-policy".to_owned())
392        },
393        ViolationResource::TrustedTypeSink { sample } => {
394            (Some(sample), "trusted-types-sink".to_owned())
395        },
396        ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
397        ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
398    }
399}
400
401impl GlobalCspReporting for GlobalScope {
402    /// <https://www.w3.org/TR/CSP/#report-violation>
403    fn report_csp_violations(
404        &self,
405        violations: Vec<Violation>,
406        element: Option<&Element>,
407        source_position: Option<SourcePosition>,
408    ) {
409        if violations.is_empty() {
410            return;
411        }
412        warn!("Reporting CSP violations: {:?}", violations);
413        let source_position =
414            source_position.unwrap_or_else(compute_scripted_caller_source_position);
415        for violation in violations {
416            let (sample, resource) =
417                obtain_blocked_uri_for_violation_resource_with_sample(violation.resource);
418            let report = CSPViolationReportBuilder::default()
419                .resource(resource)
420                .sample(sample)
421                .effective_directive(violation.directive.name)
422                .original_policy(violation.policy.to_string())
423                .report_only(violation.policy.disposition == PolicyDisposition::Report)
424                .source_file(source_position.source_file.clone())
425                .line_number(source_position.line_number)
426                .column_number(source_position.column_number)
427                .build(self);
428            // Step 1: Let global be violation’s global object.
429            // We use `self` as `global`;
430            // Step 2: Let target be violation’s element.
431            let target = element.and_then(|event_target| {
432                // Step 3.1: If target is not null, and global is a Window,
433                // and target’s shadow-including root is not global’s associated Document, set target to null.
434                if let Some(window) = self.downcast::<Window>() {
435                    // If a node is connected, its owner document is always the shadow-including root.
436                    // If it isn't connected, then it also doesn't have a corresponding document, hence
437                    // it can't be this document.
438                    if event_target.upcast::<Node>().owner_document() != window.Document() {
439                        return None;
440                    }
441                }
442                Some(event_target)
443            });
444            let target = match target {
445                // Step 3.2: If target is null:
446                None => {
447                    // Step 3.2.2: If target is a Window, set target to target’s associated Document.
448                    if let Some(window) = self.downcast::<Window>() {
449                        Trusted::new(window.Document().upcast())
450                    } else {
451                        // Step 3.2.1: Set target to violation’s global object.
452                        Trusted::new(self.upcast())
453                    }
454                },
455                Some(event_target) => Trusted::new(event_target.upcast()),
456            };
457            // Step 3: Queue a task to run the following steps:
458            let task =
459                CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy);
460            self.task_manager()
461                .dom_manipulation_task_source()
462                .queue(task);
463        }
464    }
465}
466
467fn parse_and_potentially_append_to_csp_list(
468    old_csp_list: Option<CspList>,
469    csp_header_iter: ValueIter<HeaderValue>,
470    disposition: PolicyDisposition,
471) -> Option<CspList> {
472    let mut csp_list = old_csp_list;
473    for header in csp_header_iter {
474        // This silently ignores the CSP if it contains invalid Unicode.
475        // We should probably report an error somewhere.
476        let new_csp_list = header
477            .to_str()
478            .ok()
479            .map(|value| CspList::parse(value, PolicySource::Header, disposition));
480        csp_list = csp_list.concatenate(new_csp_list);
481    }
482    csp_list
483}
484
485/// <https://www.w3.org/TR/CSP/#parse-response-csp>
486pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
487    let headers = headers.as_ref()?;
488    let csp_enforce_list = parse_and_potentially_append_to_csp_list(
489        None,
490        headers.get_all("content-security-policy").iter(),
491        PolicyDisposition::Enforce,
492    );
493
494    parse_and_potentially_append_to_csp_list(
495        csp_enforce_list,
496        headers
497            .get_all("content-security-policy-report-only")
498            .iter(),
499        PolicyDisposition::Report,
500    )
501}