Skip to main content

script/dom/security/
cspviolationreporttask.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 bytes::Bytes;
6use content_security_policy as csp;
7use headers::{ContentType, HeaderMap, HeaderMapExt};
8use js::context::JSContext;
9use net_traits::request::{
10    CredentialsMode, Destination, RequestBody, RequestId, create_request_body_with_content,
11};
12use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
13use script_bindings::str::DOMString;
14use servo_url::ServoUrl;
15use stylo_atoms::Atom;
16
17use crate::conversions::Convert;
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::refcounted::Trusted;
20use crate::dom::csp::Violation;
21use crate::dom::csppolicyviolationreport::{
22    CSPReportUriViolationReport, SecurityPolicyViolationReport,
23};
24use crate::dom::event::{Event, EventBubbles, EventCancelable, EventComposed};
25use crate::dom::eventtarget::EventTarget;
26use crate::dom::performance::performanceresourcetiming::InitiatorType;
27use crate::dom::reporting::reportingobserver::ReportingObserver;
28use crate::dom::securitypolicyviolationevent::SecurityPolicyViolationEvent;
29use crate::dom::types::GlobalScope;
30use crate::fetch::fetch::{RequestWithGlobalScope, create_a_potential_cors_request_with_claim};
31use crate::fetch::network_listener::{
32    FetchResponseListener, ResourceTimingListener, submit_timing,
33};
34use crate::tasks::task::TaskOnce;
35use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
36
37pub(crate) struct CSPViolationReportTask {
38    global: Trusted<GlobalScope>,
39    event_target: Trusted<EventTarget>,
40    violation_report: SecurityPolicyViolationReport,
41    violation_policy: csp::Policy,
42}
43
44impl CSPViolationReportTask {
45    pub fn new(
46        global: Trusted<GlobalScope>,
47        event_target: Trusted<EventTarget>,
48        violation_report: SecurityPolicyViolationReport,
49        violation_policy: csp::Policy,
50    ) -> CSPViolationReportTask {
51        CSPViolationReportTask {
52            global,
53            event_target,
54            violation_report,
55            violation_policy,
56        }
57    }
58
59    fn fire_violation_event(&self, cx: &mut JSContext) {
60        let event = SecurityPolicyViolationEvent::new(
61            cx,
62            &self.global.root(),
63            Atom::from("securitypolicyviolation"),
64            EventBubbles::Bubbles,
65            EventCancelable::NotCancelable,
66            EventComposed::Composed,
67            &self.violation_report.clone().convert(),
68        );
69
70        event.upcast::<Event>().fire(cx, &self.event_target.root());
71    }
72
73    /// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation>
74    fn serialize_violation(&self) -> Option<RequestBody> {
75        let report_body = CSPReportUriViolationReport {
76            // Steps 1-3.
77            csp_report: self.violation_report.clone().into(),
78        };
79        // Step 4. Return the result of serialize an infra value to JSON bytes given «[ "csp-report" → body ]».
80        Some(create_request_body_with_content(
81            serde_json::to_string(&report_body).unwrap_or_default(),
82        ))
83    }
84
85    /// Step 3.4 of <https://www.w3.org/TR/CSP/#report-violation>
86    fn post_csp_violation_to_report_uri(&self, report_uri_directive: &csp::Directive) {
87        let global = self.global.root();
88        // Step 3.4.1. If violation’s policy’s directive set contains a directive named
89        // "report-to", skip the remaining substeps.
90        if self
91            .violation_policy
92            .contains_a_directive_whose_name_is("report-to")
93        {
94            return;
95        }
96        // Step 3.4.2. For each token of directive’s value:
97        for token in &report_uri_directive.value {
98            // Step 3.4.2.1. Let endpoint be the result of executing the URL parser with token as the input,
99            // and violation’s url as the base URL.
100            //
101            // TODO: Figure out if this should be the URL of the containing document or not in case
102            // the url points to a blob
103            let Ok(endpoint) = ServoUrl::parse_with_base(Some(&global.get_url()), token) else {
104                // Step 3.4.2.2. If endpoint is not a valid URL, skip the remaining substeps.
105                continue;
106            };
107            // Step 3.4.2.3. Let request be a new request, initialized as follows:
108            let mut headers = HeaderMap::with_capacity(1);
109            headers.typed_insert(ContentType::from(
110                "application/csp-report".parse::<mime::Mime>().unwrap(),
111            ));
112            let request_body = self.serialize_violation();
113            let url = ensure_blob_referenced_by_url_is_kept_alive(&global, endpoint.clone());
114            let request = create_a_potential_cors_request_with_claim(
115                None,
116                url,
117                Destination::Report,
118                None,
119                None,
120                global.get_referrer(),
121            )
122            .with_global_scope(&global)
123            .method(http::Method::POST)
124            .body(request_body)
125            .credentials_mode(CredentialsMode::CredentialsSameOrigin)
126            .headers(headers);
127            // Step 3.4.2.4. Fetch request. The result will be ignored.
128            global.fetch(
129                request,
130                CSPReportUriFetchListener { endpoint },
131                global.task_manager().networking_task_source().into(),
132            );
133        }
134    }
135}
136
137/// Corresponds to the operation in 5.5 Report Violation
138/// <https://w3c.github.io/webappsec-csp/#report-violation>
139/// > Queue a task to run the following steps:
140impl TaskOnce for CSPViolationReportTask {
141    fn run_once(self, cx: &mut JSContext) {
142        // > If target implements EventTarget, fire an event named securitypolicyviolation
143        // > that uses the SecurityPolicyViolationEvent interface
144        // > at target with its attributes initialized as follows:
145        self.fire_violation_event(cx);
146        // Step 3.4. If violation’s policy’s directive set contains a directive named "report-uri" directive:
147        if let Some(report_uri_directive) = self
148            .violation_policy
149            .directive_set
150            .iter()
151            .find(|directive| directive.name == "report-uri")
152        {
153            self.post_csp_violation_to_report_uri(report_uri_directive);
154        }
155        // Step 3.5. If violation’s policy’s directive set contains a directive named "report-to" directive:
156        if let Some(report_to_directive) = self
157            .violation_policy
158            .directive_set
159            .iter()
160            .find(|directive| directive.name == "report-to")
161        {
162            // Step 3.5.1. Let body be a new CSPViolationReportBody, initialized as follows:
163            let body = self.violation_report.clone().convert();
164            // Step 3.5.2. Let settings object be violation’s global object’s relevant settings object.
165            // Step 3.5.3. Generate and queue a report with the following arguments:
166            ReportingObserver::generate_and_queue_a_report(
167                &self.global.root(),
168                DOMString::from_static("csp-violation"),
169                Some(body),
170                report_to_directive.value.join(" ").into(),
171            )
172        }
173    }
174}
175
176struct CSPReportUriFetchListener {
177    /// Endpoint URL of this request.
178    endpoint: ServoUrl,
179}
180
181impl FetchResponseListener for CSPReportUriFetchListener {
182    fn process_request_body(&mut self, _: &GlobalScope, _: RequestId) {}
183
184    fn process_response(
185        &mut self,
186        _: &mut JSContext,
187        _: &GlobalScope,
188        _: RequestId,
189        fetch_metadata: Result<FetchMetadata, NetworkError>,
190    ) {
191        _ = fetch_metadata;
192    }
193
194    fn process_response_chunk(
195        &mut self,
196        _: &mut JSContext,
197        _: &GlobalScope,
198        _: RequestId,
199        chunk: Bytes,
200    ) {
201        _ = chunk;
202    }
203
204    fn process_response_eof(
205        self,
206        cx: &mut JSContext,
207        global: &GlobalScope,
208        _: RequestId,
209        response: Result<(), NetworkError>,
210        timing: ResourceFetchTiming,
211    ) {
212        submit_timing(cx, global, &self, &response, &timing)
213    }
214
215    fn process_csp_violations(
216        &mut self,
217        _cx: &mut JSContext,
218        _: &GlobalScope,
219        _request_id: RequestId,
220        _violations: Vec<Violation>,
221    ) {
222    }
223}
224
225impl ResourceTimingListener for CSPReportUriFetchListener {
226    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
227        (InitiatorType::Other, self.endpoint.clone())
228    }
229}