diff --git a/components/script/dom/csppolicyviolationreport.rs b/components/script/dom/csppolicyviolationreport.rs new file mode 100644 index 00000000000..1ab99c99bb7 --- /dev/null +++ b/components/script/dom/csppolicyviolationreport.rs @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use net_traits::request::Referrer; +use serde::Serialize; +use servo_url::ServoUrl; + +use crate::conversions::Convert; +use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit; +use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::{ + SecurityPolicyViolationEventDisposition, SecurityPolicyViolationEventInit, +}; +use crate::dom::globalscope::GlobalScope; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SecurityPolicyViolationReport { + sample: Option, + #[serde(rename = "blockedURL")] + blocked_url: String, + referrer: String, + status_code: u16, + #[serde(rename = "documentURL")] + document_url: String, + source_file: String, + violated_directive: String, + effective_directive: String, + line_number: u32, + column_number: u32, + original_policy: String, + #[serde(serialize_with = "serialize_disposition")] + disposition: SecurityPolicyViolationEventDisposition, +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct CSPReportUriViolationReportBody { + document_uri: String, + referrer: String, + blocked_uri: String, + effective_directive: String, + violated_directive: String, + original_policy: String, + #[serde(serialize_with = "serialize_disposition")] + disposition: SecurityPolicyViolationEventDisposition, + status_code: u16, + script_sample: Option, + source_file: Option, + line_number: Option, + column_number: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct CSPReportUriViolationReport { + pub(crate) csp_report: CSPReportUriViolationReportBody, +} + +impl Convert for SecurityPolicyViolationReport { + fn convert(self) -> SecurityPolicyViolationEventInit { + SecurityPolicyViolationEventInit { + sample: self.sample.unwrap_or_default().into(), + blockedURI: self.blocked_url.into(), + referrer: self.referrer.into(), + statusCode: self.status_code, + documentURI: self.document_url.into(), + sourceFile: self.source_file.into(), + violatedDirective: self.violated_directive.into(), + effectiveDirective: self.effective_directive.into(), + lineNumber: self.line_number, + columnNumber: self.column_number, + originalPolicy: self.original_policy.into(), + disposition: self.disposition, + parent: EventInit::empty(), + } + } +} + +/// +impl From for CSPReportUriViolationReportBody { + fn from(value: SecurityPolicyViolationReport) -> Self { + // Step 1. Let body be a map with its keys initialized as follows: + let mut converted = Self { + document_uri: value.document_url, + referrer: value.referrer, + blocked_uri: value.blocked_url, + effective_directive: value.effective_directive, + violated_directive: value.violated_directive, + original_policy: value.original_policy, + disposition: value.disposition, + status_code: value.status_code, + script_sample: None, + source_file: None, + line_number: None, + column_number: None, + }; + + // Step 2. If violation’s source file is not null: + if !value.source_file.is_empty() { + // Step 2.1. Set body["source-file'] to the result of + // executing § 5.4 Strip URL for use in reports on violation’s source file. + converted.source_file = ServoUrl::parse(&value.source_file) + .map(strip_url_for_reports) + .ok(); + // Step 2.2. Set body["line-number"] to violation’s line number. + converted.line_number = Some(value.line_number); + // Step 2.3. Set body["column-number"] to violation’s column number. + converted.column_number = Some(value.column_number); + } + + // Step 3. Assert: If body["blocked-uri"] is not "inline", then body["sample"] is the empty string. + debug_assert!(converted.blocked_uri == "inline" || converted.script_sample.is_none()); + + converted + } +} + +#[derive(Default)] +pub(crate) struct CSPViolationReportBuilder { + pub report_only: bool, + /// + pub sample: Option, + /// + pub resource: String, + /// + pub line_number: u32, + /// + pub column_number: u32, + /// + pub source_file: String, + /// + pub effective_directive: String, + /// + pub original_policy: String, +} + +impl CSPViolationReportBuilder { + pub fn report_only(mut self, report_only: bool) -> CSPViolationReportBuilder { + self.report_only = report_only; + self + } + + /// + pub fn sample(mut self, sample: Option) -> CSPViolationReportBuilder { + self.sample = sample; + self + } + + /// + pub fn resource(mut self, resource: String) -> CSPViolationReportBuilder { + self.resource = resource; + self + } + + /// + pub fn line_number(mut self, line_number: u32) -> CSPViolationReportBuilder { + self.line_number = line_number; + self + } + + /// + pub fn column_number(mut self, column_number: u32) -> CSPViolationReportBuilder { + self.column_number = column_number; + self + } + + /// + pub fn source_file(mut self, source_file: String) -> CSPViolationReportBuilder { + self.source_file = source_file; + self + } + + /// + pub fn effective_directive(mut self, effective_directive: String) -> CSPViolationReportBuilder { + self.effective_directive = effective_directive; + self + } + + /// + pub fn original_policy(mut self, original_policy: String) -> CSPViolationReportBuilder { + self.original_policy = original_policy; + self + } + + pub fn build(self, global: &GlobalScope) -> SecurityPolicyViolationReport { + SecurityPolicyViolationReport { + violated_directive: self.effective_directive.clone(), + effective_directive: self.effective_directive.clone(), + document_url: strip_url_for_reports(global.get_url()), + disposition: match self.report_only { + true => SecurityPolicyViolationEventDisposition::Report, + false => SecurityPolicyViolationEventDisposition::Enforce, + }, + // https://w3c.github.io/webappsec-csp/#violation-referrer + referrer: match global.get_referrer() { + Referrer::Client(url) => strip_url_for_reports(url), + Referrer::ReferrerUrl(url) => strip_url_for_reports(url), + _ => "".to_owned(), + }, + sample: self.sample, + blocked_url: self.resource, + source_file: self.source_file, + original_policy: self.original_policy, + line_number: self.line_number, + column_number: self.column_number, + status_code: global.status_code().unwrap_or(0), + } + } +} + +fn serialize_disposition( + val: &SecurityPolicyViolationEventDisposition, + serializer: S, +) -> Result { + match val { + SecurityPolicyViolationEventDisposition::Report => serializer.serialize_str("report"), + SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"), + } +} + +/// +fn strip_url_for_reports(mut url: ServoUrl) -> String { + let scheme = url.scheme(); + // > Step 1: If url’s scheme is not an HTTP(S) scheme, then return url’s scheme. + if scheme != "https" && scheme != "http" { + return scheme.to_owned(); + } + // > Step 2: Set url’s fragment to the empty string. + url.set_fragment(None); + // > Step 3: Set url’s username to the empty string. + let _ = url.set_username(""); + // > Step 4: Set url’s password to the empty string. + let _ = url.set_password(None); + // > Step 5: Return the result of executing the URL serializer on url. + url.into_string() +} diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index 011f7eb15b6..6c3a92136af 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -107,6 +107,7 @@ use crate::dom::bindings::weakref::{DOMTracker, WeakRef}; use crate::dom::blob::Blob; use crate::dom::broadcastchannel::BroadcastChannel; use crate::dom::crypto::Crypto; +use crate::dom::csppolicyviolationreport::CSPViolationReportBuilder; use crate::dom::dedicatedworkerglobalscope::{ DedicatedWorkerControlMsg, DedicatedWorkerGlobalScope, }; @@ -145,7 +146,7 @@ use crate::realms::{AlreadyInRealm, InRealm, enter_realm}; use crate::script_module::{DynamicModuleList, ModuleScript, ModuleTree, ScriptFetchOptions}; use crate::script_runtime::{CanGc, JSContext as SafeJSContext, ThreadSafeJSContext}; use crate::script_thread::{ScriptThread, with_script_thread}; -use crate::security_manager::{CSPViolationReportBuilder, CSPViolationReportTask}; +use crate::security_manager::CSPViolationReportTask; use crate::task_manager::TaskManager; use crate::task_source::SendableTaskSource; use crate::timers::{ diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 5d7f1966207..9b7a6ce4162 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -263,6 +263,7 @@ pub(crate) mod countqueuingstrategy; mod create; pub(crate) mod crypto; pub(crate) mod cryptokey; +pub(crate) mod csppolicyviolationreport; pub(crate) mod css; pub(crate) mod cssconditionrule; pub(crate) mod cssfontfacerule; diff --git a/components/script/security_manager.rs b/components/script/security_manager.rs index 89a8ba6ed89..ae6b4543285 100644 --- a/components/script/security_manager.rs +++ b/components/script/security_manager.rs @@ -7,23 +7,21 @@ use std::sync::{Arc, Mutex}; use content_security_policy as csp; use headers::{ContentType, HeaderMap, HeaderMapExt}; use net_traits::request::{ - CredentialsMode, Referrer, RequestBody, RequestId, create_request_body_with_content, + CredentialsMode, RequestBody, RequestId, create_request_body_with_content, }; use net_traits::{ FetchMetadata, FetchResponseListener, NetworkError, ResourceFetchTiming, ResourceTimingType, }; -use serde::Serialize; use servo_url::ServoUrl; use stylo_atoms::Atom; use crate::conversions::Convert; -use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit; -use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::{ - SecurityPolicyViolationEventDisposition, SecurityPolicyViolationEventInit, -}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::root::DomRoot; +use crate::dom::csppolicyviolationreport::{ + CSPReportUriViolationReport, SecurityPolicyViolationReport, +}; use crate::dom::event::{Event, EventBubbles, EventCancelable, EventComposed}; use crate::dom::eventtarget::EventTarget; use crate::dom::performanceresourcetiming::InitiatorType; @@ -41,143 +39,6 @@ pub(crate) struct CSPViolationReportTask { violation_policy: csp::Policy, } -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct SecurityPolicyViolationReport { - sample: Option, - #[serde(rename = "blockedURL")] - blocked_url: String, - referrer: String, - status_code: u16, - #[serde(rename = "documentURL")] - document_url: String, - source_file: String, - violated_directive: String, - effective_directive: String, - line_number: u32, - column_number: u32, - original_policy: String, - #[serde(serialize_with = "serialize_disposition")] - disposition: SecurityPolicyViolationEventDisposition, -} - -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -struct CSPReportUriViolationReportBody { - document_uri: String, - referrer: String, - blocked_uri: String, - effective_directive: String, - violated_directive: String, - original_policy: String, - #[serde(serialize_with = "serialize_disposition")] - disposition: SecurityPolicyViolationEventDisposition, - status_code: u16, - script_sample: Option, - source_file: Option, - line_number: Option, - column_number: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "kebab-case")] -struct CSPReportUriViolationReport { - csp_report: CSPReportUriViolationReportBody, -} - -#[derive(Default)] -pub(crate) struct CSPViolationReportBuilder { - pub report_only: bool, - /// - pub sample: Option, - /// - pub resource: String, - /// - pub line_number: u32, - /// - pub column_number: u32, - /// - pub source_file: String, - /// - pub effective_directive: String, - /// - pub original_policy: String, -} - -impl CSPViolationReportBuilder { - pub fn report_only(mut self, report_only: bool) -> CSPViolationReportBuilder { - self.report_only = report_only; - self - } - - /// - pub fn sample(mut self, sample: Option) -> CSPViolationReportBuilder { - self.sample = sample; - self - } - - /// - pub fn resource(mut self, resource: String) -> CSPViolationReportBuilder { - self.resource = resource; - self - } - - /// - pub fn line_number(mut self, line_number: u32) -> CSPViolationReportBuilder { - self.line_number = line_number; - self - } - - /// - pub fn column_number(mut self, column_number: u32) -> CSPViolationReportBuilder { - self.column_number = column_number; - self - } - - /// - pub fn source_file(mut self, source_file: String) -> CSPViolationReportBuilder { - self.source_file = source_file; - self - } - - /// - pub fn effective_directive(mut self, effective_directive: String) -> CSPViolationReportBuilder { - self.effective_directive = effective_directive; - self - } - - /// - pub fn original_policy(mut self, original_policy: String) -> CSPViolationReportBuilder { - self.original_policy = original_policy; - self - } - - pub fn build(self, global: &GlobalScope) -> SecurityPolicyViolationReport { - SecurityPolicyViolationReport { - violated_directive: self.effective_directive.clone(), - effective_directive: self.effective_directive.clone(), - document_url: strip_url_for_reports(global.get_url()), - disposition: match self.report_only { - true => SecurityPolicyViolationEventDisposition::Report, - false => SecurityPolicyViolationEventDisposition::Enforce, - }, - // https://w3c.github.io/webappsec-csp/#violation-referrer - referrer: match global.get_referrer() { - Referrer::Client(url) => strip_url_for_reports(url), - Referrer::ReferrerUrl(url) => strip_url_for_reports(url), - _ => "".to_owned(), - }, - sample: self.sample, - blocked_url: self.resource, - source_file: self.source_file, - original_policy: self.original_policy, - line_number: self.line_number, - column_number: self.column_number, - status_code: global.status_code().unwrap_or(0), - } - } -} - impl CSPViolationReportTask { pub fn new( global: Trusted, @@ -297,92 +158,6 @@ impl TaskOnce for CSPViolationReportTask { } } -impl Convert for SecurityPolicyViolationReport { - fn convert(self) -> SecurityPolicyViolationEventInit { - SecurityPolicyViolationEventInit { - sample: self.sample.unwrap_or_default().into(), - blockedURI: self.blocked_url.into(), - referrer: self.referrer.into(), - statusCode: self.status_code, - documentURI: self.document_url.into(), - sourceFile: self.source_file.into(), - violatedDirective: self.violated_directive.into(), - effectiveDirective: self.effective_directive.into(), - lineNumber: self.line_number, - columnNumber: self.column_number, - originalPolicy: self.original_policy.into(), - disposition: self.disposition, - parent: EventInit::empty(), - } - } -} - -/// -impl From for CSPReportUriViolationReportBody { - fn from(value: SecurityPolicyViolationReport) -> Self { - // Step 1. Let body be a map with its keys initialized as follows: - let mut converted = Self { - document_uri: value.document_url, - referrer: value.referrer, - blocked_uri: value.blocked_url, - effective_directive: value.effective_directive, - violated_directive: value.violated_directive, - original_policy: value.original_policy, - disposition: value.disposition, - status_code: value.status_code, - script_sample: None, - source_file: None, - line_number: None, - column_number: None, - }; - - // Step 2. If violation’s source file is not null: - if !value.source_file.is_empty() { - // Step 2.1. Set body["source-file'] to the result of - // executing § 5.4 Strip URL for use in reports on violation’s source file. - converted.source_file = ServoUrl::parse(&value.source_file) - .map(strip_url_for_reports) - .ok(); - // Step 2.2. Set body["line-number"] to violation’s line number. - converted.line_number = Some(value.line_number); - // Step 2.3. Set body["column-number"] to violation’s column number. - converted.column_number = Some(value.column_number); - } - - // Step 3. Assert: If body["blocked-uri"] is not "inline", then body["sample"] is the empty string. - debug_assert!(converted.blocked_uri == "inline" || converted.script_sample.is_none()); - - converted - } -} - -/// -fn strip_url_for_reports(mut url: ServoUrl) -> String { - let scheme = url.scheme(); - // > Step 1: If url’s scheme is not an HTTP(S) scheme, then return url’s scheme. - if scheme != "https" && scheme != "http" { - return scheme.to_owned(); - } - // > Step 2: Set url’s fragment to the empty string. - url.set_fragment(None); - // > Step 3: Set url’s username to the empty string. - let _ = url.set_username(""); - // > Step 4: Set url’s password to the empty string. - let _ = url.set_password(None); - // > Step 5: Return the result of executing the URL serializer on url. - url.into_string() -} - -fn serialize_disposition( - val: &SecurityPolicyViolationEventDisposition, - serializer: S, -) -> Result { - match val { - SecurityPolicyViolationEventDisposition::Report => serializer.serialize_str("report"), - SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"), - } -} - struct CSPReportUriFetchListener { /// Endpoint URL of this request. endpoint: ServoUrl,