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