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:
ldm0
2026-09-23 00:14:06 +08:00
parent 1290852c72
commit c6bdae1cd0
19 changed files with 732 additions and 126 deletions
+71 -6
View File
@@ -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();
+24 -3
View File
@@ -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 {
+8
View File
@@ -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()
}
+1 -1
View File
@@ -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,
+55 -62
View File
@@ -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,