Skip to main content

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