servo/components/script/dom/csppolicyviolationreport.rs
Tim van der Lippe 70c57c6136
Add support for Reporting-Endpoints (#37965)
Does not yet handle failures of endpoints, which requires us to update
metadata. I don't see that metadata being used anywhere, so I am not
sure if there is WPT coverage for it.

Part of #37238

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
2025-07-09 19:07:29 +00:00

245 lines
9.6 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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::CSPViolationReportBodyBinding::CSPViolationReportBody;
use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit;
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::ReportBody;
use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::{
SecurityPolicyViolationEventDisposition, SecurityPolicyViolationEventInit,
};
use crate::dom::globalscope::GlobalScope;
use crate::dom::reportingobserver::ReportingObserver;
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SecurityPolicyViolationReport {
sample: Option<String>,
#[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<String>,
source_file: Option<String>,
line_number: Option<u32>,
column_number: Option<u32>,
}
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct CSPReportUriViolationReport {
pub(crate) csp_report: CSPReportUriViolationReportBody,
}
impl Convert<SecurityPolicyViolationEventInit> 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 Convert<CSPViolationReportBody> for SecurityPolicyViolationReport {
fn convert(self) -> CSPViolationReportBody {
CSPViolationReportBody {
sample: self.sample.map(|s| s.into()),
blockedURL: Some(self.blocked_url.into()),
// TODO(37328): Why does /content-security-policy/reporting-api/
// report-to-directive-allowed-in-meta.https.sub.html expect this to be
// empty, yet the spec expects us to copy referrer from SecurityPolicyViolationReport
referrer: Some("".to_owned().into()),
statusCode: self.status_code,
documentURL: self.document_url.into(),
sourceFile: Some(self.source_file.into()),
effectiveDirective: self.effective_directive.into(),
lineNumber: Some(self.line_number),
columnNumber: Some(self.column_number),
originalPolicy: self.original_policy.into(),
disposition: self.disposition,
parent: ReportBody::empty(),
}
}
}
/// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation>
impl From<SecurityPolicyViolationReport> 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 violations 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 violations source file.
converted.source_file = ServoUrl::parse(&value.source_file)
.map(ReportingObserver::strip_url_for_reports)
.ok();
// Step 2.2. Set body["line-number"] to violations line number.
converted.line_number = Some(value.line_number);
// Step 2.3. Set body["column-number"] to violations 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,
/// <https://www.w3.org/TR/CSP3/#violation-sample>
pub sample: Option<String>,
/// <https://www.w3.org/TR/CSP3/#violation-resource>
pub resource: String,
/// <https://www.w3.org/TR/CSP3/#violation-line-number>
pub line_number: u32,
/// <https://www.w3.org/TR/CSP3/#violation-column-number>
pub column_number: u32,
/// <https://www.w3.org/TR/CSP3/#violation-source-file>
pub source_file: String,
/// <https://www.w3.org/TR/CSP3/#violation-effective-directive>
pub effective_directive: String,
/// <https://www.w3.org/TR/CSP3/#violation-policy>
pub original_policy: String,
}
impl CSPViolationReportBuilder {
pub fn report_only(mut self, report_only: bool) -> CSPViolationReportBuilder {
self.report_only = report_only;
self
}
/// <https://www.w3.org/TR/CSP3/#violation-sample>
pub fn sample(mut self, sample: Option<String>) -> CSPViolationReportBuilder {
self.sample = sample;
self
}
/// <https://www.w3.org/TR/CSP3/#violation-resource>
pub fn resource(mut self, resource: String) -> CSPViolationReportBuilder {
self.resource = resource;
self
}
/// <https://www.w3.org/TR/CSP3/#violation-line-number>
pub fn line_number(mut self, line_number: u32) -> CSPViolationReportBuilder {
self.line_number = line_number;
self
}
/// <https://www.w3.org/TR/CSP3/#violation-column-number>
pub fn column_number(mut self, column_number: u32) -> CSPViolationReportBuilder {
self.column_number = column_number;
self
}
/// <https://www.w3.org/TR/CSP3/#violation-source-file>
pub fn source_file(mut self, source_file: String) -> CSPViolationReportBuilder {
self.source_file = source_file;
self
}
/// <https://www.w3.org/TR/CSP3/#violation-effective-directive>
pub fn effective_directive(mut self, effective_directive: String) -> CSPViolationReportBuilder {
self.effective_directive = effective_directive;
self
}
/// <https://www.w3.org/TR/CSP3/#violation-policy>
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: ReportingObserver::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) => ReportingObserver::strip_url_for_reports(url),
Referrer::ReferrerUrl(url) => ReportingObserver::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),
}
}
}
pub(crate) fn serialize_disposition<S: serde::Serializer>(
val: &SecurityPolicyViolationEventDisposition,
serializer: S,
) -> Result<S::Ok, S::Error> {
match val {
SecurityPolicyViolationEventDisposition::Report => serializer.serialize_str("report"),
SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"),
}
}