mirror of
https://github.com/lexmount/moli.git
synced 2026-10-08 08:01:01 +00:00
fix(csp): inherit creator policies in local workers
Carry header, meta, and report-only policies into blob/data dedicated, shared, and nested workers while retaining their self origin and report rules. Keep blob URLs linked to their creating environment until worker startup, then clone the policies for the worker lifetime. Preserve nested HTTP worker response policies and cover local worker requests, reporting metadata, and object URL metadata lifetimes.
This commit is contained in:
@@ -41,11 +41,12 @@ impl<OwnerId, PartitionId> Default for BlobEntries<OwnerId, PartitionId> {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ObjectUrlState<OwnerId, AccessKey> {
|
||||
struct ObjectUrlState<OwnerId, AccessKey, Metadata> {
|
||||
owner_id: Option<OwnerId>,
|
||||
lifetime_id: Option<u64>,
|
||||
blob_id: BlobId,
|
||||
access_key: Option<AccessKey>,
|
||||
metadata: Option<Metadata>,
|
||||
}
|
||||
|
||||
/// Renderer-neutral Blob and object URL backing store.
|
||||
@@ -54,13 +55,15 @@ struct ObjectUrlState<OwnerId, AccessKey> {
|
||||
/// counts. The embedding layer owns JS wrappers and calls the retain/release
|
||||
/// hooks from its finalizers.
|
||||
#[derive(Debug)]
|
||||
pub struct BlobStore<OwnerId, PartitionId, AccessKey = ()> {
|
||||
pub struct BlobStore<OwnerId, PartitionId, AccessKey = (), Metadata = ()> {
|
||||
blobs: Mutex<BlobEntries<OwnerId, PartitionId>>,
|
||||
next_blob_id: AtomicU64,
|
||||
object_urls: Mutex<HashMap<String, ObjectUrlState<OwnerId, AccessKey>>>,
|
||||
object_urls: Mutex<HashMap<String, ObjectUrlState<OwnerId, AccessKey, Metadata>>>,
|
||||
}
|
||||
|
||||
impl<OwnerId, PartitionId, AccessKey> Default for BlobStore<OwnerId, PartitionId, AccessKey> {
|
||||
impl<OwnerId, PartitionId, AccessKey, Metadata> Default
|
||||
for BlobStore<OwnerId, PartitionId, AccessKey, Metadata>
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blobs: Mutex::default(),
|
||||
@@ -70,7 +73,7 @@ impl<OwnerId, PartitionId, AccessKey> Default for BlobStore<OwnerId, PartitionId
|
||||
}
|
||||
}
|
||||
|
||||
impl<OwnerId, PartitionId, AccessKey> BlobStore<OwnerId, PartitionId, AccessKey>
|
||||
impl<OwnerId, PartitionId, AccessKey, Metadata> BlobStore<OwnerId, PartitionId, AccessKey, Metadata>
|
||||
where
|
||||
OwnerId: Copy + Eq + Hash,
|
||||
PartitionId: Eq,
|
||||
@@ -200,6 +203,26 @@ where
|
||||
blob_id: BlobId,
|
||||
origin: &str,
|
||||
access_key: Option<AccessKey>,
|
||||
) -> Option<String> {
|
||||
self.create_object_url_with_metadata(
|
||||
owner_id,
|
||||
lifetime_id,
|
||||
blob_id,
|
||||
origin,
|
||||
access_key,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Metadata belongs to the URL's creating environment and shares its lifetime.
|
||||
pub fn create_object_url_with_metadata(
|
||||
&self,
|
||||
owner_id: Option<OwnerId>,
|
||||
lifetime_id: Option<u64>,
|
||||
blob_id: BlobId,
|
||||
origin: &str,
|
||||
access_key: Option<AccessKey>,
|
||||
metadata: Option<Metadata>,
|
||||
) -> Option<String> {
|
||||
self.retain_blob_object_url_ref(blob_id)?;
|
||||
let mut object_urls = self.object_urls.lock();
|
||||
@@ -216,6 +239,7 @@ where
|
||||
lifetime_id,
|
||||
blob_id,
|
||||
access_key,
|
||||
metadata,
|
||||
},
|
||||
);
|
||||
Some(object_url)
|
||||
@@ -238,7 +262,7 @@ where
|
||||
fn revoke_object_url_if(
|
||||
&self,
|
||||
url: &str,
|
||||
is_authorized: impl FnOnce(&ObjectUrlState<OwnerId, AccessKey>) -> bool,
|
||||
is_authorized: impl FnOnce(&ObjectUrlState<OwnerId, AccessKey, Metadata>) -> bool,
|
||||
) -> bool {
|
||||
let state = {
|
||||
let mut object_urls = self.object_urls.lock();
|
||||
@@ -251,6 +275,14 @@ where
|
||||
true
|
||||
}
|
||||
|
||||
pub fn object_url_metadata(&self, url: &str) -> Option<Metadata>
|
||||
where
|
||||
Metadata: Clone,
|
||||
{
|
||||
let url = url.split_once('#').map_or(url, |(url, _)| url);
|
||||
self.object_urls.lock().get(url)?.metadata.clone()
|
||||
}
|
||||
|
||||
/// Return object URL bytes and MIME type, excluding its fragment.
|
||||
pub fn object_url_bytes_and_type(&self, url: &str) -> Option<(Vec<u8>, String)> {
|
||||
let (bytes, mime_type) = self.object_url_shared_bytes_and_type(url)?;
|
||||
@@ -398,6 +430,39 @@ fn random_uuid() -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn object_url_metadata_follows_url_lifetime_and_preserves_captured_environment() {
|
||||
let store = BlobStore::<u64, u64, (), Arc<Mutex<String>>>::default();
|
||||
let blob = store.create_blob(Some(1), None, b"source".to_vec(), String::new());
|
||||
let environment = Arc::new(Mutex::new("initial policy".to_owned()));
|
||||
let weak = Arc::downgrade(&environment);
|
||||
let url = store
|
||||
.create_object_url_with_metadata(
|
||||
Some(2),
|
||||
Some(9),
|
||||
blob,
|
||||
"https://example.test",
|
||||
None,
|
||||
Some(environment.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
*environment.lock() = "updated policy".to_owned();
|
||||
let captured = store.object_url_metadata(&format!("{url}#worker")).unwrap();
|
||||
assert_eq!(*captured.lock(), "updated policy");
|
||||
drop(environment);
|
||||
assert_eq!(store.cleanup_object_url_lifetime(2, 9), 1);
|
||||
assert!(store.object_url_metadata(&url).is_none());
|
||||
assert!(
|
||||
weak.upgrade().is_some(),
|
||||
"the consumer retains its captured environment"
|
||||
);
|
||||
drop(captured);
|
||||
assert!(
|
||||
weak.upgrade().is_none(),
|
||||
"cleanup must release URL environment metadata"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_url_access_keys_preserve_unauthorized_entries_and_release_authorized_entries() {
|
||||
let store = BlobStore::<u64, u64, String>::default();
|
||||
|
||||
@@ -50,8 +50,12 @@ struct ObjectUrlAccessKey {
|
||||
storage_key: moli_storage_key::MoliStorageKey,
|
||||
}
|
||||
|
||||
type RendererBlobStore =
|
||||
BlobStore<ResourceOwnerId, RendererStoragePartitionIdentity, ObjectUrlAccessKey>;
|
||||
type RendererBlobStore = BlobStore<
|
||||
ResourceOwnerId,
|
||||
RendererStoragePartitionIdentity,
|
||||
ObjectUrlAccessKey,
|
||||
crate::content_security_policy::ContentSecurityPolicySource,
|
||||
>;
|
||||
|
||||
static BLOB_STORE: OnceLock<RendererBlobStore> = OnceLock::new();
|
||||
|
||||
@@ -406,7 +410,17 @@ pub(super) fn create_object_url_for_object<'s>(
|
||||
let origin = storage_key.origin().to_owned();
|
||||
let lifetime_id = native_bridge::current_runtime_observable_context_token(scope)
|
||||
.map(native_bridge::RuntimeObservableContextToken::as_u64);
|
||||
blob_store().create_object_url_with_lifetime_and_access_key(
|
||||
let policy_source = if let Some(host_ptr) =
|
||||
crate::util::context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
let global = scope.get_current_context().global(scope);
|
||||
// SAFETY: the Window callback keeps its host alive for this call.
|
||||
unsafe { &*host_ptr }.local_worker_content_security_policy_source_for_global(scope, global)
|
||||
} else {
|
||||
crate::worker::worker_content_security_policy_snapshot(scope)
|
||||
.map(|policy| Arc::new(parking_lot::RwLock::new(policy)))
|
||||
};
|
||||
blob_store().create_object_url_with_metadata(
|
||||
owner_id,
|
||||
lifetime_id,
|
||||
blob_id,
|
||||
@@ -415,9 +429,16 @@ pub(super) fn create_object_url_for_object<'s>(
|
||||
partition,
|
||||
storage_key,
|
||||
}),
|
||||
policy_source,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn object_url_content_security_policy(
|
||||
url: &str,
|
||||
) -> Option<crate::content_security_policy::InheritedContentSecurityPolicy> {
|
||||
Some(blob_store().object_url_metadata(url)?.read().clone())
|
||||
}
|
||||
|
||||
pub(super) fn revoke_object_url(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
url: &str,
|
||||
|
||||
@@ -16,6 +16,9 @@ use percent_encoding::percent_decode_str;
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
mod inheritance;
|
||||
pub(crate) use inheritance::{ContentSecurityPolicySource, InheritedContentSecurityPolicy};
|
||||
|
||||
const CONNECT_SRC: &str = "connect-src";
|
||||
const CHILD_SRC: &str = "child-src";
|
||||
const DEFAULT_SRC: &str = "default-src";
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
use super::{ContentSecurityPolicyDisposition, ContentSecurityPolicyReportingEndpoints};
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
/// The source and delivery rules survive cloning a policy into a local Worker.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct InheritedContentSecurityPolicy {
|
||||
pub(crate) self_url: Option<Url>,
|
||||
pub(crate) header_policies: Vec<String>,
|
||||
pub(crate) meta_policies: Vec<String>,
|
||||
pub(crate) report_only_policies: Vec<String>,
|
||||
pub(crate) reporting_endpoints: ContentSecurityPolicyReportingEndpoints,
|
||||
}
|
||||
|
||||
pub(crate) type ContentSecurityPolicySource =
|
||||
Arc<parking_lot::RwLock<InheritedContentSecurityPolicy>>;
|
||||
|
||||
impl InheritedContentSecurityPolicy {
|
||||
pub(crate) fn policies(
|
||||
&self,
|
||||
disposition: ContentSecurityPolicyDisposition,
|
||||
) -> impl Iterator<Item = (&String, bool)> {
|
||||
let (headers, meta) = match disposition {
|
||||
ContentSecurityPolicyDisposition::Enforce => (
|
||||
self.header_policies.as_slice(),
|
||||
self.meta_policies.as_slice(),
|
||||
),
|
||||
ContentSecurityPolicyDisposition::Report => {
|
||||
(self.report_only_policies.as_slice(), &[][..])
|
||||
}
|
||||
};
|
||||
headers
|
||||
.iter()
|
||||
.map(|policy| (policy, true))
|
||||
.chain(meta.iter().map(|policy| (policy, false)))
|
||||
}
|
||||
|
||||
pub(crate) fn enforced_strings(&self) -> Vec<String> {
|
||||
self.policies(ContentSecurityPolicyDisposition::Enforce)
|
||||
.map(|(policy, _)| policy.to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn url_violation(
|
||||
&self,
|
||||
protected_url: &Url,
|
||||
request_url: &Url,
|
||||
kind: super::ContentSecurityPolicyResourceKind,
|
||||
redirect_status: super::ContentSecurityPolicyRedirectStatus,
|
||||
disposition: ContentSecurityPolicyDisposition,
|
||||
) -> Option<super::ContentSecurityPolicyUrlViolation> {
|
||||
self.policies(disposition).find_map(|(policy, report_uri_enabled)| {
|
||||
let mut violation = super::content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
std::slice::from_ref(policy),
|
||||
self.self_url.as_ref().unwrap_or(protected_url),
|
||||
request_url,
|
||||
kind,
|
||||
redirect_status,
|
||||
disposition,
|
||||
&self.reporting_endpoints,
|
||||
)?;
|
||||
violation.document_uri = super::csp_url_for_report(protected_url);
|
||||
violation.source_file = super::csp_url_for_report(protected_url);
|
||||
if !report_uri_enabled {
|
||||
violation.report_uri_endpoints.clear();
|
||||
}
|
||||
Some(violation)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::content_security_policy::{
|
||||
ContentSecurityPolicyRedirectStatus, ContentSecurityPolicyResourceKind,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn inherited_worker_policy_preserves_self_origin_delivery_and_report_url() {
|
||||
let source_url = Url::parse("https://example.test/creator/page.html").unwrap();
|
||||
let policy_text = "connect-src 'self'; report-uri ./report".to_owned();
|
||||
for scheme in ["blob:https://example.test/worker", "data:text/javascript,"] {
|
||||
let worker_url = Url::parse(scheme).unwrap();
|
||||
for disposition in [
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
] {
|
||||
for is_meta in [false, true] {
|
||||
if is_meta && disposition == ContentSecurityPolicyDisposition::Report {
|
||||
continue;
|
||||
}
|
||||
let mut policy = InheritedContentSecurityPolicy {
|
||||
self_url: Some(source_url.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
match (disposition, is_meta) {
|
||||
(ContentSecurityPolicyDisposition::Report, _) => {
|
||||
policy.report_only_policies.push(policy_text.clone())
|
||||
}
|
||||
(_, true) => policy.meta_policies.push(policy_text.clone()),
|
||||
(_, false) => policy.header_policies.push(policy_text.clone()),
|
||||
}
|
||||
let check = |request: &str| {
|
||||
policy.url_violation(
|
||||
&worker_url,
|
||||
&Url::parse(request).unwrap(),
|
||||
ContentSecurityPolicyResourceKind::WorkerConnect,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
disposition,
|
||||
)
|
||||
};
|
||||
assert!(check("https://example.test/allowed").is_none());
|
||||
let violation = check("https://cross.test/blocked").unwrap();
|
||||
assert_eq!(violation.document_uri, worker_url.scheme());
|
||||
assert_eq!(violation.source_file, worker_url.scheme());
|
||||
assert_eq!(violation.original_policy, policy_text);
|
||||
assert_eq!(violation.disposition, disposition);
|
||||
if is_meta {
|
||||
assert!(violation.report_uri_endpoints.is_empty());
|
||||
} else {
|
||||
assert_eq!(
|
||||
violation.report_uri_endpoints,
|
||||
vec!["https://example.test/creator/report"]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +331,17 @@ fn shared_worker_constructor_callback_inner<'s>(
|
||||
}
|
||||
};
|
||||
let message_port_registry = context.browser_context_runtime.message_port_registry();
|
||||
let script_load = if resolved_url.scheme() == "data" {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
// SAFETY: the constructor's Window realm owns this host for the call.
|
||||
let policy = unsafe { &*host_ptr }
|
||||
.local_worker_content_security_policy_source_for_global(scope, global)
|
||||
.map(|source| source.read().clone())
|
||||
.unwrap_or_default();
|
||||
script_load.with_ready_content_security_policy(policy)
|
||||
} else {
|
||||
script_load
|
||||
};
|
||||
let Some(message_port_realm) = MessagePortRealmBinding::current(scope) else {
|
||||
throw_type_error(
|
||||
scope,
|
||||
|
||||
@@ -278,6 +278,10 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
rv.set(worker.into());
|
||||
return;
|
||||
}
|
||||
// Capture the creator policy before loading the body so concurrent URL
|
||||
// revocation cannot leave a loaded script without its policy.
|
||||
let blob_policy = (resolved_url.scheme() == "blob")
|
||||
.then(|| crate::blob::object_url_content_security_policy(resolved_url.as_str()));
|
||||
let materialized = match materialize_worker_script_source(&resolved_url) {
|
||||
Ok(source) => source,
|
||||
Err(message) => {
|
||||
@@ -287,6 +291,13 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
};
|
||||
let creator_secure_context = moli_url::is_potentially_trustworthy_url(&base_url);
|
||||
let worker_id = if let Some(script_source) = materialized {
|
||||
let inherited_policy = if let Some(policy) = blob_policy {
|
||||
policy.unwrap_or_default()
|
||||
} else {
|
||||
host.local_worker_content_security_policy_source_for_owner(dispatch_scope)
|
||||
.map(|source| source.read().clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let request_url = worker_script_resource_url(&resolved_url);
|
||||
let network_response =
|
||||
local_worker_main_script_network_response(&resolved_url, &script_source);
|
||||
@@ -324,6 +335,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
creator_request_client.clone(),
|
||||
)
|
||||
.with_script_kind(worker_options.worker_type)
|
||||
.with_content_security_policy_snapshot(inherited_policy)
|
||||
.with_module_credentials_mode(worker_options.credentials_mode)
|
||||
.with_module_static_import_initiator_url(base_url.clone())
|
||||
.with_module_static_import_content_security_policies(document_content_security_policies)
|
||||
@@ -487,7 +499,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
);
|
||||
return;
|
||||
}
|
||||
let (script_url, script_source) =
|
||||
let (script_url, script_source, content_security_policy) =
|
||||
match materialize_nested_worker_script_source(&resolved_url, &nested_context) {
|
||||
Ok(source) => source,
|
||||
Err(message) => {
|
||||
@@ -523,6 +535,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
script_url,
|
||||
nested_context.loader.request_client().clone(),
|
||||
)
|
||||
.with_content_security_policy_snapshot(content_security_policy)
|
||||
.with_script_kind(worker_options.worker_type)
|
||||
.with_module_credentials_mode(worker_options.credentials_mode)
|
||||
.with_module_static_import_initiator_url(nested_context.base_url.clone())
|
||||
@@ -898,9 +911,23 @@ fn child_context_handle_from_global<'s>(
|
||||
fn materialize_nested_worker_script_source(
|
||||
script_url: &Url,
|
||||
context: &NestedWorkerContext,
|
||||
) -> Result<(String, String), String> {
|
||||
) -> Result<
|
||||
(
|
||||
String,
|
||||
String,
|
||||
crate::content_security_policy::InheritedContentSecurityPolicy,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let blob_policy = (script_url.scheme() == "blob")
|
||||
.then(|| crate::blob::object_url_content_security_policy(script_url.as_str()));
|
||||
if let Some(source) = materialize_worker_script_source(script_url)? {
|
||||
return Ok((script_url.to_string(), source));
|
||||
let policy = if let Some(policy) = blob_policy {
|
||||
policy.unwrap_or_default()
|
||||
} else {
|
||||
context.content_security_policy_snapshot.clone()
|
||||
};
|
||||
return Ok((script_url.to_string(), source, policy));
|
||||
}
|
||||
let loader = context.loader.clone();
|
||||
let resource_url = worker_script_resource_url(script_url);
|
||||
@@ -928,9 +955,25 @@ fn materialize_nested_worker_script_source(
|
||||
response.body_bytes(),
|
||||
)?;
|
||||
let (head, body) = response.into_text_parts();
|
||||
let policy = crate::content_security_policy::InheritedContentSecurityPolicy {
|
||||
self_url: Some(head.final_url.clone()),
|
||||
header_policies: crate::content_security_policy::content_security_policy_headers(
|
||||
&head.headers,
|
||||
),
|
||||
report_only_policies:
|
||||
crate::content_security_policy::content_security_policy_report_only_headers(
|
||||
&head.headers,
|
||||
),
|
||||
reporting_endpoints:
|
||||
crate::content_security_policy::content_security_policy_reporting_endpoints_from_headers(
|
||||
&head.headers,
|
||||
&head.final_url,
|
||||
),
|
||||
meta_policies: Vec::new(),
|
||||
};
|
||||
let mut final_url = head.final_url;
|
||||
final_url.set_fragment(script_url.fragment());
|
||||
Ok((final_url.to_string(), body))
|
||||
Ok((final_url.to_string(), body, policy))
|
||||
}
|
||||
|
||||
fn worker_script_inherits_parent_service_worker_controller(script_url: &Url) -> bool {
|
||||
|
||||
@@ -762,6 +762,14 @@ pub(super) struct DocumentRuntime {
|
||||
bypass_content_security_policy: bool,
|
||||
policy_container: DocumentPolicyContainer,
|
||||
delivered_meta_content_security_policies: RefCell<HashMap<DomHandle, Vec<String>>>,
|
||||
local_worker_policy_sources: RefCell<
|
||||
HashMap<
|
||||
DomHandle,
|
||||
std::sync::Weak<
|
||||
parking_lot::RwLock<crate::content_security_policy::InheritedContentSecurityPolicy>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
processed_meta_content_security_policy_handles: RefCell<HashSet<(DomHandle, DomHandle)>>,
|
||||
document_character_set: String,
|
||||
resource_loader_binding: Option<DocumentResourceLoaderBinding>,
|
||||
|
||||
@@ -295,6 +295,7 @@ impl DocumentRuntime {
|
||||
self.delivered_meta_content_security_policies
|
||||
.get_mut()
|
||||
.clear();
|
||||
self.local_worker_policy_sources.get_mut().clear();
|
||||
self.processed_meta_content_security_policy_handles
|
||||
.get_mut()
|
||||
.clear();
|
||||
|
||||
@@ -78,6 +78,7 @@ impl DocumentRuntime {
|
||||
bypass_content_security_policy: false,
|
||||
policy_container: DocumentPolicyContainer::default(),
|
||||
delivered_meta_content_security_policies: RefCell::new(HashMap::new()),
|
||||
local_worker_policy_sources: RefCell::new(HashMap::new()),
|
||||
processed_meta_content_security_policy_handles: RefCell::new(HashSet::new()),
|
||||
document_character_set: "UTF-8".to_owned(),
|
||||
resource_loader_binding: None,
|
||||
@@ -213,6 +214,7 @@ impl DocumentRuntime {
|
||||
bypass_content_security_policy: _,
|
||||
policy_container: _,
|
||||
delivered_meta_content_security_policies: _,
|
||||
local_worker_policy_sources: _,
|
||||
processed_meta_content_security_policy_handles: _,
|
||||
document_character_set: _,
|
||||
resource_loader_binding: _,
|
||||
|
||||
@@ -82,6 +82,7 @@ pub(crate) enum DocumentNavigationEmbeddingContext<'a> {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct DocumentConnectPolicySnapshot {
|
||||
policy_self_url: Option<Box<Url>>,
|
||||
enforce_policies: Vec<DocumentContentSecurityPolicyString>,
|
||||
report_only_policies: Vec<String>,
|
||||
reporting_endpoints: ContentSecurityPolicyReportingEndpoints,
|
||||
@@ -94,6 +95,7 @@ impl DocumentConnectPolicySnapshot {
|
||||
reporting_endpoints: ContentSecurityPolicyReportingEndpoints,
|
||||
) -> Self {
|
||||
Self {
|
||||
policy_self_url: None,
|
||||
enforce_policies: document_response_content_security_policy_strings(
|
||||
&enforce_policies,
|
||||
&reporting_endpoints,
|
||||
@@ -107,28 +109,61 @@ impl DocumentConnectPolicySnapshot {
|
||||
!self.enforce_policies.is_empty() || !self.report_only_policies.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn from_inherited_policy(
|
||||
policy: &crate::content_security_policy::InheritedContentSecurityPolicy,
|
||||
) -> Self {
|
||||
let mut snapshot = Self::from_policies(
|
||||
policy.header_policies.clone(),
|
||||
policy.report_only_policies.clone(),
|
||||
policy.reporting_endpoints.clone(),
|
||||
);
|
||||
snapshot.policy_self_url = policy.self_url.clone().map(Box::new);
|
||||
snapshot
|
||||
.enforce_policies
|
||||
.extend(policy.meta_policies.iter().map(|policy_text| {
|
||||
DocumentContentSecurityPolicyString {
|
||||
policy: policy_text.clone(),
|
||||
report_uri_enabled: false,
|
||||
reporting_endpoints: policy.reporting_endpoints.clone(),
|
||||
}
|
||||
}));
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub(crate) fn check_redirect(
|
||||
&self,
|
||||
document_url: &Url,
|
||||
request_url: &Url,
|
||||
) -> DocumentContentSecurityPolicyCheck {
|
||||
DocumentContentSecurityPolicyCheck {
|
||||
let policy_url = self.policy_self_url.as_deref().unwrap_or(document_url);
|
||||
let mut result = DocumentContentSecurityPolicyCheck {
|
||||
report_only_violations: document_connect_policy_violations(
|
||||
&self.report_only_policies,
|
||||
&self.reporting_endpoints,
|
||||
document_url,
|
||||
policy_url,
|
||||
request_url,
|
||||
ContentSecurityPolicyRedirectStatus::FollowedRedirect,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
),
|
||||
enforced_violations: document_connect_policy_violations_from_document_policies(
|
||||
self.enforce_policies.clone(),
|
||||
document_url,
|
||||
policy_url,
|
||||
request_url,
|
||||
ContentSecurityPolicyRedirectStatus::FollowedRedirect,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
),
|
||||
};
|
||||
for violation in result
|
||||
.report_only_violations
|
||||
.iter_mut()
|
||||
.chain(&mut result.enforced_violations)
|
||||
{
|
||||
violation.document_uri =
|
||||
crate::content_security_policy::csp_url_for_report(document_url);
|
||||
violation.source_file =
|
||||
crate::content_security_policy::csp_url_for_report(document_url);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -150,12 +185,19 @@ impl DocumentConnectPolicySnapshot {
|
||||
) -> Option<DocumentContentSecurityPolicyViolation> {
|
||||
document_url_policy_violation_from_document_policies(
|
||||
self.enforce_policies.clone(),
|
||||
document_url,
|
||||
self.policy_self_url.as_deref().unwrap_or(document_url),
|
||||
request_url,
|
||||
ContentSecurityPolicyResourceKind::DocumentConnect,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
)
|
||||
.map(|mut violation| {
|
||||
violation.document_uri =
|
||||
crate::content_security_policy::csp_url_for_report(document_url);
|
||||
violation.source_file =
|
||||
crate::content_security_policy::csp_url_for_report(document_url);
|
||||
violation
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn report_only_violation(
|
||||
@@ -167,11 +209,18 @@ impl DocumentConnectPolicySnapshot {
|
||||
document_connect_policy_violation(
|
||||
&self.report_only_policies,
|
||||
&self.reporting_endpoints,
|
||||
document_url,
|
||||
self.policy_self_url.as_deref().unwrap_or(document_url),
|
||||
request_url,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
)
|
||||
.map(|mut violation| {
|
||||
violation.document_uri =
|
||||
crate::content_security_policy::csp_url_for_report(document_url);
|
||||
violation.source_file =
|
||||
crate::content_security_policy::csp_url_for_report(document_url);
|
||||
violation
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,6 +948,10 @@ impl DocumentRuntime {
|
||||
);
|
||||
}
|
||||
DocumentConnectPolicySnapshot {
|
||||
policy_self_url: policy
|
||||
.content_security_policy_self_url
|
||||
.clone()
|
||||
.map(Box::new),
|
||||
enforce_policies: self.document_content_security_policy_strings_for_optional_document(
|
||||
document_handle,
|
||||
&policy.response_content_security_policies,
|
||||
@@ -911,6 +964,50 @@ impl DocumentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn local_worker_content_security_policy_source(
|
||||
&self,
|
||||
document: Option<DomHandle>,
|
||||
document_url: &Url,
|
||||
policy: &DocumentPolicyContainer,
|
||||
) -> crate::content_security_policy::ContentSecurityPolicySource {
|
||||
use crate::content_security_policy::InheritedContentSecurityPolicy;
|
||||
let snapshot = if self.bypass_content_security_policy() {
|
||||
InheritedContentSecurityPolicy::default()
|
||||
} else {
|
||||
InheritedContentSecurityPolicy {
|
||||
self_url: Some(
|
||||
policy
|
||||
.content_security_policy_self_url
|
||||
.as_ref()
|
||||
.unwrap_or(document_url)
|
||||
.clone(),
|
||||
),
|
||||
header_policies: policy.response_content_security_policies.clone(),
|
||||
meta_policies: document
|
||||
.map(|handle| self.meta_content_security_policy_strings_for_document(handle))
|
||||
.unwrap_or_default(),
|
||||
report_only_policies: policy
|
||||
.response_content_security_report_only_policies
|
||||
.clone(),
|
||||
reporting_endpoints: policy.content_security_reporting_endpoints.clone(),
|
||||
}
|
||||
};
|
||||
let mut sources = self.local_worker_policy_sources.borrow_mut();
|
||||
if let Some(source) = document
|
||||
.and_then(|handle| sources.get(&handle))
|
||||
.and_then(std::sync::Weak::upgrade)
|
||||
{
|
||||
*source.write() = snapshot;
|
||||
return source;
|
||||
}
|
||||
sources.retain(|_, source| source.strong_count() != 0);
|
||||
let source = std::sync::Arc::new(parking_lot::RwLock::new(snapshot));
|
||||
if let Some(document) = document {
|
||||
sources.insert(document, std::sync::Arc::downgrade(&source));
|
||||
}
|
||||
source
|
||||
}
|
||||
|
||||
pub(crate) fn document_subresource_csp_check(
|
||||
&self,
|
||||
request_url: &Url,
|
||||
@@ -1987,6 +2084,14 @@ impl DocumentRuntime {
|
||||
return;
|
||||
}
|
||||
if let Some(policy) = policy {
|
||||
if let Some(source) = self
|
||||
.local_worker_policy_sources
|
||||
.borrow()
|
||||
.get(&document_handle)
|
||||
.and_then(std::sync::Weak::upgrade)
|
||||
{
|
||||
source.write().meta_policies.push(policy.clone());
|
||||
}
|
||||
self.delivered_meta_content_security_policies
|
||||
.borrow_mut()
|
||||
.entry(document_handle)
|
||||
|
||||
@@ -162,6 +162,31 @@ impl JsContextHost {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn local_worker_content_security_policy_source_for_owner(
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
) -> Option<crate::content_security_policy::ContentSecurityPolicySource> {
|
||||
let snapshot = self.owner_document_policy_snapshot(owner)?;
|
||||
// SAFETY: this host and its DocumentRuntime belong to the same ScriptVm.
|
||||
Some(
|
||||
unsafe { &*self.runtime }.local_worker_content_security_policy_source(
|
||||
snapshot.document_handle,
|
||||
&snapshot.document_url,
|
||||
&snapshot.policy_container,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn local_worker_content_security_policy_source_for_global<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
global: v8::Local<'s, v8::Object>,
|
||||
) -> Option<crate::content_security_policy::ContentSecurityPolicySource> {
|
||||
self.local_worker_content_security_policy_source_for_owner(
|
||||
policy_owner_dispatch_scope_for_global(scope, global),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn document_permissions_policy_for_owner(
|
||||
&self,
|
||||
owner: OwnerDispatchScope,
|
||||
|
||||
@@ -1924,6 +1924,88 @@ async fn drive_window_message_until(
|
||||
anyhow::bail!("{context}; diagnostics={diagnostics}; progress_sources={progress_sources:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_worker_content_security_policy_preserves_self_and_blocks_cross_origin() {
|
||||
run_page_vm_async_test(async move {
|
||||
for kind in ["dedicated", "shared", "nested"] {
|
||||
let shared = kind == "shared";
|
||||
for blob in [false, true] {
|
||||
for meta in [false, true] {
|
||||
let (base_url, same_server) = spawn_path_response_http_server(vec![(
|
||||
"/allowed", "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *", "ok".to_owned(), Duration::ZERO,
|
||||
)]).await;
|
||||
let (cross_url, mut cross_request, cross_server) = spawn_header_capture_http_server().await;
|
||||
let mut page_vm = test_page_vm_with_document_url(Url::parse(&format!("{base_url}/page.html")).unwrap());
|
||||
if !meta {
|
||||
page_vm.vm_mut().set_response_content_security_policies(&["connect-src 'self'".to_owned()]);
|
||||
}
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
let result = local_executor.run(async move {
|
||||
let source = format!(r#"
|
||||
const events = [];
|
||||
addEventListener('securitypolicyviolation', e => events.push([e.effectiveDirective, e.disposition, e.documentURI]));
|
||||
async function run(send) {{
|
||||
const allowed = await fetch({same}).then(r => r.text()).catch(e => 'fetch-error:' + e.name);
|
||||
let blocked = 'allowed';
|
||||
try {{ await fetch({cross}); }} catch (e) {{ blocked = e.name; }}
|
||||
setTimeout(() => send({{allowed, blocked, events}}), 50);
|
||||
}}
|
||||
{start}
|
||||
"#,
|
||||
same = serde_json::to_string(&format!("{base_url}/allowed")).unwrap(),
|
||||
cross = serde_json::to_string(&format!("{cross_url}/blocked")).unwrap(),
|
||||
start = if shared { "onconnect = e => run(value => e.ports[0].postMessage(value));" } else { "run(value => postMessage(value));" },
|
||||
);
|
||||
let source = if kind == "nested" {
|
||||
format!(r#"
|
||||
const source = {source};
|
||||
const url = {url};
|
||||
globalThis.child = new Worker(url);
|
||||
child.onmessage = e => postMessage(e.data);
|
||||
child.onerror = e => postMessage({{error: e.message}});
|
||||
"#,
|
||||
source = serde_json::to_string(&source).unwrap(),
|
||||
url = if blob { "URL.createObjectURL(new Blob([source], {type: 'text/javascript'}))" } else { "'data:text/javascript,' + encodeURIComponent(source)" },
|
||||
)
|
||||
} else { source };
|
||||
page_vm.vm_mut().eval(&format!(r#"
|
||||
globalThis.__localWorkerResult = null;
|
||||
const source = {source};
|
||||
const url = {url};
|
||||
{meta}
|
||||
globalThis.__localWorker = new {constructor}(url);
|
||||
{port}.onmessage = e => globalThis.__localWorkerResult = e.data;
|
||||
__localWorker.onerror = e => globalThis.__localWorkerResult = {{error: e.message}};
|
||||
{start}
|
||||
"#,
|
||||
source = serde_json::to_string(&source).unwrap(),
|
||||
url = if blob { "URL.createObjectURL(new Blob([source], {type: 'text/javascript'}))" } else { "'data:text/javascript,' + encodeURIComponent(source)" },
|
||||
meta = if meta { "const meta = document.createElement('meta'); meta.httpEquiv = 'Content-Security-Policy'; meta.content = \"connect-src 'self'\"; document.head.append(meta);" } else { "" },
|
||||
constructor = if shared { "SharedWorker" } else { "Worker" },
|
||||
port = if shared { "__localWorker.port" } else { "__localWorker" },
|
||||
start = if shared { "__localWorker.port.start();" } else { "" },
|
||||
))?;
|
||||
let done = "String(globalThis.__localWorkerResult !== null)";
|
||||
if shared {
|
||||
drive_shared_worker_until_done(&mut page_vm, done, "local SharedWorker CSP result").await?;
|
||||
} else {
|
||||
drive_websocket_until_done(&mut page_vm, done, "local Worker CSP result").await?;
|
||||
}
|
||||
let result = page_vm.vm_mut().eval("JSON.stringify(__localWorkerResult)")?;
|
||||
page_vm.vm_mut().eval(if shared { "__localWorker.port.close()" } else { "__localWorker.terminate()" })?;
|
||||
anyhow::Ok(result)
|
||||
}).await;
|
||||
same_server.abort();
|
||||
cross_server.abort();
|
||||
let result: serde_json::Value = serde_json::from_str(&result.expect("local worker CSP test should finish")).unwrap();
|
||||
assert_eq!(result, serde_json::json!({"allowed": "ok", "blocked": "TypeError", "events": [["connect-src", "enforce", if blob { "blob" } else { "data" }]]}), "kind={kind}, blob={blob}, meta={meta}");
|
||||
assert!(cross_request.try_recv().is_err(), "CSP must prevent contacting the cross-origin target");
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_post_message_flows_through_page_client_event_source() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
@@ -60,6 +60,11 @@ impl RendererSharedWorkerHost {
|
||||
.with_creator_storage_key(params.key.storage_key().clone())
|
||||
.with_indexed_db_manager(execution_policy.indexed_db_manager)
|
||||
.with_storage_bucket_store(execution_policy.storage_bucket_store);
|
||||
let options = if let Some(policy) = script.content_security_policy_snapshot {
|
||||
options.with_content_security_policy_snapshot(*policy)
|
||||
} else {
|
||||
options
|
||||
};
|
||||
let options = if let Some(runtime) = execution_policy.service_worker_runtime {
|
||||
options.with_service_worker_runtime(runtime)
|
||||
} else {
|
||||
|
||||
@@ -35,6 +35,8 @@ pub(crate) struct SharedWorkerScriptLoad {
|
||||
pub(super) struct SharedWorkerLoadedScript {
|
||||
pub(super) script_url: String,
|
||||
pub(super) source: String,
|
||||
pub(super) content_security_policy_snapshot:
|
||||
Option<Box<crate::content_security_policy::InheritedContentSecurityPolicy>>,
|
||||
pub(super) response_referrer_policy: Option<String>,
|
||||
pub(super) response_policy_context: Option<SubresourcePolicyContext>,
|
||||
pub(super) response_content_security_policies: Vec<String>,
|
||||
@@ -303,6 +305,7 @@ impl SharedWorkerLoadedScript {
|
||||
Self {
|
||||
script_url,
|
||||
source,
|
||||
content_security_policy_snapshot: None,
|
||||
response_referrer_policy: None,
|
||||
response_policy_context: None,
|
||||
response_content_security_policies: Vec::new(),
|
||||
@@ -356,6 +359,16 @@ impl SharedWorkerLoadedScript {
|
||||
}
|
||||
|
||||
impl SharedWorkerScriptLoad {
|
||||
pub(crate) fn with_ready_content_security_policy(
|
||||
mut self,
|
||||
policy: crate::content_security_policy::InheritedContentSecurityPolicy,
|
||||
) -> Self {
|
||||
if let SharedWorkerScriptLoadKind::Ready(script) = &mut self.kind {
|
||||
script.content_security_policy_snapshot = Some(Box::new(policy));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn ready(script_url: String, script_source: String) -> Self {
|
||||
Self {
|
||||
kind: SharedWorkerScriptLoadKind::Ready(SharedWorkerLoadedScript::new(
|
||||
@@ -595,8 +608,14 @@ pub(super) fn load_shared_worker_blob_script_source(
|
||||
) -> Result<SharedWorkerLoadedScript, String> {
|
||||
let mut resource_url = script_url.clone();
|
||||
resource_url.set_fragment(None);
|
||||
let policy =
|
||||
crate::blob::object_url_content_security_policy(resource_url.as_str()).unwrap_or_default();
|
||||
crate::blob::object_url_body_and_type(resource_url.as_str())
|
||||
.map(|(body, _)| SharedWorkerLoadedScript::new(script_url.to_string(), body))
|
||||
.map(|(body, _)| {
|
||||
let mut script = SharedWorkerLoadedScript::new(script_url.to_string(), body);
|
||||
script.content_security_policy_snapshot = Some(Box::new(policy));
|
||||
script
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
format!("Failed to load shared worker script `{script_url}`: blob URL is unavailable.")
|
||||
})
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::content_security_policy::{
|
||||
content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints,
|
||||
content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints,
|
||||
content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints,
|
||||
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints,
|
||||
create_security_policy_violation_event, current_script_violation_location,
|
||||
send_content_security_policy_reports,
|
||||
};
|
||||
@@ -39,6 +38,70 @@ use super::{
|
||||
record_worker_subresource_failure_with_handle, request_body_text,
|
||||
};
|
||||
|
||||
pub(super) fn worker_policy_snapshot(
|
||||
state: &WorkerGlobalState,
|
||||
) -> crate::content_security_policy::InheritedContentSecurityPolicy {
|
||||
state
|
||||
.content_security_policy_snapshot
|
||||
.clone()
|
||||
.unwrap_or_else(
|
||||
|| crate::content_security_policy::InheritedContentSecurityPolicy {
|
||||
self_url: state.current_script_url.clone(),
|
||||
header_policies: state.content_security_policies.clone(),
|
||||
meta_policies: Vec::new(),
|
||||
report_only_policies: state.content_security_report_only_policies.clone(),
|
||||
reporting_endpoints: state.content_security_reporting_endpoints.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn worker_policy_url<'a>(state: &'a WorkerGlobalState, protected_url: &'a Url) -> &'a Url {
|
||||
state
|
||||
.content_security_policy_snapshot
|
||||
.as_ref()
|
||||
.and_then(|policy| policy.self_url.as_ref())
|
||||
.unwrap_or(protected_url)
|
||||
}
|
||||
|
||||
fn worker_policy_violation(
|
||||
protected_url: &Url,
|
||||
report_uri_enabled: bool,
|
||||
mut violation: ContentSecurityPolicyUrlViolation,
|
||||
) -> ContentSecurityPolicyUrlViolation {
|
||||
violation.document_uri = crate::content_security_policy::csp_url_for_report(protected_url);
|
||||
violation.source_file = crate::content_security_policy::csp_url_for_report(protected_url);
|
||||
if !report_uri_enabled {
|
||||
violation.report_uri_endpoints.clear();
|
||||
}
|
||||
violation
|
||||
}
|
||||
|
||||
fn worker_policies(
|
||||
state: &WorkerGlobalState,
|
||||
disposition: ContentSecurityPolicyDisposition,
|
||||
) -> impl Iterator<Item = (&String, bool)> {
|
||||
let (headers, meta) = match (state.content_security_policy_snapshot.as_ref(), disposition) {
|
||||
(Some(policy), ContentSecurityPolicyDisposition::Enforce) => (
|
||||
policy.header_policies.as_slice(),
|
||||
policy.meta_policies.as_slice(),
|
||||
),
|
||||
(Some(policy), ContentSecurityPolicyDisposition::Report) => {
|
||||
(policy.report_only_policies.as_slice(), &[][..])
|
||||
}
|
||||
(None, ContentSecurityPolicyDisposition::Enforce) => {
|
||||
(state.content_security_policies.as_slice(), &[][..])
|
||||
}
|
||||
(None, ContentSecurityPolicyDisposition::Report) => (
|
||||
state.content_security_report_only_policies.as_slice(),
|
||||
&[][..],
|
||||
),
|
||||
};
|
||||
headers
|
||||
.iter()
|
||||
.map(|policy| (policy, true))
|
||||
.chain(meta.iter().map(|policy| (policy, false)))
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_worker_content_security_policy_violation_event<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
request_client: &crate::network::context::WorkerResourceLoader,
|
||||
@@ -99,15 +162,15 @@ pub(super) fn dispatch_worker_trusted_types_sink_violation_event_for_state<'s>(
|
||||
return;
|
||||
};
|
||||
trusted_types_policies(&state_ref)
|
||||
.filter_map(|(policy, disposition)| {
|
||||
.filter_map(|(policy, disposition, report_uri_enabled)| {
|
||||
content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints(
|
||||
policy,
|
||||
protected_url,
|
||||
worker_policy_url(&state_ref, protected_url),
|
||||
sink,
|
||||
sample,
|
||||
disposition,
|
||||
&state_ref.content_security_reporting_endpoints,
|
||||
)
|
||||
).map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
@@ -126,15 +189,15 @@ pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>(
|
||||
return true;
|
||||
};
|
||||
trusted_types_policies(&state_ref)
|
||||
.filter_map(|(policy, disposition)| {
|
||||
.filter_map(|(policy, disposition, report_uri_enabled)| {
|
||||
content_security_policy_trusted_types_policy_violation_with_disposition_and_reporting_endpoints(
|
||||
policy,
|
||||
protected_url,
|
||||
worker_policy_url(&state_ref, protected_url),
|
||||
policy_name,
|
||||
is_duplicate,
|
||||
disposition,
|
||||
&state_ref.content_security_reporting_endpoints,
|
||||
)
|
||||
).map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
@@ -147,24 +210,16 @@ pub(super) fn allows_worker_trusted_type_policy_name_for_state<'s>(
|
||||
|
||||
fn trusted_types_policies(
|
||||
state: &WorkerGlobalState,
|
||||
) -> impl Iterator<Item = (&str, ContentSecurityPolicyDisposition)> {
|
||||
// Preserve the same partition ordering as document reporting. Identical
|
||||
// policies remain distinct entries and each can produce a violation event.
|
||||
) -> impl Iterator<Item = (&str, ContentSecurityPolicyDisposition, bool)> {
|
||||
[
|
||||
(
|
||||
&state.content_security_policies,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
),
|
||||
(
|
||||
&state.content_security_report_only_policies,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
),
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(policies, disposition)| {
|
||||
policies
|
||||
.iter()
|
||||
.map(move |policy| (policy.as_str(), disposition))
|
||||
.flat_map(move |disposition| {
|
||||
worker_policies(state, disposition).map(move |(policy, report_uri_enabled)| {
|
||||
(policy.as_str(), disposition, report_uri_enabled)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -889,24 +944,44 @@ pub(super) fn worker_eval_content_security_policy_violation(
|
||||
source: Option<&str>,
|
||||
disposition: ContentSecurityPolicyDisposition,
|
||||
) -> Option<ContentSecurityPolicyUrlViolation> {
|
||||
let policies = match disposition {
|
||||
ContentSecurityPolicyDisposition::Enforce => &state.content_security_policies,
|
||||
ContentSecurityPolicyDisposition::Report => &state.content_security_report_only_policies,
|
||||
};
|
||||
let kind = if allow_trusted_types_eval {
|
||||
ContentSecurityPolicyNonUrlKind::TrustedTypesEval
|
||||
} else {
|
||||
ContentSecurityPolicyNonUrlKind::Eval
|
||||
};
|
||||
policies.iter().find_map(|policy| {
|
||||
worker_policies(state, disposition).find_map(|(policy, report_uri_enabled)| {
|
||||
content_security_policy_non_url_violation_with_source(
|
||||
policy,
|
||||
protected_url,
|
||||
worker_policy_url(state, protected_url),
|
||||
kind,
|
||||
source,
|
||||
disposition,
|
||||
&state.content_security_reporting_endpoints,
|
||||
)
|
||||
.map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation))
|
||||
})
|
||||
}
|
||||
|
||||
fn worker_url_policy_violation(
|
||||
state: &WorkerGlobalState,
|
||||
protected_url: &Url,
|
||||
checked_url: &Url,
|
||||
blocked_url: &Url,
|
||||
kind: ContentSecurityPolicyResourceKind,
|
||||
redirect_status: ContentSecurityPolicyRedirectStatus,
|
||||
disposition: ContentSecurityPolicyDisposition,
|
||||
) -> Option<ContentSecurityPolicyUrlViolation> {
|
||||
worker_policies(state, disposition).find_map(|(policy, report_uri_enabled)| {
|
||||
content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
std::slice::from_ref(policy),
|
||||
worker_policy_url(state, protected_url),
|
||||
checked_url,
|
||||
blocked_url,
|
||||
kind,
|
||||
redirect_status,
|
||||
disposition,
|
||||
&state.content_security_reporting_endpoints,
|
||||
).map(|violation| worker_policy_violation(protected_url, report_uri_enabled, violation))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -917,14 +992,14 @@ pub(super) fn worker_content_security_policy_violation_with_redirect_status(
|
||||
kind: ContentSecurityPolicyResourceKind,
|
||||
redirect_status: ContentSecurityPolicyRedirectStatus,
|
||||
) -> Option<ContentSecurityPolicyUrlViolation> {
|
||||
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&state.content_security_policies,
|
||||
worker_url_policy_violation(
|
||||
state,
|
||||
protected_url,
|
||||
request_url,
|
||||
request_url,
|
||||
kind,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
&state.content_security_reporting_endpoints,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -936,15 +1011,14 @@ pub(super) fn worker_content_security_policy_violation_for_checked_url_with_redi
|
||||
kind: ContentSecurityPolicyResourceKind,
|
||||
redirect_status: ContentSecurityPolicyRedirectStatus,
|
||||
) -> Option<ContentSecurityPolicyUrlViolation> {
|
||||
content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&state.content_security_policies,
|
||||
worker_url_policy_violation(
|
||||
state,
|
||||
protected_url,
|
||||
checked_url,
|
||||
blocked_url,
|
||||
kind,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
&state.content_security_reporting_endpoints,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -970,14 +1044,14 @@ pub(super) fn worker_content_security_policy_report_only_violation_with_redirect
|
||||
kind: ContentSecurityPolicyResourceKind,
|
||||
redirect_status: ContentSecurityPolicyRedirectStatus,
|
||||
) -> Option<ContentSecurityPolicyUrlViolation> {
|
||||
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&state.content_security_report_only_policies,
|
||||
worker_url_policy_violation(
|
||||
state,
|
||||
protected_url,
|
||||
request_url,
|
||||
request_url,
|
||||
kind,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
&state.content_security_reporting_endpoints,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -989,15 +1063,14 @@ pub(super) fn worker_content_security_policy_report_only_violation_for_checked_u
|
||||
kind: ContentSecurityPolicyResourceKind,
|
||||
redirect_status: ContentSecurityPolicyRedirectStatus,
|
||||
) -> Option<ContentSecurityPolicyUrlViolation> {
|
||||
content_security_policy_url_violation_for_checked_url_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&state.content_security_report_only_policies,
|
||||
worker_url_policy_violation(
|
||||
state,
|
||||
protected_url,
|
||||
checked_url,
|
||||
blocked_url,
|
||||
kind,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
&state.content_security_reporting_endpoints,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2330,10 +2330,8 @@ pub(in crate::worker) fn worker_fetch_callback<'s>(
|
||||
|
||||
let connect_policy = {
|
||||
let state = state.borrow();
|
||||
crate::document_runtime::DocumentConnectPolicySnapshot::from_policies(
|
||||
state.content_security_policies.clone(),
|
||||
state.content_security_report_only_policies.clone(),
|
||||
state.content_security_reporting_endpoints.clone(),
|
||||
crate::document_runtime::DocumentConnectPolicySnapshot::from_inherited_policy(
|
||||
&super::content_security_policy::worker_policy_snapshot(&state),
|
||||
)
|
||||
};
|
||||
// Local URLs are resolved by the worker fetch task without interception.
|
||||
|
||||
@@ -1558,6 +1558,8 @@ pub(crate) struct WorkerGlobalState {
|
||||
pub(super) module_static_import_content_security_policies: Vec<String>,
|
||||
/// Enforce CSP policies parsed from the top-level worker script response.
|
||||
pub(super) content_security_policies: Vec<String>,
|
||||
pub(super) content_security_policy_snapshot:
|
||||
Option<crate::content_security_policy::InheritedContentSecurityPolicy>,
|
||||
/// Report-only CSP policies parsed from the top-level worker script response.
|
||||
pub(super) content_security_report_only_policies: Vec<String>,
|
||||
/// Reporting API endpoints parsed from the top-level worker script response.
|
||||
@@ -2860,6 +2862,8 @@ pub(crate) struct NestedWorkerContext {
|
||||
pub(crate) indexed_db_manager: Option<crate::context_bootstrap::WeakIndexedDbManager>,
|
||||
pub(crate) storage_bucket_store: Option<crate::context_bootstrap::SharedStorageBucketStore>,
|
||||
pub(crate) module_static_import_content_security_policies: Vec<String>,
|
||||
pub(crate) content_security_policy_snapshot:
|
||||
crate::content_security_policy::InheritedContentSecurityPolicy,
|
||||
pub(crate) require_trusted_types_for_script: bool,
|
||||
pub(crate) network_policy: super::handle::WorkerNetworkPolicy,
|
||||
pub(crate) policy_context: crate::types::SubresourcePolicyContext,
|
||||
@@ -2893,6 +2897,7 @@ pub(crate) fn reserve_nested_worker_context(
|
||||
indexed_db_manager: state.indexed_db_manager.clone(),
|
||||
storage_bucket_store: state.storage_bucket_store.clone(),
|
||||
module_static_import_content_security_policies: state.content_security_policies.clone(),
|
||||
content_security_policy_snapshot: content_security_policy::worker_policy_snapshot(&state),
|
||||
require_trusted_types_for_script:
|
||||
crate::content_security_policy::content_security_policy_requires_trusted_types_for_script(
|
||||
&state.content_security_policies,
|
||||
@@ -6350,6 +6355,22 @@ pub(crate) fn worker_storage_partition_identity(
|
||||
)
|
||||
}
|
||||
|
||||
impl WorkerGlobalState {
|
||||
pub(in crate::worker) fn content_security_policy_snapshot_for_inheritance(
|
||||
&self,
|
||||
) -> crate::content_security_policy::InheritedContentSecurityPolicy {
|
||||
content_security_policy::worker_policy_snapshot(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn worker_content_security_policy_snapshot(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> Option<crate::content_security_policy::InheritedContentSecurityPolicy> {
|
||||
Some(content_security_policy::worker_policy_snapshot(
|
||||
&get_worker_state(scope)?.borrow(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn worker_current_script_url(scope: &mut v8::PinScope<'_, '_>) -> Option<Url> {
|
||||
get_worker_state(scope)?.borrow().current_script_url.clone()
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(crate) use global_scope::{
|
||||
worker_allows_trusted_type_policy_name_by_csp, worker_allows_trusted_types_eval,
|
||||
worker_broadcast_channel_registry, worker_broadcast_channel_storage_key,
|
||||
worker_broadcast_channel_wake_sender, worker_broadcast_channel_wrapper,
|
||||
worker_current_script_url, worker_global_is_closed,
|
||||
worker_content_security_policy_snapshot, worker_current_script_url, worker_global_is_closed,
|
||||
worker_message_port_event_listener_snapshots, worker_message_port_registry,
|
||||
worker_message_port_wake_sender, worker_message_port_wrapper,
|
||||
worker_notification_permission_state, worker_opfs_directory_iterator_registry,
|
||||
|
||||
@@ -21,7 +21,6 @@ use crate::broadcast_channel_runtime::{
|
||||
use crate::content_security_policy::{
|
||||
ContentSecurityPolicyDisposition, ContentSecurityPolicyRedirectStatus,
|
||||
ContentSecurityPolicyReportingEndpoints, ContentSecurityPolicyUrlViolation,
|
||||
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints,
|
||||
};
|
||||
use crate::context_bootstrap::flush_one_pending_file_reader;
|
||||
use crate::exception_reporting::{V8ExceptionReport, build_event_handler_exception_report};
|
||||
@@ -192,6 +191,8 @@ pub(crate) struct WorkerSpawnOptions {
|
||||
pub(crate) content_security_policies: Vec<String>,
|
||||
pub(crate) content_security_report_only_policies: Vec<String>,
|
||||
pub(crate) content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints,
|
||||
pub(crate) content_security_policy_snapshot:
|
||||
Option<crate::content_security_policy::InheritedContentSecurityPolicy>,
|
||||
pub(crate) network_policy: WorkerNetworkPolicy,
|
||||
pub(crate) policy_context: crate::types::SubresourcePolicyContext,
|
||||
pub(crate) worker_context_runtime: RendererWorkerContextRuntime,
|
||||
@@ -319,6 +320,7 @@ impl WorkerSpawnOptions {
|
||||
module_credentials_mode: RequestCredentialsMode::SameOrigin,
|
||||
referrer_policy: None,
|
||||
module_static_import_content_security_policies: Vec::new(),
|
||||
content_security_policy_snapshot: None,
|
||||
content_security_policies: Vec::new(),
|
||||
content_security_report_only_policies: Vec::new(),
|
||||
content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints::default(
|
||||
@@ -408,6 +410,17 @@ impl WorkerSpawnOptions {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_content_security_policy_snapshot(
|
||||
mut self,
|
||||
policy: crate::content_security_policy::InheritedContentSecurityPolicy,
|
||||
) -> Self {
|
||||
self.content_security_policies = policy.enforced_strings();
|
||||
self.content_security_report_only_policies = policy.report_only_policies.clone();
|
||||
self.content_security_reporting_endpoints = policy.reporting_endpoints.clone();
|
||||
self.content_security_policy_snapshot = Some(policy);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_content_security_reporting_endpoints(
|
||||
mut self,
|
||||
endpoints: ContentSecurityPolicyReportingEndpoints,
|
||||
@@ -590,50 +603,37 @@ fn start_worker_module_graph_fetch(
|
||||
loader: WorkerResourceLoader,
|
||||
network_partition_key: Option<String>,
|
||||
module_static_import_content_security_policies: Vec<String>,
|
||||
worker_global_content_security_policies: Vec<String>,
|
||||
worker_global_content_security_report_only_policies: Vec<String>,
|
||||
worker_global_content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints,
|
||||
worker_global_policy: crate::content_security_policy::InheritedContentSecurityPolicy,
|
||||
completion_tx: mpsc::UnboundedSender<WorkerModuleGraphFetchCompletion>,
|
||||
) {
|
||||
let fetch_id = request.fetch_id();
|
||||
let (
|
||||
content_security_policies,
|
||||
content_security_report_only_policies,
|
||||
content_security_reporting_endpoints,
|
||||
resource_kind,
|
||||
) =
|
||||
match request.csp_source() {
|
||||
let (policy, resource_kind) = match request.csp_source() {
|
||||
WorkerModuleGraphFetchCspSource::StaticModuleGraph => (
|
||||
module_static_import_content_security_policies,
|
||||
Vec::new(),
|
||||
ContentSecurityPolicyReportingEndpoints::default(),
|
||||
crate::content_security_policy::InheritedContentSecurityPolicy {
|
||||
self_url: Some(request.initiator_url().clone()),
|
||||
header_policies: module_static_import_content_security_policies,
|
||||
..Default::default()
|
||||
},
|
||||
crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerStaticModuleImport,
|
||||
),
|
||||
WorkerModuleGraphFetchCspSource::DynamicImportGraph => (
|
||||
worker_global_content_security_policies,
|
||||
worker_global_content_security_report_only_policies,
|
||||
worker_global_content_security_reporting_endpoints,
|
||||
worker_global_policy,
|
||||
crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport,
|
||||
),
|
||||
};
|
||||
let initial_csp_report_only_violation =
|
||||
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&content_security_report_only_policies,
|
||||
request.initiator_url(),
|
||||
request.url(),
|
||||
resource_kind,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
&content_security_reporting_endpoints,
|
||||
);
|
||||
if let Some(violation) = content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&content_security_policies,
|
||||
let initial_csp_report_only_violation = policy.url_violation(
|
||||
request.initiator_url(),
|
||||
request.url(),
|
||||
resource_kind,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
);
|
||||
if let Some(violation) = policy.url_violation(
|
||||
request.initiator_url(),
|
||||
request.url(),
|
||||
resource_kind,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
&content_security_reporting_endpoints,
|
||||
) {
|
||||
let message = module_graph_csp_violation_message(&violation);
|
||||
let mut completion = WorkerModuleGraphFetchCompletion::new(fetch_id, Err(message))
|
||||
@@ -675,11 +675,7 @@ fn start_worker_module_graph_fetch(
|
||||
let requested_module_type = request.module_type().map(str::to_owned);
|
||||
let requested_kind = request.kind();
|
||||
let request_credentials_mode = request.credentials_mode();
|
||||
let response_content_security_policies = content_security_policies.clone();
|
||||
let response_content_security_report_only_policies =
|
||||
content_security_report_only_policies.clone();
|
||||
let response_content_security_reporting_endpoints =
|
||||
content_security_reporting_endpoints.clone();
|
||||
let response_policy = policy.clone();
|
||||
let response_resource_kind = resource_kind;
|
||||
let completion_tx_for_callback = completion_tx.clone();
|
||||
let response_started_at = Instant::now();
|
||||
@@ -705,25 +701,20 @@ fn start_worker_module_graph_fetch(
|
||||
if redirect_status == ContentSecurityPolicyRedirectStatus::FollowedRedirect
|
||||
&& csp_report_only_violation.is_none()
|
||||
{
|
||||
csp_report_only_violation =
|
||||
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&response_content_security_report_only_policies,
|
||||
&request_initiator_url,
|
||||
&response.final_url,
|
||||
response_resource_kind,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
&response_content_security_reporting_endpoints,
|
||||
);
|
||||
csp_report_only_violation = response_policy.url_violation(
|
||||
&request_initiator_url,
|
||||
&response.final_url,
|
||||
response_resource_kind,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
);
|
||||
}
|
||||
if let Some(violation) = content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&response_content_security_policies,
|
||||
if let Some(violation) = response_policy.url_violation(
|
||||
&request_initiator_url,
|
||||
&response.final_url,
|
||||
response_resource_kind,
|
||||
redirect_status,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
&response_content_security_reporting_endpoints,
|
||||
) {
|
||||
let message = module_graph_csp_violation_message(&violation);
|
||||
csp_violation = Some(violation);
|
||||
@@ -809,17 +800,13 @@ fn start_worker_module_graph_fetch(
|
||||
request.url()
|
||||
)),
|
||||
);
|
||||
if let Some(violation) =
|
||||
content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&content_security_report_only_policies,
|
||||
request.initiator_url(),
|
||||
request.url(),
|
||||
resource_kind,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
&content_security_reporting_endpoints,
|
||||
)
|
||||
{
|
||||
if let Some(violation) = policy.url_violation(
|
||||
request.initiator_url(),
|
||||
request.url(),
|
||||
resource_kind,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
ContentSecurityPolicyDisposition::Report,
|
||||
) {
|
||||
completion = completion.with_csp_report_only_violation(violation);
|
||||
}
|
||||
let _ = completion_tx.send(completion);
|
||||
@@ -840,9 +827,9 @@ fn start_worker_module_graph_fetch_batch(
|
||||
.borrow()
|
||||
.module_static_import_content_security_policies
|
||||
.clone(),
|
||||
state.borrow().content_security_policies.clone(),
|
||||
state.borrow().content_security_report_only_policies.clone(),
|
||||
state.borrow().content_security_reporting_endpoints.clone(),
|
||||
state
|
||||
.borrow()
|
||||
.content_security_policy_snapshot_for_inheritance(),
|
||||
module_graph_fetch_tx.clone(),
|
||||
);
|
||||
}
|
||||
@@ -1353,6 +1340,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa
|
||||
content_security_policies,
|
||||
content_security_report_only_policies,
|
||||
content_security_reporting_endpoints,
|
||||
content_security_policy_snapshot,
|
||||
network_policy,
|
||||
policy_context,
|
||||
worker_context_runtime,
|
||||
@@ -1404,6 +1392,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa
|
||||
content_security_policies,
|
||||
content_security_report_only_policies,
|
||||
content_security_reporting_endpoints,
|
||||
content_security_policy_snapshot,
|
||||
network_policy,
|
||||
policy_context,
|
||||
worker_context_runtime,
|
||||
@@ -1535,6 +1524,9 @@ async fn worker_main(
|
||||
content_security_policies: Vec<String>,
|
||||
content_security_report_only_policies: Vec<String>,
|
||||
content_security_reporting_endpoints: ContentSecurityPolicyReportingEndpoints,
|
||||
content_security_policy_snapshot: Option<
|
||||
crate::content_security_policy::InheritedContentSecurityPolicy,
|
||||
>,
|
||||
network_policy: WorkerNetworkPolicy,
|
||||
policy_context: crate::types::SubresourcePolicyContext,
|
||||
worker_context_runtime: RendererWorkerContextRuntime,
|
||||
@@ -1667,6 +1659,7 @@ async fn worker_main(
|
||||
content_security_policies,
|
||||
content_security_report_only_policies,
|
||||
content_security_reporting_endpoints,
|
||||
content_security_policy_snapshot,
|
||||
secure_context,
|
||||
permission_overrides: network_policy.permission_overrides,
|
||||
extra_http_headers: network_policy.extra_http_headers,
|
||||
|
||||
Reference in New Issue
Block a user