script/dom/security/
csppolicyviolationreport.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 net_traits::request::Referrer;
6use serde::Serialize;
7use servo_url::ServoUrl;
8
9use crate::conversions::Convert;
10use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
11use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit;
12use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::ReportBody;
13use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::{
14    SecurityPolicyViolationEventDisposition, SecurityPolicyViolationEventInit,
15};
16use crate::dom::globalscope::GlobalScope;
17use crate::dom::reporting::reportingobserver::ReportingObserver;
18
19#[derive(Clone)]
20pub(crate) struct SecurityPolicyViolationReport {
21    sample: Option<String>,
22    blocked_url: String,
23    referrer: String,
24    status_code: u16,
25    document_url: String,
26    source_file: String,
27    violated_directive: String,
28    effective_directive: String,
29    line_number: u32,
30    column_number: u32,
31    original_policy: String,
32    disposition: SecurityPolicyViolationEventDisposition,
33}
34
35#[derive(Serialize)]
36#[serde(rename_all = "kebab-case")]
37pub(crate) struct CSPReportUriViolationReportBody {
38    document_uri: String,
39    referrer: String,
40    blocked_uri: String,
41    effective_directive: String,
42    violated_directive: String,
43    original_policy: String,
44    #[serde(serialize_with = "serialize_disposition")]
45    disposition: SecurityPolicyViolationEventDisposition,
46    status_code: u16,
47    script_sample: Option<String>,
48    source_file: Option<String>,
49    line_number: Option<u32>,
50    column_number: Option<u32>,
51}
52
53#[derive(Serialize)]
54#[serde(rename_all = "kebab-case")]
55pub(crate) struct CSPReportUriViolationReport {
56    pub(crate) csp_report: CSPReportUriViolationReportBody,
57}
58
59impl Convert<SecurityPolicyViolationEventInit> for SecurityPolicyViolationReport {
60    fn convert(self) -> SecurityPolicyViolationEventInit {
61        SecurityPolicyViolationEventInit {
62            sample: self.sample.unwrap_or_default().into(),
63            blockedURI: self.blocked_url.into(),
64            referrer: self.referrer.into(),
65            statusCode: self.status_code,
66            documentURI: self.document_url.into(),
67            sourceFile: self.source_file.into(),
68            violatedDirective: self.violated_directive.into(),
69            effectiveDirective: self.effective_directive.into(),
70            lineNumber: self.line_number,
71            columnNumber: self.column_number,
72            originalPolicy: self.original_policy.into(),
73            disposition: self.disposition,
74            parent: EventInit::empty(),
75        }
76    }
77}
78
79impl Convert<CSPViolationReportBody> for SecurityPolicyViolationReport {
80    fn convert(self) -> CSPViolationReportBody {
81        let (source_file, line_number, column_number) = if !self.source_file.is_empty() {
82            (
83                Some(self.source_file.into()),
84                Some(self.line_number),
85                Some(self.column_number),
86            )
87        } else {
88            (None, None, None)
89        };
90        CSPViolationReportBody {
91            sample: self.sample.map(|s| s.into()),
92            blockedURL: Some(self.blocked_url.into()),
93            // TODO(37328): Why does /content-security-policy/reporting-api/
94            // report-to-directive-allowed-in-meta.https.sub.html expect this to be
95            // empty, yet the spec expects us to copy referrer from SecurityPolicyViolationReport
96            referrer: Some("".to_owned().into()),
97            statusCode: self.status_code,
98            documentURL: self.document_url.into(),
99            sourceFile: source_file,
100            effectiveDirective: self.effective_directive.into(),
101            lineNumber: line_number,
102            columnNumber: column_number,
103            originalPolicy: self.original_policy.into(),
104            disposition: self.disposition,
105            parent: ReportBody::empty(),
106        }
107    }
108}
109
110/// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation>
111impl From<SecurityPolicyViolationReport> for CSPReportUriViolationReportBody {
112    fn from(value: SecurityPolicyViolationReport) -> Self {
113        // Step 1. Let body be a map with its keys initialized as follows:
114        let mut converted = Self {
115            document_uri: value.document_url,
116            referrer: value.referrer,
117            blocked_uri: value.blocked_url,
118            effective_directive: value.effective_directive,
119            violated_directive: value.violated_directive,
120            original_policy: value.original_policy,
121            disposition: value.disposition,
122            status_code: value.status_code,
123            script_sample: None,
124            source_file: None,
125            line_number: None,
126            column_number: None,
127        };
128
129        // Step 2. If violation’s source file is not null:
130        if !value.source_file.is_empty() {
131            // Step 2.1. Set body["source-file'] to the result of
132            // executing § 5.4 Strip URL for use in reports on violation’s source file.
133            converted.source_file = ServoUrl::parse(&value.source_file)
134                .map(ReportingObserver::strip_url_for_reports)
135                .ok();
136            // Step 2.2. Set body["line-number"] to violation’s line number.
137            converted.line_number = Some(value.line_number);
138            // Step 2.3. Set body["column-number"] to violation’s column number.
139            converted.column_number = Some(value.column_number);
140        }
141
142        // Step 3. Assert: If body["blocked-uri"] is not "inline", then body["sample"] is the empty string.
143        debug_assert!(converted.blocked_uri == "inline" || converted.script_sample.is_none());
144
145        converted
146    }
147}
148
149#[derive(Default)]
150pub(crate) struct CSPViolationReportBuilder {
151    pub report_only: bool,
152    /// <https://www.w3.org/TR/CSP3/#violation-sample>
153    pub sample: Option<String>,
154    /// <https://www.w3.org/TR/CSP3/#violation-resource>
155    pub resource: String,
156    /// <https://www.w3.org/TR/CSP3/#violation-line-number>
157    pub line_number: u32,
158    /// <https://www.w3.org/TR/CSP3/#violation-column-number>
159    pub column_number: u32,
160    /// <https://www.w3.org/TR/CSP3/#violation-source-file>
161    pub source_file: String,
162    /// <https://www.w3.org/TR/CSP3/#violation-effective-directive>
163    pub effective_directive: String,
164    /// <https://www.w3.org/TR/CSP3/#violation-policy>
165    pub original_policy: String,
166}
167
168impl CSPViolationReportBuilder {
169    pub fn report_only(mut self, report_only: bool) -> CSPViolationReportBuilder {
170        self.report_only = report_only;
171        self
172    }
173
174    /// <https://www.w3.org/TR/CSP3/#violation-sample>
175    pub fn sample(mut self, sample: Option<String>) -> CSPViolationReportBuilder {
176        self.sample = sample;
177        self
178    }
179
180    /// <https://www.w3.org/TR/CSP3/#violation-resource>
181    pub fn resource(mut self, resource: String) -> CSPViolationReportBuilder {
182        self.resource = resource;
183        self
184    }
185
186    /// <https://www.w3.org/TR/CSP3/#violation-line-number>
187    pub fn line_number(mut self, line_number: u32) -> CSPViolationReportBuilder {
188        self.line_number = line_number;
189        self
190    }
191
192    /// <https://www.w3.org/TR/CSP3/#violation-column-number>
193    pub fn column_number(mut self, column_number: u32) -> CSPViolationReportBuilder {
194        self.column_number = column_number;
195        self
196    }
197
198    /// <https://www.w3.org/TR/CSP3/#violation-source-file>
199    pub fn source_file(mut self, source_file: String) -> CSPViolationReportBuilder {
200        self.source_file = source_file;
201        self
202    }
203
204    /// <https://www.w3.org/TR/CSP3/#violation-effective-directive>
205    pub fn effective_directive(mut self, effective_directive: String) -> CSPViolationReportBuilder {
206        self.effective_directive = effective_directive;
207        self
208    }
209
210    /// <https://www.w3.org/TR/CSP3/#violation-policy>
211    pub fn original_policy(mut self, original_policy: String) -> CSPViolationReportBuilder {
212        self.original_policy = original_policy;
213        self
214    }
215
216    pub fn build(self, global: &GlobalScope) -> SecurityPolicyViolationReport {
217        SecurityPolicyViolationReport {
218            violated_directive: self.effective_directive.clone(),
219            effective_directive: self.effective_directive.clone(),
220            document_url: ReportingObserver::strip_url_for_reports(global.get_url()),
221            disposition: match self.report_only {
222                true => SecurityPolicyViolationEventDisposition::Report,
223                false => SecurityPolicyViolationEventDisposition::Enforce,
224            },
225            // https://w3c.github.io/webappsec-csp/#violation-referrer
226            referrer: match global.get_referrer() {
227                Referrer::Client(url) => ReportingObserver::strip_url_for_reports(url),
228                Referrer::ReferrerUrl(url) => ReportingObserver::strip_url_for_reports(url),
229                _ => "".to_owned(),
230            },
231            sample: self.sample,
232            blocked_url: self.resource,
233            source_file: self.source_file,
234            original_policy: self.original_policy,
235            line_number: self.line_number,
236            column_number: self.column_number,
237            status_code: global.status_code().unwrap_or(0),
238        }
239    }
240}
241
242pub(crate) fn serialize_disposition<S: serde::Serializer>(
243    val: &SecurityPolicyViolationEventDisposition,
244    serializer: S,
245) -> Result<S::Ok, S::Error> {
246    match val {
247        SecurityPolicyViolationEventDisposition::Report => serializer.serialize_str("report"),
248        SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"),
249    }
250}