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