diff --git a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs index ddd9f4e24d..b87712c450 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs @@ -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)) } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs index 5ac058326b..a39b238225 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/security_policy.rs @@ -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}}]}}"# + ) + ); + } +}