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