Files
servo/components/script/dom/security/csp.rs
Tim van der Lippe 6656061fc3 script: Implement frame-ancestors CSP check (#43630)
Requires communication between the script thread and the constellation
to be able to retrieve the origin of a cross-origin document. However,
in the fast-path where the document resides in the same script-thread,
we use the `frame_element` instead.

If no CSP list is active, then we skip all this logic, to have a minimal
impact on document navigation.

Part of #4577
Fixes #36468

---------

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
Signed-off-by: Tim van der Lippe <TimvdLippe@users.noreply.github.com>
2026-03-28 08:54:17 +00:00

502 lines
18 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 std::borrow::Cow;
/// Used to determine which inline check to run
pub use content_security_policy::InlineCheckType;
/// Used to report CSP violations in Fetch handlers
pub use content_security_policy::Violation;
use content_security_policy::{
CheckResult, CspList, Destination, Element as CspElement, Initiator, NavigationCheckType,
Origin, ParserMetadata, PolicyDisposition, PolicySource, Request, Response as CspResponse,
ViolationResource,
};
use http::header::{HeaderMap, HeaderValue, ValueIter};
use hyper_serde::Serde;
use js::rust::describe_scripted_caller;
use log::warn;
use servo_constellation_traits::{LoadData, LoadOrigin};
use url::Url;
use super::csppolicyviolationreport::CSPViolationReportBuilder;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::root::DomRoot;
use crate::dom::element::Element;
use crate::dom::globalscope::GlobalScope;
use crate::dom::node::{Node, NodeTraits};
use crate::dom::reporting::reportingobserver::ReportingObserver;
use crate::dom::security::cspviolationreporttask::CSPViolationReportTask;
use crate::dom::trustedtypes::trustedscript::TrustedScript;
use crate::dom::window::Window;
pub(crate) trait CspReporting {
fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool;
fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool;
fn should_navigation_request_be_blocked(
&self,
cx: &mut js::context::JSContext,
global: &GlobalScope,
load_data: &mut LoadData,
element: Option<&Element>,
) -> bool;
fn should_navigation_response_to_navigation_request_be_blocked(
&self,
window: &Window,
url: Url,
self_origin: &url::Origin,
) -> bool;
fn should_elements_inline_type_behavior_be_blocked(
&self,
global: &GlobalScope,
el: &Element,
type_: InlineCheckType,
source: &str,
current_line: u32,
) -> bool;
fn is_trusted_type_policy_creation_allowed(
&self,
global: &GlobalScope,
policy_name: &str,
created_policy_names: &[&str],
) -> bool;
fn does_sink_type_require_trusted_types(
&self,
sink_group: &str,
include_report_only_policies: bool,
) -> bool;
fn should_sink_type_mismatch_violation_be_blocked_by_csp(
&self,
global: &GlobalScope,
sink: &str,
sink_group: &str,
source: &str,
) -> bool;
fn is_base_allowed_for_document(
&self,
global: &GlobalScope,
base: &url::Url,
self_origin: &url::Origin,
) -> bool;
fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList>;
}
impl CspReporting for Option<CspList> {
/// <https://www.w3.org/TR/CSP/#can-compile-strings>
fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool {
let Some(csp_list) = self else {
return true;
};
let (is_js_evaluation_allowed, violations) = csp_list.is_js_evaluation_allowed(source);
global.report_csp_violations(violations, None, None);
is_js_evaluation_allowed == CheckResult::Allowed
}
/// <https://www.w3.org/TR/CSP/#can-compile-wasm-bytes>
fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool {
let Some(csp_list) = self else {
return true;
};
let (is_wasm_evaluation_allowed, violations) = csp_list.is_wasm_evaluation_allowed();
global.report_csp_violations(violations, None, None);
is_wasm_evaluation_allowed == CheckResult::Allowed
}
/// <https://www.w3.org/TR/CSP/#should-block-navigation-request>
fn should_navigation_request_be_blocked(
&self,
cx: &mut js::context::JSContext,
global: &GlobalScope,
load_data: &mut LoadData,
element: Option<&Element>,
) -> bool {
let Some(csp_list) = self else {
return false;
};
let mut request = Request {
url: load_data.url.clone().into_url(),
// TODO: Figure out how to propagate redirect data from LoadData into here
current_url: load_data.url.clone().into_url(),
origin: match &load_data.load_origin {
LoadOrigin::Script(origin) => origin.immutable().clone().into_url_origin(),
_ => Origin::new_opaque(),
},
// TODO: populate this field correctly
redirect_count: 0,
destination: Destination::None,
initiator: Initiator::None,
nonce: "".to_owned(),
integrity_metadata: "".to_owned(),
parser_metadata: ParserMetadata::None,
};
// TODO: set correct navigation check type for form submission if applicable
let (result, violations) = csp_list.should_navigation_request_be_blocked(
&mut request,
NavigationCheckType::Other,
|script_source| {
// Step 4. Let convertedScriptSource be the result of executing
// Process value with a default policy algorithm, with the following arguments:
TrustedScript::get_trusted_type_compliant_string(
cx,
global,
TrustedScriptOrString::String(script_source.into()),
"Location href",
)
.ok()
.map(|s| s.into())
},
);
// In case trusted types processing has changed the Javascript contents
load_data.url = request.url.into();
global.report_csp_violations(violations, element, None);
result == CheckResult::Blocked
}
/// <https://w3c.github.io/webappsec-csp/#should-block-navigation-response>
fn should_navigation_response_to_navigation_request_be_blocked(
&self,
window: &Window,
url: Url,
self_origin: &url::Origin,
) -> bool {
let Some(csp_list) = self else {
return false;
};
let mut window_proxy = window.window_proxy();
let mut parent_navigable_origins = vec![];
loop {
// Same-origin parents can go via their own script-thread (fast-path)
if let Some(container_element) = window_proxy.frame_element() {
let container_document = container_element.owner_document();
let parent_origin = Url::parse(
&container_document
.origin()
.immutable()
.ascii_serialization(),
)
.expect("Must always be able to parse document origin");
parent_navigable_origins.push(parent_origin);
window_proxy = container_document.window().window_proxy();
continue;
}
// Cross-origin parents go via the constellation (slower)
if let Some(parent_proxy) = window_proxy.parent() {
let Some(parent_origin) = parent_proxy.document_origin() else {
break;
};
let parent_origin = Url::parse(&parent_origin)
.expect("Must always be able to parse document origin");
parent_navigable_origins.push(parent_origin);
window_proxy = DomRoot::from_ref(parent_proxy);
continue;
}
// We don't have a parent, hence we stop traversing
break;
}
let (is_navigation_response_blocked, violations) = csp_list
.should_navigation_response_to_navigation_request_be_blocked(
&CspResponse {
url,
redirect_count: 0,
},
self_origin,
&parent_navigable_origins,
);
window
.as_global_scope()
.report_csp_violations(violations, None, None);
is_navigation_response_blocked == CheckResult::Blocked
}
/// <https://www.w3.org/TR/CSP/#should-block-inline>
fn should_elements_inline_type_behavior_be_blocked(
&self,
global: &GlobalScope,
el: &Element,
type_: InlineCheckType,
source: &str,
current_line: u32,
) -> bool {
let Some(csp_list) = self else {
return false;
};
let element = CspElement {
nonce: if el.is_nonceable() {
Some(Cow::Owned(el.nonce_value().trim().to_owned()))
} else {
None
},
};
let (result, violations) =
csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
let source_position = el.compute_source_position(current_line.saturating_sub(2).max(1));
global.report_csp_violations(violations, Some(el), Some(source_position));
result == CheckResult::Blocked
}
/// <https://w3c.github.io/trusted-types/dist/spec/#should-block-create-policy>
fn is_trusted_type_policy_creation_allowed(
&self,
global: &GlobalScope,
policy_name: &str,
created_policy_names: &[&str],
) -> bool {
let Some(csp_list) = self else {
return true;
};
let (allowed_by_csp, violations) =
csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
global.report_csp_violations(violations, None, None);
allowed_by_csp == CheckResult::Allowed
}
/// <https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-does-sink-type-require-trusted-types>
fn does_sink_type_require_trusted_types(
&self,
sink_group: &str,
include_report_only_policies: bool,
) -> bool {
let Some(csp_list) = self else {
return false;
};
csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
}
/// <https://w3c.github.io/trusted-types/dist/spec/#should-block-sink-type-mismatch>
fn should_sink_type_mismatch_violation_be_blocked_by_csp(
&self,
global: &GlobalScope,
sink: &str,
sink_group: &str,
source: &str,
) -> bool {
let Some(csp_list) = self else {
return false;
};
let (allowed_by_csp, violations) = csp_list
.should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
global.report_csp_violations(violations, None, None);
allowed_by_csp == CheckResult::Blocked
}
/// <https://www.w3.org/TR/CSP3/#allow-base-for-document>
fn is_base_allowed_for_document(
&self,
global: &GlobalScope,
base: &url::Url,
self_origin: &url::Origin,
) -> bool {
let Some(csp_list) = self else {
return true;
};
let (is_base_allowed, violations) =
csp_list.is_base_allowed_for_document(base, self_origin);
global.report_csp_violations(violations, None, None);
is_base_allowed == CheckResult::Allowed
}
fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
let Some(new_csp_list) = new_csp_list else {
return self;
};
match self {
None => Some(new_csp_list),
Some(mut old_csp_list) => {
old_csp_list.append(new_csp_list);
Some(old_csp_list)
},
}
}
}
pub(crate) struct SourcePosition {
pub(crate) source_file: String,
pub(crate) line_number: u32,
pub(crate) column_number: u32,
}
pub(crate) trait GlobalCspReporting {
fn report_csp_violations(
&self,
violations: Vec<Violation>,
element: Option<&Element>,
source_position: Option<SourcePosition>,
);
}
#[expect(unsafe_code)]
fn compute_scripted_caller_source_position() -> SourcePosition {
match unsafe { describe_scripted_caller(*GlobalScope::get_cx()) } {
Ok(scripted_caller) => SourcePosition {
source_file: scripted_caller.filename,
line_number: scripted_caller.line,
column_number: scripted_caller.col + 1,
},
Err(()) => SourcePosition {
source_file: String::new(),
line_number: 0,
column_number: 0,
},
}
}
/// <https://www.w3.org/TR/CSP3/#obtain-violation-blocked-uri>
fn obtain_blocked_uri_for_violation_resource_with_sample(
resource: ViolationResource,
) -> (Option<String>, String) {
// Step 1. Assert: resource is a URL or a string.
//
// Already done since we destructure the relevant enum value
// Step 3. Return resource.
match resource {
ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
// Step 2. If resource is a URL, return the result of executing §5.4 Strip URL for use in reports on resource.
ViolationResource::Url(url) => (
Some(String::new()),
ReportingObserver::strip_url_for_reports(url.into()),
),
ViolationResource::TrustedTypePolicy { sample } => {
(Some(sample), "trusted-types-policy".to_owned())
},
ViolationResource::TrustedTypeSink { sample } => {
(Some(sample), "trusted-types-sink".to_owned())
},
ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
}
}
impl GlobalCspReporting for GlobalScope {
/// <https://www.w3.org/TR/CSP/#report-violation>
fn report_csp_violations(
&self,
violations: Vec<Violation>,
element: Option<&Element>,
source_position: Option<SourcePosition>,
) {
if violations.is_empty() {
return;
}
warn!("Reporting CSP violations: {:?}", violations);
let source_position =
source_position.unwrap_or_else(compute_scripted_caller_source_position);
for violation in violations {
let (sample, resource) =
obtain_blocked_uri_for_violation_resource_with_sample(violation.resource);
let report = CSPViolationReportBuilder::default()
.resource(resource)
.sample(sample)
.effective_directive(violation.directive.name)
.original_policy(violation.policy.to_string())
.report_only(violation.policy.disposition == PolicyDisposition::Report)
.source_file(source_position.source_file.clone())
.line_number(source_position.line_number)
.column_number(source_position.column_number)
.build(self);
// Step 1: Let global be violations global object.
// We use `self` as `global`;
// Step 2: Let target be violations element.
let target = element.and_then(|event_target| {
// Step 3.1: If target is not null, and global is a Window,
// and targets shadow-including root is not globals associated Document, set target to null.
if let Some(window) = self.downcast::<Window>() {
// If a node is connected, its owner document is always the shadow-including root.
// If it isn't connected, then it also doesn't have a corresponding document, hence
// it can't be this document.
if event_target.upcast::<Node>().owner_document() != window.Document() {
return None;
}
}
Some(event_target)
});
let target = match target {
// Step 3.2: If target is null:
None => {
// Step 3.2.2: If target is a Window, set target to targets associated Document.
if let Some(window) = self.downcast::<Window>() {
Trusted::new(window.Document().upcast())
} else {
// Step 3.2.1: Set target to violations global object.
Trusted::new(self.upcast())
}
},
Some(event_target) => Trusted::new(event_target.upcast()),
};
// Step 3: Queue a task to run the following steps:
let task =
CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy);
self.task_manager()
.dom_manipulation_task_source()
.queue(task);
}
}
}
fn parse_and_potentially_append_to_csp_list(
old_csp_list: Option<CspList>,
csp_header_iter: ValueIter<HeaderValue>,
disposition: PolicyDisposition,
) -> Option<CspList> {
let mut csp_list = old_csp_list;
for header in csp_header_iter {
// This silently ignores the CSP if it contains invalid Unicode.
// We should probably report an error somewhere.
let new_csp_list = header
.to_str()
.ok()
.map(|value| CspList::parse(value, PolicySource::Header, disposition));
csp_list = csp_list.concatenate(new_csp_list);
}
csp_list
}
/// <https://www.w3.org/TR/CSP/#parse-response-csp>
pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
let headers = headers.as_ref()?;
let csp_enforce_list = parse_and_potentially_append_to_csp_list(
None,
headers.get_all("content-security-policy").iter(),
PolicyDisposition::Enforce,
);
parse_and_potentially_append_to_csp_list(
csp_enforce_list,
headers
.get_all("content-security-policy-report-only")
.iter(),
PolicyDisposition::Report,
)
}