mirror of
https://github.com/lexmount/moli.git
synced 2026-09-24 16:01:31 +00:00
fix(csp): correct importScripts policy checks and events
Use script-src-elem with script-src/default-src fallbacks for imported classic scripts. Queue Worker CSP violations after the current task while preserving the importer's sanitized source location and synchronous NetworkError behavior. Cover late listeners, microtask ordering, directive precedence, imported callsites, and report-only behavior in dedicated and shared workers.
This commit is contained in:
@@ -2047,11 +2047,12 @@ impl ContentSecurityPolicyResourceKind {
|
||||
Self::DocumentImage => IMG_SRC,
|
||||
Self::DocumentManifest => MANIFEST_SRC,
|
||||
Self::DocumentMedia => MEDIA_SRC,
|
||||
Self::DocumentScriptElement | Self::WorkerDynamicModuleImport => SCRIPT_SRC_ELEM,
|
||||
Self::DocumentScriptElement | Self::WorkerDynamicModuleImport | Self::WorkerScript => {
|
||||
SCRIPT_SRC_ELEM
|
||||
}
|
||||
Self::DocumentStyleElement => STYLE_SRC_ELEM,
|
||||
Self::WorkerConstructor | Self::WorkerStaticModuleImport => WORKER_SRC,
|
||||
Self::WorkerConnect => CONNECT_SRC,
|
||||
Self::WorkerScript => SCRIPT_SRC,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2068,7 +2069,7 @@ impl ContentSecurityPolicyResourceKind {
|
||||
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::WorkerScript => &[SCRIPT_SRC_ELEM, SCRIPT_SRC, DEFAULT_SRC],
|
||||
Self::WorkerStaticModuleImport => &[WORKER_SRC, CHILD_SRC, SCRIPT_SRC, DEFAULT_SRC],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +137,13 @@ fn check_import_script_csp(
|
||||
checked_url: &Url,
|
||||
redirect_status: ContentSecurityPolicyRedirectStatus,
|
||||
) -> Result<(), WorkerImportScriptError> {
|
||||
let (report, enforce) = {
|
||||
let (wake_tx, mut report, mut enforce) = {
|
||||
let state = state.borrow();
|
||||
let Some(protected_url) = state.current_script_url.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
(
|
||||
state.worker_wake_tx.clone(),
|
||||
worker_content_security_policy_report_only_violation_for_checked_url_with_redirect_status(
|
||||
&state, protected_url, checked_url, request_url, ContentSecurityPolicyResourceKind::WorkerScript, redirect_status,
|
||||
),
|
||||
@@ -151,14 +152,26 @@ fn check_import_script_csp(
|
||||
),
|
||||
)
|
||||
};
|
||||
// importScripts throws synchronously, but CSP events run in a later task.
|
||||
// Capture the caller now, before its stack is lost or imported code runs.
|
||||
if report.is_some() || enforce.is_some() {
|
||||
let location =
|
||||
crate::content_security_policy::ContentSecurityPolicySourceLocation::capture(scope);
|
||||
for violation in [&mut report, &mut enforce].into_iter().flatten() {
|
||||
location.apply_to(violation);
|
||||
}
|
||||
}
|
||||
if let Some(violation) = report {
|
||||
dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation);
|
||||
let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation(
|
||||
Box::new(violation),
|
||||
));
|
||||
}
|
||||
if let Some(violation) = enforce {
|
||||
dispatch_worker_content_security_policy_violation_event_for_state(scope, state, &violation);
|
||||
return Err(WorkerImportScriptError::network(
|
||||
worker_content_security_policy_error_message(&violation, "importScripts"),
|
||||
let message = worker_content_security_policy_error_message(&violation, "importScripts");
|
||||
let _ = wake_tx.send(WorkerMessage::DispatchContentSecurityPolicyViolation(
|
||||
Box::new(violation),
|
||||
));
|
||||
return Err(WorkerImportScriptError::network(message));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -161,9 +161,9 @@ async fn worker_importscripts_cross_origin_respects_corp_and_coep() {
|
||||
async fn worker_importscripts_redirects_check_csp_and_ignore_redirected_paths() {
|
||||
ensure_v8();
|
||||
for (report_only, allow_foreign, expected) in [
|
||||
(false, false, r#"["NetworkError",false,["enforce"]]"#),
|
||||
(true, false, r#"["ok",true,["report"]]"#),
|
||||
(false, true, r#"["ok",true,[]]"#),
|
||||
(false, false, r#"["NetworkError",false,["enforce"],true]"#),
|
||||
(true, false, r#"["ok",true,["report"],true]"#),
|
||||
(false, true, r#"["ok",true,[],true]"#),
|
||||
] {
|
||||
let (foreign_url, foreign_server) = spawn_path_response_http_server(vec![(
|
||||
"/redirect-target/foreign.js",
|
||||
@@ -190,11 +190,28 @@ async fn worker_importscripts_redirects_check_csp_and_ignore_redirected_paths()
|
||||
let mut options = WorkerSpawnOptions::new(
|
||||
r#"
|
||||
const violations = [];
|
||||
addEventListener('securitypolicyviolation', event => violations.push(event.disposition));
|
||||
let outcome = 'ok';
|
||||
try { importScripts('./redirect.js'); } catch (error) { outcome = error.name; }
|
||||
postMessage([outcome, self.loaded === true, violations]); close();
|
||||
"#.into(), format!("{worker_url}/worker/main.js"),
|
||||
let microtaskRan = false;
|
||||
queueMicrotask(() => microtaskRan = true);
|
||||
const finish = () => {
|
||||
postMessage([outcome, self.loaded === true, violations, microtaskRan]);
|
||||
close();
|
||||
};
|
||||
if (EXPECT_VIOLATION) {
|
||||
addEventListener('securitypolicyviolation', event => {
|
||||
violations.push(event.disposition);
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
queueMicrotask(finish);
|
||||
}
|
||||
"#
|
||||
.replace(
|
||||
"EXPECT_VIOLATION",
|
||||
if allow_foreign { "false" } else { "true" },
|
||||
),
|
||||
format!("{worker_url}/worker/main.js"),
|
||||
);
|
||||
options = if report_only {
|
||||
options.with_content_security_report_only_policies(vec![policy])
|
||||
|
||||
@@ -154,32 +154,42 @@ async fn worker_importscripts_obeys_response_csp_script_src() {
|
||||
WorkerSpawnOptions::new(
|
||||
r#"
|
||||
const events = [];
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
events.push({
|
||||
type: event.type,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
violatedDirective: event.violatedDirective,
|
||||
blockedURI: event.blockedURI,
|
||||
documentURI: event.documentURI,
|
||||
originalPolicy: event.originalPolicy,
|
||||
disposition: event.disposition,
|
||||
instance: event instanceof SecurityPolicyViolationEvent
|
||||
});
|
||||
});
|
||||
let name;
|
||||
addEventListener("securitypolicyviolation", event => events.push(event));
|
||||
try {
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
postMessage("unexpected");
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
events,
|
||||
name: error && error.name,
|
||||
ran: globalThis.__ran === true,
|
||||
});
|
||||
name = error.name;
|
||||
}
|
||||
close();
|
||||
const eventsAtReturn = events.length;
|
||||
let eventsAtMicrotask;
|
||||
queueMicrotask(() => eventsAtMicrotask = events.length);
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
postMessage({
|
||||
event: {
|
||||
type: event.type,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
violatedDirective: event.violatedDirective,
|
||||
blockedURI: event.blockedURI,
|
||||
documentURI: event.documentURI,
|
||||
originalPolicy: event.originalPolicy,
|
||||
disposition: event.disposition,
|
||||
instance: event instanceof SecurityPolicyViolationEvent,
|
||||
sourceFile: event.sourceFile,
|
||||
lineNumber: event.lineNumber,
|
||||
columnNumber: event.columnNumber,
|
||||
},
|
||||
name,
|
||||
ran: globalThis.__ran === true,
|
||||
eventsAtReturn,
|
||||
eventsAtMicrotask,
|
||||
});
|
||||
close();
|
||||
});
|
||||
"#
|
||||
.into(),
|
||||
"https://app.test/worker/main.js".into(),
|
||||
"https://app.test/worker/main.js?secret=1".into(),
|
||||
)
|
||||
.with_content_security_policies(vec!["script-src 'none'".to_owned()]),
|
||||
);
|
||||
@@ -190,7 +200,105 @@ async fn worker_importscripts_obeys_response_csp_script_src() {
|
||||
.expect("channel closed");
|
||||
assert_eq!(
|
||||
expect_post_json(msg),
|
||||
r#"{"events":[{"type":"securitypolicyviolation","effectiveDirective":"script-src","violatedDirective":"script-src","blockedURI":"data","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src 'none'","disposition":"enforce","instance":true}],"name":"NetworkError","ran":false}"#
|
||||
r#"{"event":{"type":"securitypolicyviolation","effectiveDirective":"script-src-elem","violatedDirective":"script-src-elem","blockedURI":"data","documentURI":"https://app.test/worker/main.js?secret=1","originalPolicy":"script-src 'none'","disposition":"enforce","instance":true,"sourceFile":"https://app.test/worker/main.js","lineNumber":6,"columnNumber":17},"name":"NetworkError","ran":false,"eventsAtReturn":0,"eventsAtMicrotask":0}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_importscripts_csp_uses_script_src_elem_and_its_fallbacks() {
|
||||
ensure_v8();
|
||||
for (policies, blocked) in [
|
||||
(vec!["script-src 'none'; script-src-elem data:"], false),
|
||||
(vec!["script-src data:; script-src-elem 'none'"], true),
|
||||
(vec!["default-src 'none'; script-src data:"], false),
|
||||
(vec!["default-src data:; script-src 'none'"], true),
|
||||
(vec!["default-src data:"], false),
|
||||
(vec!["default-src 'none'"], true),
|
||||
(vec!["worker-src 'none'"], false),
|
||||
(vec!["script-src-elem data:", "script-src 'none'"], true),
|
||||
] {
|
||||
let mut handle = spawn_test_worker_with_options(
|
||||
WorkerSpawnOptions::new(
|
||||
r#"
|
||||
let name;
|
||||
try {
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
} catch (error) {
|
||||
name = error.name;
|
||||
}
|
||||
if (name === undefined) {
|
||||
postMessage({ran: globalThis.__ran === true});
|
||||
close();
|
||||
} else {
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
postMessage({
|
||||
ran: globalThis.__ran === true,
|
||||
name,
|
||||
directive: event.effectiveDirective,
|
||||
disposition: event.disposition,
|
||||
});
|
||||
close();
|
||||
});
|
||||
}
|
||||
"#
|
||||
.into(),
|
||||
"https://app.test/worker/main.js".into(),
|
||||
)
|
||||
.with_content_security_policies(policies.iter().map(|p| (*p).to_owned()).collect()),
|
||||
);
|
||||
let msg = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("timed out")
|
||||
.expect("channel closed");
|
||||
assert_eq!(
|
||||
expect_post_json(msg),
|
||||
if blocked {
|
||||
r#"{"ran":false,"name":"NetworkError","directive":"script-src-elem","disposition":"enforce"}"#
|
||||
} else {
|
||||
r#"{"ran":true}"#
|
||||
},
|
||||
"{policies:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_importscripts_csp_reports_the_imported_callsite() {
|
||||
ensure_v8();
|
||||
let mut handle = spawn_test_worker_with_options(
|
||||
WorkerSpawnOptions::new(
|
||||
r#"
|
||||
let name;
|
||||
try {
|
||||
importScripts("data:text/javascript," + encodeURIComponent(
|
||||
"\n importScripts('https://blocked.test/script.js');"
|
||||
));
|
||||
} catch (error) {
|
||||
name = error.name;
|
||||
}
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
postMessage({
|
||||
name,
|
||||
blockedURI: event.blockedURI,
|
||||
sourceFile: event.sourceFile,
|
||||
lineNumber: event.lineNumber,
|
||||
columnNumber: event.columnNumber,
|
||||
});
|
||||
close();
|
||||
});
|
||||
"#
|
||||
.into(),
|
||||
"https://app.test/worker/main.js".into(),
|
||||
)
|
||||
.with_content_security_policies(vec!["script-src data:".into()]),
|
||||
);
|
||||
let msg = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("timed out")
|
||||
.expect("channel closed");
|
||||
assert_eq!(
|
||||
expect_post_json(msg),
|
||||
r#"{"name":"NetworkError","blockedURI":"https://blocked.test/script.js","sourceFile":"data","lineNumber":2,"columnNumber":3}"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,27 +314,27 @@ async fn worker_csp_violation_event_survives_mutated_event_globals() {
|
||||
writable: false,
|
||||
configurable: true
|
||||
});
|
||||
const events = [];
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
events.push({
|
||||
type: event.type,
|
||||
blockedURI: event.blockedURI,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
disposition: event.disposition,
|
||||
instance: event instanceof SecurityPolicyViolationEvent
|
||||
});
|
||||
});
|
||||
let name;
|
||||
try {
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
postMessage("unexpected");
|
||||
} catch (error) {
|
||||
name = error.name;
|
||||
}
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
postMessage({
|
||||
events,
|
||||
name: error && error.name,
|
||||
event: {
|
||||
type: event.type,
|
||||
blockedURI: event.blockedURI,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
disposition: event.disposition,
|
||||
instance: event instanceof SecurityPolicyViolationEvent
|
||||
},
|
||||
name,
|
||||
ran: globalThis.__ran === true,
|
||||
});
|
||||
}
|
||||
close();
|
||||
close();
|
||||
});
|
||||
"#
|
||||
.into(),
|
||||
"https://app.test/worker/main.js".into(),
|
||||
@@ -240,7 +348,7 @@ async fn worker_csp_violation_event_survives_mutated_event_globals() {
|
||||
.expect("channel closed");
|
||||
assert_eq!(
|
||||
expect_post_json(msg),
|
||||
r#"{"events":[{"type":"securitypolicyviolation","blockedURI":"data","effectiveDirective":"script-src","disposition":"enforce","instance":true}],"name":"NetworkError","ran":false}"#
|
||||
r#"{"event":{"type":"securitypolicyviolation","blockedURI":"data","effectiveDirective":"script-src-elem","disposition":"enforce","instance":true},"name":"NetworkError","ran":false}"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -257,24 +365,27 @@ async fn shared_worker_importscripts_csp_block_dispatches_securitypolicyviolatio
|
||||
WorkerSpawnOptions::new(
|
||||
r#"
|
||||
onconnect = () => {
|
||||
let matched = false;
|
||||
let name;
|
||||
try {
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
} catch (error) {
|
||||
name = error.name;
|
||||
}
|
||||
let microtaskRan = false;
|
||||
queueMicrotask(() => microtaskRan = true);
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
matched = event.type === "securitypolicyviolation" &&
|
||||
event.effectiveDirective === "script-src" &&
|
||||
event.violatedDirective === "script-src" &&
|
||||
const matched = event.type === "securitypolicyviolation" &&
|
||||
event.effectiveDirective === "script-src-elem" &&
|
||||
event.violatedDirective === "script-src-elem" &&
|
||||
event.blockedURI === "data" &&
|
||||
event.documentURI === "https://app.test/shared-worker.js" &&
|
||||
event.originalPolicy === "script-src 'none'" &&
|
||||
event.disposition === "enforce" &&
|
||||
event instanceof SecurityPolicyViolationEvent;
|
||||
});
|
||||
try {
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
} catch (_) {
|
||||
if (matched && globalThis.__ran !== true) {
|
||||
if (matched && name === "NetworkError" && microtaskRan && globalThis.__ran !== true) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
"#
|
||||
.into(),
|
||||
@@ -309,24 +420,29 @@ async fn worker_importscripts_report_only_csp_dispatches_without_blocking() {
|
||||
WorkerSpawnOptions::new(
|
||||
r#"
|
||||
const events = [];
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
events.push({
|
||||
type: event.type,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
violatedDirective: event.violatedDirective,
|
||||
blockedURI: event.blockedURI,
|
||||
documentURI: event.documentURI,
|
||||
originalPolicy: event.originalPolicy,
|
||||
disposition: event.disposition,
|
||||
instance: event instanceof SecurityPolicyViolationEvent
|
||||
});
|
||||
});
|
||||
addEventListener("securitypolicyviolation", event => events.push(event));
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
postMessage({
|
||||
events,
|
||||
ran: globalThis.__ran === true,
|
||||
const eventsAtReturn = events.length;
|
||||
let eventsAtMicrotask;
|
||||
queueMicrotask(() => eventsAtMicrotask = events.length);
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
postMessage({
|
||||
event: {
|
||||
type: event.type,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
violatedDirective: event.violatedDirective,
|
||||
blockedURI: event.blockedURI,
|
||||
documentURI: event.documentURI,
|
||||
originalPolicy: event.originalPolicy,
|
||||
disposition: event.disposition,
|
||||
instance: event instanceof SecurityPolicyViolationEvent
|
||||
},
|
||||
ran: globalThis.__ran === true,
|
||||
eventsAtReturn,
|
||||
eventsAtMicrotask,
|
||||
});
|
||||
close();
|
||||
});
|
||||
close();
|
||||
"#
|
||||
.into(),
|
||||
"https://app.test/worker/main.js".into(),
|
||||
@@ -340,7 +456,7 @@ async fn worker_importscripts_report_only_csp_dispatches_without_blocking() {
|
||||
.expect("channel closed");
|
||||
assert_eq!(
|
||||
expect_post_json(msg),
|
||||
r#"{"events":[{"type":"securitypolicyviolation","effectiveDirective":"script-src","violatedDirective":"script-src","blockedURI":"data","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src 'none'","disposition":"report","instance":true}],"ran":true}"#
|
||||
r#"{"event":{"type":"securitypolicyviolation","effectiveDirective":"script-src-elem","violatedDirective":"script-src-elem","blockedURI":"data","documentURI":"https://app.test/worker/main.js","originalPolicy":"script-src 'none'","disposition":"report","instance":true},"ran":true,"eventsAtReturn":0,"eventsAtMicrotask":0}"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -357,21 +473,22 @@ async fn shared_worker_importscripts_report_only_csp_dispatches_without_blocking
|
||||
WorkerSpawnOptions::new(
|
||||
r#"
|
||||
onconnect = () => {
|
||||
let matched = false;
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
let microtaskRan = false;
|
||||
queueMicrotask(() => microtaskRan = true);
|
||||
addEventListener("securitypolicyviolation", event => {
|
||||
matched = event.type === "securitypolicyviolation" &&
|
||||
event.effectiveDirective === "script-src" &&
|
||||
event.violatedDirective === "script-src" &&
|
||||
const matched = event.type === "securitypolicyviolation" &&
|
||||
event.effectiveDirective === "script-src-elem" &&
|
||||
event.violatedDirective === "script-src-elem" &&
|
||||
event.blockedURI === "data" &&
|
||||
event.documentURI === "https://app.test/shared-worker.js" &&
|
||||
event.originalPolicy === "script-src 'none'" &&
|
||||
event.disposition === "report" &&
|
||||
event instanceof SecurityPolicyViolationEvent;
|
||||
if (matched && microtaskRan && globalThis.__ran === true) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
importScripts("data:text/javascript,globalThis.__ran=true");
|
||||
if (matched && globalThis.__ran === true) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
"#
|
||||
.into(),
|
||||
|
||||
Reference in New Issue
Block a user