mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 00:01:00 +00:00
fix(csp): enforce worker module import policies
This commit is contained in:
@@ -42,6 +42,7 @@ pub(crate) enum ContentSecurityPolicyResourceKind {
|
||||
DocumentStyleElement,
|
||||
WorkerConstructor,
|
||||
WorkerConnect,
|
||||
WorkerDynamicModuleImport,
|
||||
WorkerScript,
|
||||
WorkerStaticModuleImport,
|
||||
}
|
||||
@@ -1896,7 +1897,7 @@ impl ContentSecurityPolicyResourceKind {
|
||||
Self::DocumentImage => IMG_SRC,
|
||||
Self::DocumentManifest => MANIFEST_SRC,
|
||||
Self::DocumentMedia => MEDIA_SRC,
|
||||
Self::DocumentScriptElement => SCRIPT_SRC_ELEM,
|
||||
Self::DocumentScriptElement | Self::WorkerDynamicModuleImport => SCRIPT_SRC_ELEM,
|
||||
Self::DocumentStyleElement => STYLE_SRC_ELEM,
|
||||
Self::WorkerConstructor | Self::WorkerStaticModuleImport => WORKER_SRC,
|
||||
Self::WorkerConnect => CONNECT_SRC,
|
||||
@@ -1915,6 +1916,7 @@ impl ContentSecurityPolicyResourceKind {
|
||||
Self::DocumentStyleElement => &[STYLE_SRC_ELEM, STYLE_SRC, DEFAULT_SRC],
|
||||
Self::WorkerConstructor => &[WORKER_SRC, CHILD_SRC, SCRIPT_SRC, DEFAULT_SRC],
|
||||
Self::WorkerConnect => &[CONNECT_SRC, DEFAULT_SRC],
|
||||
Self::WorkerDynamicModuleImport => &[SCRIPT_SRC_ELEM, SCRIPT_SRC, DEFAULT_SRC],
|
||||
Self::WorkerScript => &[SCRIPT_SRC, DEFAULT_SRC],
|
||||
Self::WorkerStaticModuleImport => &[WORKER_SRC, CHILD_SRC, SCRIPT_SRC, DEFAULT_SRC],
|
||||
}
|
||||
@@ -2359,6 +2361,44 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_module_imports_use_their_request_specific_directive_fallbacks() {
|
||||
let cross_origin = "https://cdn.test/worker-import.js";
|
||||
|
||||
assert!(!allowed(
|
||||
"worker-src 'self'; script-src *",
|
||||
ContentSecurityPolicyResourceKind::WorkerStaticModuleImport,
|
||||
cross_origin,
|
||||
));
|
||||
assert!(allowed(
|
||||
"worker-src *; script-src 'self'",
|
||||
ContentSecurityPolicyResourceKind::WorkerStaticModuleImport,
|
||||
cross_origin,
|
||||
));
|
||||
assert!(!allowed(
|
||||
"script-src-elem 'self'; script-src *",
|
||||
ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport,
|
||||
cross_origin,
|
||||
));
|
||||
assert!(allowed(
|
||||
"worker-src 'self'; script-src *",
|
||||
ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport,
|
||||
cross_origin,
|
||||
));
|
||||
|
||||
let violation = content_security_policy_url_violation_with_redirect_status_disposition_and_reporting_endpoints(
|
||||
&["script-src 'self'".to_owned()],
|
||||
&protected_url(),
|
||||
&request_url(cross_origin),
|
||||
ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport,
|
||||
ContentSecurityPolicyRedirectStatus::NoRedirect,
|
||||
ContentSecurityPolicyDisposition::Enforce,
|
||||
&ContentSecurityPolicyReportingEndpoints::default(),
|
||||
)
|
||||
.expect("script-src fallback should block the cross-origin dynamic import");
|
||||
assert_eq!(violation.effective_directive, "script-src-elem");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_source_uses_csp_secure_upgrade_rules() {
|
||||
let protected = Url::parse("http://app.test/page.html").unwrap();
|
||||
|
||||
@@ -148,7 +148,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let document_content_security_policies =
|
||||
let document_meta_content_security_policies =
|
||||
current_document_content_security_policies(scope, global);
|
||||
let document_referrer_policy = current_document_referrer_policy(scope, global);
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
@@ -183,6 +183,18 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
crate::native_bridge::WorkerOwnerScope::Top
|
||||
};
|
||||
let dispatch_scope = crate::native_bridge::OwnerDispatchScope::from(owner_scope);
|
||||
let mut document_content_security_policies = if let Some(handle) = child_handle {
|
||||
host.child_browsing_context_content_security_policies(handle)
|
||||
.map(<[String]>::to_vec)
|
||||
.unwrap_or_else(|| host.document_content_security_policies().to_vec())
|
||||
} else if let Some(policies) =
|
||||
host.active_lightweight_popup_content_security_policies(scope)
|
||||
{
|
||||
policies.to_vec()
|
||||
} else {
|
||||
host.document_content_security_policies().to_vec()
|
||||
};
|
||||
document_content_security_policies.extend(document_meta_content_security_policies);
|
||||
let Some(creator_document_loader) =
|
||||
host.document_resource_loader_for_dispatch_scope(dispatch_scope)
|
||||
else {
|
||||
@@ -420,6 +432,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
|
||||
worker_options.worker_type,
|
||||
worker_options.credentials_mode,
|
||||
document_referrer_policy,
|
||||
document_content_security_policies,
|
||||
worker_options.name.clone(),
|
||||
reserved_service_worker_client_id,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,11 @@ pub(crate) enum WorkerOwnerScope {
|
||||
LightweightPopup(u64),
|
||||
}
|
||||
|
||||
pub(super) struct WorkerModuleStaticImportPolicySnapshot {
|
||||
pub(super) initiator_url: url::Url,
|
||||
pub(super) content_security_policies: Vec<String>,
|
||||
}
|
||||
|
||||
pub(super) enum WorkerExecutionState {
|
||||
Loading {
|
||||
pending_messages: Vec<V8StructuredClonePayload>,
|
||||
@@ -26,6 +31,7 @@ pub(super) enum WorkerExecutionState {
|
||||
outside_settings_load: crate::network::loads::ResourceLoadLease,
|
||||
name: String,
|
||||
module_credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
module_static_import_policy: Option<Box<WorkerModuleStaticImportPolicySnapshot>>,
|
||||
storage_key_top_level_site: String,
|
||||
creator_storage_key: MoliStorageKey,
|
||||
reserved_service_worker_client_id:
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::super::JsContextHost;
|
||||
use super::{WorkerConnectionState, WorkerExecutionState, WorkerRelayTerminalState};
|
||||
use super::{
|
||||
WorkerConnectionState, WorkerExecutionState, WorkerModuleStaticImportPolicySnapshot,
|
||||
WorkerRelayTerminalState,
|
||||
};
|
||||
use crate::RendererSyntheticResponseBody;
|
||||
use crate::network::loads::{ResourceLoadDisposition, ResourceLoadKind, ResourceLoadLease};
|
||||
use crate::page_task_queue::{
|
||||
@@ -221,6 +224,7 @@ impl JsContextHost {
|
||||
outside_settings_load,
|
||||
name,
|
||||
module_credentials_mode,
|
||||
module_static_import_policy: None,
|
||||
storage_key_top_level_site,
|
||||
creator_storage_key,
|
||||
reserved_service_worker_client_id,
|
||||
@@ -240,6 +244,7 @@ impl JsContextHost {
|
||||
script_kind: WorkerScriptKind,
|
||||
module_credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
document_referrer_policy: Option<String>,
|
||||
document_content_security_policies: Vec<String>,
|
||||
name: String,
|
||||
reserved_service_worker_client_id: Option<ServiceWorkerClientId>,
|
||||
) -> bool {
|
||||
@@ -265,6 +270,10 @@ impl JsContextHost {
|
||||
let cancel_handle = moli_fetch::FetchCancelHandle::new();
|
||||
outside_settings_load.attach_cancel_handle(cancel_handle.clone());
|
||||
let creator_secure_context = moli_url::is_potentially_trustworthy_url(&initiator_url);
|
||||
let module_static_import_policy = Box::new(WorkerModuleStaticImportPolicySnapshot {
|
||||
initiator_url: initiator_url.clone(),
|
||||
content_security_policies: document_content_security_policies,
|
||||
});
|
||||
let task_runner = outside_settings_load.task_runner();
|
||||
let fetch_task_runner = task_runner.clone();
|
||||
let load_task = task_runner.spawn_abortable(async move {
|
||||
@@ -329,6 +338,7 @@ impl JsContextHost {
|
||||
load_task: slot,
|
||||
terminated,
|
||||
name: loading_name,
|
||||
module_static_import_policy: loading_module_static_import_policy,
|
||||
..
|
||||
} => {
|
||||
if *terminated {
|
||||
@@ -336,6 +346,7 @@ impl JsContextHost {
|
||||
return false;
|
||||
}
|
||||
*loading_name = name;
|
||||
*loading_module_static_import_policy = Some(module_static_import_policy);
|
||||
*slot = Some(load_task);
|
||||
true
|
||||
}
|
||||
@@ -424,18 +435,21 @@ impl JsContextHost {
|
||||
content_security_reporting_endpoints:
|
||||
crate::content_security_policy::ContentSecurityPolicyReportingEndpoints,
|
||||
) -> bool {
|
||||
struct FinishLoadingWorkerSpawn {
|
||||
pending_messages: Vec<V8StructuredClonePayload>,
|
||||
name: String,
|
||||
storage_key_top_level_site: String,
|
||||
creator_storage_key: MoliStorageKey,
|
||||
reserved_service_worker_client_id: Option<ServiceWorkerClientId>,
|
||||
module_credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
module_static_import_policy: Option<Box<WorkerModuleStaticImportPolicySnapshot>>,
|
||||
request_client: crate::network::ResourceRequestClient,
|
||||
}
|
||||
|
||||
enum FinishLoadingAction {
|
||||
MissingOrRunning,
|
||||
DiscardTerminated,
|
||||
Spawn {
|
||||
pending_messages: Vec<V8StructuredClonePayload>,
|
||||
name: String,
|
||||
storage_key_top_level_site: String,
|
||||
creator_storage_key: MoliStorageKey,
|
||||
reserved_service_worker_client_id: Option<ServiceWorkerClientId>,
|
||||
module_credentials_mode: moli_fetch::RequestCredentialsMode,
|
||||
request_client: crate::network::ResourceRequestClient,
|
||||
},
|
||||
Spawn(Box<FinishLoadingWorkerSpawn>),
|
||||
}
|
||||
|
||||
let action = match self.workers.get_mut(&worker_id) {
|
||||
@@ -446,6 +460,7 @@ impl JsContextHost {
|
||||
terminated,
|
||||
name,
|
||||
module_credentials_mode,
|
||||
module_static_import_policy,
|
||||
storage_key_top_level_site,
|
||||
creator_storage_key,
|
||||
reserved_service_worker_client_id,
|
||||
@@ -455,7 +470,7 @@ impl JsContextHost {
|
||||
if *terminated {
|
||||
FinishLoadingAction::DiscardTerminated
|
||||
} else {
|
||||
FinishLoadingAction::Spawn {
|
||||
FinishLoadingAction::Spawn(Box::new(FinishLoadingWorkerSpawn {
|
||||
pending_messages: std::mem::take(pending_messages),
|
||||
name: name.clone(),
|
||||
storage_key_top_level_site: storage_key_top_level_site.clone(),
|
||||
@@ -463,45 +478,31 @@ impl JsContextHost {
|
||||
reserved_service_worker_client_id: reserved_service_worker_client_id
|
||||
.take(),
|
||||
module_credentials_mode: *module_credentials_mode,
|
||||
module_static_import_policy: module_static_import_policy.take(),
|
||||
request_client: outside_settings_load.request_client(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
WorkerExecutionState::Running { .. } => FinishLoadingAction::MissingOrRunning,
|
||||
},
|
||||
None => FinishLoadingAction::MissingOrRunning,
|
||||
};
|
||||
let (
|
||||
let FinishLoadingWorkerSpawn {
|
||||
pending_messages,
|
||||
name,
|
||||
storage_key_top_level_site,
|
||||
creator_storage_key,
|
||||
reserved_service_worker_client_id,
|
||||
module_credentials_mode,
|
||||
module_static_import_policy,
|
||||
request_client,
|
||||
) = match action {
|
||||
} = match action {
|
||||
FinishLoadingAction::MissingOrRunning => return false,
|
||||
FinishLoadingAction::DiscardTerminated => {
|
||||
self.forget_worker(worker_id);
|
||||
return false;
|
||||
}
|
||||
FinishLoadingAction::Spawn {
|
||||
pending_messages,
|
||||
name,
|
||||
storage_key_top_level_site,
|
||||
creator_storage_key,
|
||||
reserved_service_worker_client_id,
|
||||
module_credentials_mode,
|
||||
request_client,
|
||||
} => (
|
||||
pending_messages,
|
||||
name,
|
||||
storage_key_top_level_site,
|
||||
creator_storage_key,
|
||||
reserved_service_worker_client_id,
|
||||
module_credentials_mode,
|
||||
request_client,
|
||||
),
|
||||
FinishLoadingAction::Spawn(spawn) => *spawn,
|
||||
};
|
||||
let network_policy = WorkerNetworkPolicy {
|
||||
secure_context,
|
||||
@@ -538,6 +539,13 @@ impl JsContextHost {
|
||||
self.browser_context_runtime()
|
||||
.dedicated_worker_pause_on_start_for_devtools(),
|
||||
);
|
||||
if let Some(policy) = module_static_import_policy {
|
||||
spawn_options = spawn_options
|
||||
.with_module_static_import_initiator_url(policy.initiator_url)
|
||||
.with_module_static_import_content_security_policies(
|
||||
policy.content_security_policies,
|
||||
);
|
||||
}
|
||||
if let Some(client_id) = reserved_service_worker_client_id {
|
||||
spawn_options = spawn_options.with_reserved_service_worker_client_id(client_id);
|
||||
}
|
||||
|
||||
@@ -2909,6 +2909,88 @@ async fn worker_pending_activity_diagnostics_split_loading_and_running_worker_is
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports() {
|
||||
run_page_vm_async_test(async move {
|
||||
let (dependency_base_url, dependency_server) = spawn_path_response_http_server(vec![(
|
||||
"/dependency.js",
|
||||
"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *",
|
||||
"export const value = 'unexpected';".to_owned(),
|
||||
Duration::ZERO,
|
||||
)])
|
||||
.await;
|
||||
let dependency_url = format!("{dependency_base_url}/dependency.js");
|
||||
let worker_source = format!(
|
||||
r#"
|
||||
import {dependency_url:?};
|
||||
postMessage("unexpected");
|
||||
"#
|
||||
);
|
||||
let (base_url, server) = spawn_path_response_http_server(vec![(
|
||||
"/worker.js",
|
||||
"HTTP/1.1 200 OK",
|
||||
worker_source,
|
||||
Duration::ZERO,
|
||||
)])
|
||||
.await;
|
||||
let document_url = Url::parse(&format!("{base_url}/page.html")).expect("document url");
|
||||
let mut page_vm = test_page_vm_with_document_url(document_url.clone());
|
||||
page_vm.vm_mut().set_main_navigation_policy_container(
|
||||
crate::document_runtime::DocumentPolicyContainer::from_navigation_response_headers(
|
||||
&[(
|
||||
"Content-Security-Policy".to_owned(),
|
||||
"worker-src 'self'; script-src data:".to_owned(),
|
||||
)],
|
||||
&document_url,
|
||||
),
|
||||
);
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
local_executor
|
||||
.run(async move {
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__moduleWorkerCspResult = null;
|
||||
globalThis.__moduleWorkerCspDone = false;
|
||||
const worker = new Worker("/worker.js", { type: "module" });
|
||||
worker.onmessage = event => {
|
||||
globalThis.__moduleWorkerCspResult = "message:" + event.data;
|
||||
globalThis.__moduleWorkerCspDone = true;
|
||||
};
|
||||
worker.onerror = event => {
|
||||
event.preventDefault();
|
||||
globalThis.__moduleWorkerCspResult = "error:" + event.message;
|
||||
globalThis.__moduleWorkerCspDone = true;
|
||||
};
|
||||
})()
|
||||
"#,
|
||||
)?;
|
||||
drive_websocket_until_done(
|
||||
&mut page_vm,
|
||||
"String(globalThis.__moduleWorkerCspDone === true)",
|
||||
"creator CSP should settle the external module worker",
|
||||
)
|
||||
.await?;
|
||||
let result = page_vm
|
||||
.vm_mut()
|
||||
.eval("globalThis.__moduleWorkerCspResult")?;
|
||||
assert!(
|
||||
result.starts_with("error:") && result.contains("Content Security Policy"),
|
||||
"static module import should be blocked by creator worker-src: {result:?}"
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("external module worker creator CSP test should run on owner lane");
|
||||
server
|
||||
.await
|
||||
.expect("external module worker CSP server should finish");
|
||||
dependency_server.abort();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn service_worker_register_starts_module_worker_global() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
@@ -611,7 +611,7 @@ fn start_worker_module_graph_fetch(
|
||||
worker_global_content_security_policies,
|
||||
worker_global_content_security_report_only_policies,
|
||||
worker_global_content_security_reporting_endpoints,
|
||||
crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerScript,
|
||||
crate::content_security_policy::ContentSecurityPolicyResourceKind::WorkerDynamicModuleImport,
|
||||
),
|
||||
};
|
||||
let initial_csp_report_only_violation =
|
||||
|
||||
@@ -2225,7 +2225,7 @@ async fn worker_dynamic_import_uses_worker_response_csp_not_outside_static_csp()
|
||||
.with_module_static_import_content_security_policies(vec![
|
||||
"worker-src *; script-src 'self'".to_owned(),
|
||||
])
|
||||
.with_content_security_policies(vec!["script-src 'self'".to_owned()]),
|
||||
.with_content_security_policies(vec!["script-src-elem 'self'; script-src *".to_owned()]),
|
||||
);
|
||||
|
||||
let msg = timeout(TIMEOUT, handle.recv())
|
||||
@@ -2234,7 +2234,7 @@ async fn worker_dynamic_import_uses_worker_response_csp_not_outside_static_csp()
|
||||
.expect("channel closed");
|
||||
assert_eq!(
|
||||
expect_post_json(msg),
|
||||
r#"{"status":"blocked","events":[{"type":"securitypolicyviolation","effectiveDirective":"script-src","violatedDirective":"script-src","blockedURI":"http://127.0.0.1:9/worker/dynamic.js","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src 'self'","disposition":"enforce","instance":true}],"name":"TypeError","csp":true}"#
|
||||
r#"{"status":"blocked","events":[{"type":"securitypolicyviolation","effectiveDirective":"script-src-elem","violatedDirective":"script-src-elem","blockedURI":"http://127.0.0.1:9/worker/dynamic.js","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src-elem 'self'; script-src *","disposition":"enforce","instance":true}],"name":"TypeError","csp":true}"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2391,8 +2391,8 @@ async fn shared_worker_dynamic_import_csp_block_dispatches_securitypolicyviolati
|
||||
let matched = false;
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
matched = event.type === "securitypolicyviolation" &&
|
||||
event.effectiveDirective === "script-src" &&
|
||||
event.violatedDirective === "script-src" &&
|
||||
event.effectiveDirective === "script-src-elem" &&
|
||||
event.violatedDirective === "script-src-elem" &&
|
||||
event.blockedURI === "http://127.0.0.1:9/worker/dynamic.js" &&
|
||||
event.documentURI === "https://app.test/shared-worker.js" &&
|
||||
event.originalPolicy === "script-src 'self'" &&
|
||||
@@ -2661,7 +2661,7 @@ async fn worker_dynamic_import_report_only_csp_dispatches_without_blocking() {
|
||||
assert_eq!(
|
||||
expect_post_json(msg),
|
||||
format!(
|
||||
r#"{{"events":[{{"type":"securitypolicyviolation","effectiveDirective":"script-src","violatedDirective":"script-src","blockedURI":"{dep_url}","documentURI":"{script_url}","originalPolicy":"script-src 'none'","disposition":"report","instance":true}}],"value":42}}"#
|
||||
r#"{{"events":[{{"type":"securitypolicyviolation","effectiveDirective":"script-src-elem","violatedDirective":"script-src-elem","blockedURI":"{dep_url}","documentURI":"{script_url}","originalPolicy":"script-src 'none'","disposition":"report","instance":true}}],"value":42}}"#
|
||||
)
|
||||
);
|
||||
server
|
||||
|
||||
+2
-2
@@ -117,7 +117,7 @@ promise_test(async function () {
|
||||
assert_equals(outcome.value, "dynamic-import-ok");
|
||||
assert_true(
|
||||
outcome.events.some(event => event.disposition === "report" &&
|
||||
event.effectiveDirective === "script-src" &&
|
||||
event.effectiveDirective === "script-src-elem" &&
|
||||
event.instance &&
|
||||
event.blockedURI.includes("/report-to-dynamic-dependency.js")),
|
||||
"SharedWorker report-only CSP should dispatch a dynamic import SecurityPolicyViolationEvent",
|
||||
@@ -126,7 +126,7 @@ promise_test(async function () {
|
||||
const match = await wait_for_csp_report(token, report => {
|
||||
return report.type === "csp-violation" &&
|
||||
report.body.disposition === "report" &&
|
||||
report.body.effectiveDirective === "script-src" &&
|
||||
report.body.effectiveDirective === "script-src-elem" &&
|
||||
report.body.blockedURL.includes("/report-to-dynamic-dependency.js");
|
||||
});
|
||||
assert_true(
|
||||
|
||||
@@ -82,7 +82,7 @@ promise_test(async function () {
|
||||
"dedicated worker report-only CSP should dispatch an XHR SecurityPolicyViolationEvent",
|
||||
);
|
||||
assert_true(
|
||||
has_event(outcome.events, "script-src", "worker-response-csp-report-to-dynamic-dependency.js"),
|
||||
has_event(outcome.events, "script-src-elem", "worker-response-csp-report-to-dynamic-dependency.js"),
|
||||
"dedicated worker report-only CSP should dispatch a dynamic import SecurityPolicyViolationEvent",
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ promise_test(async function () {
|
||||
{ directive: "connect-src", blockedText: "target.txt?worker-fetch", label: "fetch" },
|
||||
{ directive: "connect-src", blockedText: "target.txt?worker-xhr", label: "XHR" },
|
||||
{
|
||||
directive: "script-src",
|
||||
directive: "script-src-elem",
|
||||
blockedText: "worker-response-csp-report-to-dynamic-dependency.js",
|
||||
label: "dynamic import",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user