fix(csp): report eval source locations

Source-commit: 0bc8cbd5bc
This commit is contained in:
ldm0
2026-09-11 01:44:35 +08:00
parent 5699c4621b
commit b834b680e9
2 changed files with 99 additions and 12 deletions
@@ -831,12 +831,15 @@ impl JsContextHost {
if report_only_violation.is_none() && enforced_violation.is_none() {
return true;
}
if include_call_location {
let (line_number, column_number) = current_script_call_location(scope);
if include_call_location
&& let Some((source_file, line_number, column_number)) =
current_script_violation_location(scope)
{
for violation in [&mut report_only_violation, &mut enforced_violation]
.into_iter()
.flatten()
{
violation.source_file = source_file.clone();
violation.line_number = line_number;
violation.column_number = column_number;
}
@@ -1264,14 +1267,21 @@ impl JsContextHost {
}
}
fn current_script_call_location(scope: &v8::PinScope<'_, '_>) -> (i32, i32) {
let Some(stack) = v8::StackTrace::current_stack_trace(scope, 1) else {
return (0, 0);
};
let Some(frame) = stack.get_frame(scope, 0) else {
return (0, 0);
};
let line = i32::try_from(frame.get_line_number()).unwrap_or(0).max(0);
let column = i32::try_from(frame.get_column()).unwrap_or(0).max(0);
(line, column)
fn current_script_violation_location(
scope: &mut v8::PinScope<'_, '_>,
) -> Option<(String, i32, i32)> {
let stack = v8::StackTrace::current_stack_trace(scope, 1)?;
let frame = stack.get_frame(scope, 0)?;
let source_file = frame
.get_script_name_or_source_url(scope)
.map(|source| source.to_rust_string_lossy(scope))
.map(|source| {
crate::content_security_policy::content_security_policy_source_file_for_report(&source)
})
.unwrap_or_default();
let line_number = i32::try_from(frame.get_line_number())
.unwrap_or_default()
.max(0);
let column_number = i32::try_from(frame.get_column()).unwrap_or_default().max(0);
Some((source_file, line_number, column_number))
}
@@ -1062,3 +1062,80 @@ async fn prepared_parser_inline_script_csp_blocks_before_v8_execution_and_report
r#"[{"blockedURI":"inline","effectiveDirective":"script-src-elem","lineNumber":0,"columnNumber":0}]"#
);
}
#[tokio::test]
async fn eval_csp_violation_reports_external_script_scheme_and_call_location() {
let source = r#"
globalThis.__evalCspErrorName = "";
try {
eval("globalThis.__blockedEvalRan = true");
} catch (error) {
globalThis.__evalCspErrorName = error.name;
}
"#;
for (index, (script_url, expected_source_file)) in [
("data:text/javascript,eval-source", "data"),
(
"blob:https://eval-source-location.test/00000000-0000-0000-0000-000000000000",
"blob",
),
]
.into_iter()
.enumerate()
{
let mut vm = new_storage_test_vm("https://eval-source-location.test/page.html");
vm.set_response_content_security_policies(&["script-src data: blob:".to_owned()]);
vm.eval(
r#"
globalThis.__evalCspViolations = [];
document.addEventListener("securitypolicyviolation", event => {
globalThis.__evalCspViolations.push({
blockedURI: event.blockedURI,
sourceFile: event.sourceFile,
linePositive: event.lineNumber > 0,
columnPositive: event.columnNumber > 0,
});
});
"#,
)
.expect("eval CSP observer should install");
let script_url = Url::parse(script_url).expect("external script URL");
let script = PreparedScript {
position: index,
node_id: NodeId::new(index + 1),
kind: ScriptKind::Classic,
mode: ScriptMode::Async,
source_kind: ScriptSourceKind::External,
fetch_metadata: crate::planning::ScriptFetchMetadata::default(),
source: ScriptSource::External,
url: script_url.clone(),
base_url: script_url,
initiator_url: Url::parse("https://eval-source-location.test/page.html")
.expect("initiator URL"),
host_script_handle: None,
};
vm.execute_loaded_prepared_script_source(&script, source, None)
.await
.expect("external script with blocked eval should complete");
assert_eq!(
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm),
1
);
assert_eq!(
vm.eval(
r#"JSON.stringify({
errorName: globalThis.__evalCspErrorName,
ran: globalThis.__blockedEvalRan === true,
violations: globalThis.__evalCspViolations,
})"#
)
.expect("eval CSP result should remain observable"),
format!(
r#"{{"errorName":"EvalError","ran":false,"violations":[{{"blockedURI":"eval","sourceFile":"{expected_source_file}","linePositive":true,"columnPositive":true}}]}}"#
)
);
}
}