From d2db161d331e70643e3f3e34109f4e935bf8d98f Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 25 Oct 2026 22:22:08 +0800 Subject: [PATCH] fix(csp): preserve Document response status in violations --- .../tests/scripts/csp_response_status.rs | 127 ++++++++++++++++++ moli-core/tests/scripts/integrity.rs | 4 + moli-csp/src/types.rs | 1 + moli-dom/src/native/document.rs | 9 ++ moli-dom/src/native/host/clone.rs | 22 +++ moli-dom/src/native/host/document.rs | 27 ++++ .../src/document_runtime/runtime_core.rs | 7 + .../src/document_runtime/security_policy.rs | 41 ++++-- .../context_host/child_documents/commit.rs | 5 + .../context_host/child_documents/loads.rs | 8 +- .../context_host/child_documents/snapshots.rs | 1 + .../context_host/child_frame_snapshots.rs | 7 + .../context_host/misc_platform_api_tasks.rs | 10 +- .../src/native_bridge/context_host/popups.rs | 27 +++- .../context_host/popups/renderer_owned.rs | 1 + .../context_host/security_policy.rs | 27 +++- .../src/network_host/csp_reports.rs | 25 +++- .../page_task_queue/resource_completions.rs | 2 + .../src/runtime/owner/document_response.rs | 2 + .../src/runtime/owner/page_creation.rs | 2 + .../runtime/page_vm/followed_navigation.rs | 1 + moli-renderer-v8/src/runtime/page_vm/mod.rs | 9 +- .../src/runtime/page_vm/test_support.rs | 1 + .../tests/child_document_completion.rs | 1 + .../tests/extracted/page_task_dispatch.rs | 1 + .../src/runtime/page_vm/tests/mod.rs | 1 + .../tests/popup_document_completion.rs | 1 + moli-renderer-v8/src/runtime/phase_one/mod.rs | 1 + .../src/runtime/phase_one/streaming.rs | 3 +- .../extracted/parser_blocking_scripts.rs | 4 + .../tests/extracted/parser_script_handoffs.rs | 6 + .../tree_mutations_and_document_write.rs | 1 + moli-renderer-v8/src/types.rs | 2 + 33 files changed, 359 insertions(+), 28 deletions(-) create mode 100644 moli-core/tests/scripts/csp_response_status.rs diff --git a/moli-core/tests/scripts/csp_response_status.rs b/moli-core/tests/scripts/csp_response_status.rs new file mode 100644 index 0000000000..135bf0c6e1 --- /dev/null +++ b/moli-core/tests/scripts/csp_response_status.rs @@ -0,0 +1,127 @@ +use super::*; + +pub(super) fn fixture_response(query: &str) -> (&'static str, String, String) { + let status = match query { + "201" => "201 Created", + "404" => "404 Not Found", + _ => "200 OK", + }; + ( + status, + "Content-Type: text/html\r\nContent-Security-Policy: connect-src 'none'\r\nCache-Control: no-store\r\n".into(), + "CSP response metadata".into(), + ) +} + +const PROBE: &str = r#"async function probe(view) { + const reported = new Promise(resolve => { + view.document.addEventListener('securitypolicyviolation', resolve, {once:true}); + }); + await view.fetch('/blocked-csp-response-status').then( + () => {throw new Error('CSP must prevent transport');}, + error => {if (!(error instanceof view.TypeError)) throw error;} + ); + const event = await reported; + return [event.statusCode, event instanceof view.SecurityPolicyViolationEvent, + event.isTrusted, event.target === view.document, event.effectiveDirective]; +}"#; + +#[tokio::test(flavor = "multi_thread")] +async fn document_csp_response_status_retains_navigation_metadata_through_open() -> Result<()> { + let servers = IntegrityServers::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + for status in [201, 404] { + let mut page = browser + .fetch_allow_http_error(&format!( + "{}/csp-response-status.html?{status}", + servers.origin + )) + .await?; + let expression = format!( + r#"(async () => {{ + const probe = {PROBE}; + const before = await probe(window); + document.open(); + document.write('replaced'); + document.close(); + return [before, await probe(window)]; + }})().then(JSON.stringify)"# + ); + let result = tokio::time::timeout( + Duration::from_secs(10), + page.evaluate_runtime_expression_with_await_async(&expression, true), + ) + .await??; + let observed: serde_json::Value = + serde_json::from_str(result["value"].as_str().expect("CSP observations"))?; + assert_eq!( + observed, + serde_json::json!([ + [status, true, true, true, "connect-src"], + [status, true, true, true, "connect-src"] + ]) + ); + } + assert!( + servers + .requests + .lock() + .iter() + .all(|request| { !request.path.starts_with("/blocked-csp-response-status") }) + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_csp_response_status_uses_its_response_without_inheriting_parent_status() -> Result<()> +{ + let servers = IntegrityServers::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let mut page = browser + .fetch(&format!("{}/csp-response-status.html?201", servers.origin)) + .await?; + let expression = format!( + r#"(async () => {{ + const probe = {PROBE}; + const rows = []; + for (const source of ['/csp-response-status.html?404', 'about:blank', 'srcdoc']) {{ + const frame = document.createElement('iframe'); + if (source === 'srcdoc') frame.srcdoc = 'srcdoc'; + else frame.src = source; + await new Promise(resolve => {{frame.onload = resolve; document.body.append(frame);}}); + try {{rows.push(await probe(frame.contentWindow));}} + finally {{frame.remove();}} + }} + const popup = open(); + try {{rows.push(await probe(popup));}} + finally {{popup.close();}} + rows.push(await probe(window)); + return rows; + }})().then(JSON.stringify)"# + ); + let result = tokio::time::timeout( + Duration::from_secs(10), + page.evaluate_runtime_expression_with_await_async(&expression, true), + ) + .await??; + let observed: serde_json::Value = + serde_json::from_str(result["value"].as_str().expect("child CSP observations"))?; + assert_eq!( + observed, + serde_json::json!([ + [404, true, true, true, "connect-src"], + [0, true, true, true, "connect-src"], + [0, true, true, true, "connect-src"], + [0, true, true, true, "connect-src"], + [201, true, true, true, "connect-src"] + ]) + ); + assert!( + servers + .requests + .lock() + .iter() + .all(|request| { !request.path.starts_with("/blocked-csp-response-status") }) + ); + Ok(()) +} diff --git a/moli-core/tests/scripts/integrity.rs b/moli-core/tests/scripts/integrity.rs index 6ce4e2279f..6cf2cb9f35 100644 --- a/moli-core/tests/scripts/integrity.rs +++ b/moli-core/tests/scripts/integrity.rs @@ -56,6 +56,9 @@ mod local_worker_referrer; #[path = "service_worker_referrer.rs"] mod service_worker_referrer; +#[path = "csp_response_status.rs"] +mod csp_response_status; + struct IntegrityServers { origin: String, cross_origin: String, @@ -216,6 +219,7 @@ fn fixture_response( "/document-referrer.html" | "/document-referrer.js" => { module_document_referrer::fixture_response(path, query, origin, cross) } + "/csp-response-status.html" => csp_response_status::fixture_response(query), "/import-referrer.js" => dynamic_import_referrer::fixture_response(query), "/local-referrer-worker.js" | "/local-referrer-page.html" => { local_worker_referrer::fixture_response(path, query, request.referer.as_deref()) diff --git a/moli-csp/src/types.rs b/moli-csp/src/types.rs index d6536eb5d6..19ffd54a49 100644 --- a/moli-csp/src/types.rs +++ b/moli-csp/src/types.rs @@ -140,6 +140,7 @@ impl ContentSecurityPolicyReportingEndpoints { } } +#[derive(Clone, Copy)] pub struct ContentSecurityPolicyViolationEventFields<'a> { pub document_uri: &'a str, pub referrer: &'a str, diff --git a/moli-dom/src/native/document.rs b/moli-dom/src/native/document.rs index b6743191d7..ce56537108 100644 --- a/moli-dom/src/native/document.rs +++ b/moli-dom/src/native/document.rs @@ -89,6 +89,7 @@ struct DocumentMetadata { // Document identity. Only a whole-arena snapshot may copy them unchanged. #[derive(Debug, Clone, Default)] struct DocumentArenaState { + source_response_status: Option, // Meta delivery updates policy in time order; removing the element does // not undo the policy it delivered to this Document. meta_referrer_policy: MetaReferrerPolicy, @@ -263,6 +264,10 @@ impl Document { self.metadata.source_last_modified_ms } + pub fn source_response_status(&self) -> Option { + self.arena_state.source_response_status + } + pub fn quirks_mode(&self) -> QuirksMode { self.metadata.quirks_mode } @@ -374,6 +379,10 @@ impl Document { self.metadata.source_last_modified_ms = timestamp_ms; } + pub fn set_source_response_status(&mut self, status: Option) { + self.arena_state.source_response_status = status; + } + pub fn set_quirks_mode(&mut self, quirks_mode: QuirksMode) { self.metadata.quirks_mode = quirks_mode; } diff --git a/moli-dom/src/native/host/clone.rs b/moli-dom/src/native/host/clone.rs index 0a4973df7d..b1a63d0f0a 100644 --- a/moli-dom/src/native/host/clone.rs +++ b/moli-dom/src/native/host/clone.rs @@ -642,6 +642,28 @@ mod tests { assert_eq!(host.owner_document_handle(template), Some(destination)); } + #[test] + fn document_response_metadata_survives_stream_replacement_but_not_cloning() { + let mut host = test_host(); + let document = host.document_handle(); + assert_eq!( + host.document_source_response_status_for_handle(document), + None + ); + assert!(host.set_document_source_response_status_for_handle(document, Some(404))); + host.reset_html_document_shell(); + assert_eq!( + host.document_source_response_status_for_handle(document), + Some(404) + ); + let clone = host.clone_node(document, true).unwrap(); + assert_eq!(host.document_source_response_status_for_handle(clone), None); + assert_eq!( + host.document_source_response_status_for_handle(document), + Some(404) + ); + } + #[test] fn element_copies_preserve_native_identity_and_target_template_document() { const HTML: &str = "http://www.w3.org/1999/xhtml"; diff --git a/moli-dom/src/native/host/document.rs b/moli-dom/src/native/host/document.rs index 4ddd4493d0..c225c35287 100644 --- a/moli-dom/src/native/host/document.rs +++ b/moli-dom/src/native/host/document.rs @@ -1229,6 +1229,15 @@ impl DomHost { .and_then(Document::source_last_modified_ms) } + pub fn document_source_response_status_for_handle( + &self, + document_handle: DomHandle, + ) -> Option { + self.node(document_handle) + .and_then(Node::as_document) + .and_then(Document::source_response_status) + } + pub fn document_default_language_for_node(&self, handle: DomHandle) -> Option { let document_handle = self.owner_document_handle(handle)?; self.document_meta_default_language_for_handle(document_handle) @@ -1420,6 +1429,24 @@ impl DomHost { true } + pub fn set_document_source_response_status_for_handle( + &mut self, + document_handle: DomHandle, + status: Option, + ) -> bool { + let Some(document) = self + .node_mut(document_handle) + .and_then(|node| node.data_mut().as_document_mut()) + else { + return false; + }; + if document.source_response_status() == status { + return false; + } + document.set_source_response_status(status); + true + } + pub fn document_target_element(&self, document_handle: DomHandle) -> Option { let target = self .node(document_handle) diff --git a/moli-renderer-v8/src/document_runtime/runtime_core.rs b/moli-renderer-v8/src/document_runtime/runtime_core.rs index d053eb9512..02cdc4db50 100644 --- a/moli-renderer-v8/src/document_runtime/runtime_core.rs +++ b/moli-renderer-v8/src/document_runtime/runtime_core.rs @@ -343,6 +343,13 @@ impl DocumentRuntime { .set_document_source_last_modified_for_handle(document_handle, timestamp_ms); } + pub(crate) fn set_document_source_response_status(&mut self, status: Option) { + let document_handle = self.dom_host.document_handle(); + let _ = self + .dom_host + .set_document_source_response_status_for_handle(document_handle, status); + } + pub(crate) fn take_style_source_document_sync_pending(&mut self) -> bool { std::mem::take(&mut self.style_source_document_sync_pending) } diff --git a/moli-renderer-v8/src/document_runtime/security_policy.rs b/moli-renderer-v8/src/document_runtime/security_policy.rs index 956c94693b..e7d691687e 100644 --- a/moli-renderer-v8/src/document_runtime/security_policy.rs +++ b/moli-renderer-v8/src/document_runtime/security_policy.rs @@ -2280,7 +2280,12 @@ impl DocumentRuntime { .current_main_document_task_owner() .ok_or_else(|| anyhow::anyhow!("main CSP violation document owner is unavailable"))?; if send_report { - let fields = ContentSecurityPolicyViolationEventFields::from(violation); + let fields = self.content_security_policy_violation_event_fields( + target + .and_then(|target| self.dom_host.owner_document_handle(target)) + .unwrap_or_else(|| self.document_handle()), + violation, + ); let report_owner = event_owner.unwrap_or(document_owner); let child_handle = event_owner.and(target).and_then(|document| { host.child_browsing_context_host_for_document_handle(document) @@ -2361,11 +2366,19 @@ impl DocumentRuntime { }) .unwrap_or_else(|| scope.get_current_context()); let scope = &mut v8::ContextScope::new(scope, context); + let document = match event_target { + EventTargetHandle::Node(handle) => self + .dom_host + .owner_document_handle(handle) + .unwrap_or_else(|| self.document_handle()), + _ => self.document_handle(), + }; + let fields = self.content_security_policy_violation_event_fields(document, violation); let event = create_content_security_policy_violation_event( scope, event_target_value, event_target_value, - violation, + &fields, ) .map_err(anyhow::Error::msg)?; self.dispatch_public_event_best_effort( @@ -2379,6 +2392,21 @@ impl DocumentRuntime { Ok(()) } + pub(crate) fn content_security_policy_violation_event_fields<'a>( + &self, + document: DomHandle, + violation: &'a DocumentContentSecurityPolicyViolation, + ) -> ContentSecurityPolicyViolationEventFields<'a> { + ContentSecurityPolicyViolationEventFields { + status_code: i32::from( + self.dom_host + .document_source_response_status_for_handle(document) + .unwrap_or(0), + ), + ..ContentSecurityPolicyViolationEventFields::from(violation) + } + } + fn document_content_security_policies_for_optional_document( &self, document_handle: Option, @@ -3038,13 +3066,10 @@ pub(crate) fn create_content_security_policy_violation_event<'s>( scope: &mut v8::PinScope<'s, '_>, current_target: v8::Local<'s, v8::Value>, target: v8::Local<'s, v8::Value>, - violation: &DocumentContentSecurityPolicyViolation, + fields: &ContentSecurityPolicyViolationEventFields<'_>, ) -> anyhow::Result> { - let event = create_security_policy_violation_event( - scope, - &ContentSecurityPolicyViolationEventFields::from(violation), - ) - .ok_or_else(|| anyhow::anyhow!("failed to create CSP violation event"))?; + let event = create_security_policy_violation_event(scope, fields) + .ok_or_else(|| anyhow::anyhow!("failed to create CSP violation event"))?; let _ = crate::context_bootstrap::event_backing(scope, event).set( scope, v8str(scope, "target").into(), diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs index 59d15d3c25..4a8ff43e8d 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs @@ -203,6 +203,11 @@ impl JsContextHost { }; self.dom_host_mut() .set_document_character_set_for_handle(document_handle, &snapshot.character_set); + self.dom_host_mut() + .set_document_source_response_status_for_handle( + document_handle, + snapshot.response_status, + ); self.dom_host_mut() .set_document_allow_declarative_shadow_roots_for_handle(document_handle, true); // Navigation-created Documents start loading before parser scripts can diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs index 7d05929b50..d74a1c56ee 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/loads.rs @@ -501,8 +501,10 @@ impl JsContextHost { // SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime. unsafe { &mut *self.runtime } .record_content_security_policy_inspector_issue(Some(handle), violation); - let fields = - ContentSecurityPolicyViolationEventFields::from_url_violation(violation); + let fields = ContentSecurityPolicyViolationEventFields { + status_code: i32::from(loaded.response_status.unwrap_or(0)), + ..ContentSecurityPolicyViolationEventFields::from_url_violation(violation) + }; send_content_security_policy_reports( &reports, pending.resource_loader.request_client(), @@ -710,6 +712,7 @@ impl JsContextHost { loaded.content_type, loaded.character_set, ) + .with_response_status(loaded.response_status) .with_resource_was_cached(resource_was_cached); self.cache_child_snapshot_with_current_document_policy(handle, snapshot) } @@ -1024,6 +1027,7 @@ fn child_document_load_outcome_from_response( }; Ok(ChildDocumentLoadOutcome::Loaded(Box::new( LoadedChildDocument { + response_status: Some(head.status), response_links: crate::response_links::DocumentResponseLinks::parse( &head.final_url, &head.headers, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs index a6ee47706c..eb7c1e30e6 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs @@ -106,6 +106,7 @@ impl JsContextHost { content_type, character_set, ) + .with_response_status(Some(head.status)) .with_policy_container(policy_container), )) } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_snapshots.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_snapshots.rs index f4469d18e3..ec31cc3aed 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_snapshots.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_snapshots.rs @@ -55,6 +55,7 @@ pub(crate) struct ChildBrowsingContextSnapshot { pub(crate) resource_was_cached: bool, pub(crate) content_type: Option, pub(crate) character_set: String, + pub(crate) response_status: Option, pub(crate) policy_container: DocumentPolicyContainer, } @@ -89,6 +90,7 @@ impl ChildBrowsingContextSnapshot { resource_was_cached: false, content_type, character_set: character_set.into(), + response_status: None, policy_container: DocumentPolicyContainer::default(), } } @@ -126,6 +128,11 @@ impl ChildBrowsingContextSnapshot { self } + pub(crate) fn with_response_status(mut self, status: Option) -> Self { + self.response_status = status; + self + } + pub(crate) fn with_policy_container( mut self, policy_container: DocumentPolicyContainer, diff --git a/moli-renderer-v8/src/native_bridge/context_host/misc_platform_api_tasks.rs b/moli-renderer-v8/src/native_bridge/context_host/misc_platform_api_tasks.rs index 05cfc92cfc..c3794b849e 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/misc_platform_api_tasks.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/misc_platform_api_tasks.rs @@ -265,13 +265,15 @@ impl JsContextHost { }) .unwrap_or(resolved.document_handle); let target = EventTargetHandle::Node(event_target); + let fields = self.content_security_policy_violation_event_fields( + resolved.document_handle, + &violation, + ); crate::host::event_target_value(scope, host_ptr, target) .ok() .and_then(|value| { - create_content_security_policy_violation_event( - scope, value, value, &violation, - ) - .ok() + create_content_security_policy_violation_event(scope, value, value, &fields) + .ok() }) .is_some_and(|event| { // SAFETY: the Host and its DocumentRuntime share one ScriptVm. diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index 660579eeb4..767fd19f59 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -289,6 +289,7 @@ enum LightweightPopupClassicScriptAdvance { } struct LightweightPopupDocumentProjectionSource<'a> { + response_status: Option, response_links: &'a crate::response_links::DocumentResponseLinks, task: LightweightPopupNavigationTaskToken, document_url: &'a Url, @@ -2997,6 +2998,7 @@ impl JsContextHost { policy_container.response_content_security_policies.clone(); } let loaded = LoadedChildDocument { + response_status: snapshot.response_status, response_links: Default::default(), final_url: snapshot.url, policy_container, @@ -3089,6 +3091,7 @@ impl JsContextHost { ); Ok(PopupDocumentLoadOutcome::Loaded(Box::new( LoadedChildDocument { + response_status: Some(head.status), response_links: crate::response_links::DocumentResponseLinks::parse( &head.final_url, &head.headers, @@ -3333,6 +3336,7 @@ impl JsContextHost { scope, window, LightweightPopupDocumentProjectionSource { + response_status: loaded.response_status, response_links: &loaded.response_links, task, document_url: &final_url, @@ -3795,11 +3799,18 @@ impl JsContextHost { let Some(window) = self.lightweight_popup_window(scope, popup_id) else { return; }; + let fields = crate::content_security_policy::ContentSecurityPolicyViolationEventFields { + status_code: i32::from( + self.content_security_policy_response_status( + super::OwnerDispatchScope::LightweightPopup(popup_id), + ) + .unwrap_or(0), + ), + ..crate::content_security_policy::ContentSecurityPolicyViolationEventFields::from( + violation, + ) + }; if send_report { - let fields = - crate::content_security_policy::ContentSecurityPolicyViolationEventFields::from( - violation, - ); let Some(document_owner) = self.current_lightweight_popup_document_owner(popup_id) else { tracing::debug!(popup_id, "discarded CSP report for retired popup document"); @@ -3819,7 +3830,7 @@ impl JsContextHost { scope, window.into(), window.into(), - violation, + &fields, ) { Ok(event) => { self.dispatch_lightweight_popup_window_event( @@ -3985,6 +3996,11 @@ impl JsContextHost { let document_handle = self.remember_lightweight_popup_document_handle(scope, popup_id, document); if let Some(document_handle) = document_handle { + self.dom_host_mut() + .set_document_source_response_status_for_handle( + document_handle, + source.response_status, + ); let _ = crate::context_bootstrap::install_css_runtime_state_for_document( scope, window, @@ -5130,6 +5146,7 @@ impl JsContextHost { scope, window, LightweightPopupDocumentProjectionSource { + response_status: None, response_links: &Default::default(), task, document_url: &document_url, diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs index 8c57c85a59..bedffb4967 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs @@ -292,6 +292,7 @@ impl JsContextHost { document_policy_container: policy.clone(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: moli_page_types::DocumentSettings { script_execution_disabled: !policy.sandbox.allows_scripts, debugger_enabled: self.dom_debugger_state.debugger_pauses_enabled(), 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 52bd1ac4fe..4d54c2c7b6 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 @@ -1791,6 +1791,23 @@ impl JsContextHost { } } + pub(crate) fn content_security_policy_response_status( + &self, + owner: OwnerDispatchScope, + ) -> Option { + let document = match owner { + OwnerDispatchScope::Top => Some(self.document_handle()), + OwnerDispatchScope::Child(handle) => { + self.child_browsing_context_document_handle(handle) + } + OwnerDispatchScope::LightweightPopup(popup_id) => { + self.lightweight_popup_document_handle(popup_id) + } + }?; + self.dom_host() + .document_source_response_status_for_handle(document) + } + fn dispatch_child_content_security_policy_violation_event<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, @@ -1801,14 +1818,20 @@ impl JsContextHost { let window = self .child_browsing_context_window_wrapper(scope, handle) .ok_or_else(|| anyhow::anyhow!("child window wrapper is unavailable"))?; + let fields = ContentSecurityPolicyViolationEventFields { + status_code: i32::from( + self.content_security_policy_response_status(OwnerDispatchScope::Child(handle)) + .unwrap_or(0), + ), + ..ContentSecurityPolicyViolationEventFields::from(violation) + }; let event = create_content_security_policy_violation_event( scope, window.into(), window.into(), - violation, + &fields, )?; if send_report { - let fields = ContentSecurityPolicyViolationEventFields::from(violation); let document_owner = self.current_child_document_task_owner(handle) .ok_or_else(|| { diff --git a/moli-renderer-v8/src/network_host/csp_reports.rs b/moli-renderer-v8/src/network_host/csp_reports.rs index 8320d8e180..0e15b078f2 100644 --- a/moli-renderer-v8/src/network_host/csp_reports.rs +++ b/moli-renderer-v8/src/network_host/csp_reports.rs @@ -31,19 +31,23 @@ pub(crate) fn send_content_security_policy_reports_for_window( else { return; }; + let fields = ContentSecurityPolicyViolationEventFields { + status_code: i32::from(request_context.response_status), + ..*fields + }; if let Some(execution_owner) = host.current_window_execution_context_owner(owner.dispatch_scope) && let Some((_, context)) = host.window_execution_context(scope, execution_owner, owner.dispatch_scope) { let scope = &mut v8::ContextScope::new(scope, context); crate::content_security_policy::notify_content_security_policy_reporting_observers( - scope, fields, + scope, &fields, ); } send_content_security_policy_reports_from_window_context( host, &request_context, - fields, + &fields, report_uri_endpoints, report_to_endpoints, ); @@ -67,19 +71,23 @@ pub(crate) fn send_content_security_policy_reports_for_lightweight_popup( else { return; }; + let fields = ContentSecurityPolicyViolationEventFields { + status_code: i32::from(request_context.response_status), + ..*fields + }; if let Some(execution_owner) = host.current_window_execution_context_owner(owner.dispatch_scope) && let Some((_, context)) = host.window_execution_context(scope, execution_owner, owner.dispatch_scope) { let scope = &mut v8::ContextScope::new(scope, context); crate::content_security_policy::notify_content_security_policy_reporting_observers( - scope, fields, + scope, &fields, ); } send_content_security_policy_reports_from_window_context( host, &request_context, - fields, + &fields, report_uri_endpoints, report_to_endpoints, ); @@ -118,6 +126,7 @@ pub(crate) struct WindowCspReportRequestContext { request_client: ResourceRequestClient, frame_id: Option, document_url: url::Url, + response_status: u16, request_origin: moli_url::WebOrigin, network_partition_key: Option, policy_context: crate::types::SubresourcePolicyContext, @@ -208,7 +217,10 @@ pub(crate) fn send_content_security_policy_violation_report_from_window_context( request_context: &WindowCspReportRequestContext, violation: &crate::document_runtime::DocumentContentSecurityPolicyViolation, ) { - let fields = ContentSecurityPolicyViolationEventFields::from(violation); + let fields = ContentSecurityPolicyViolationEventFields { + status_code: i32::from(request_context.response_status), + ..ContentSecurityPolicyViolationEventFields::from(violation) + }; send_content_security_policy_reports_from_window_context( host, request_context, @@ -552,6 +564,9 @@ fn window_csp_report_request_context_for_identity( resource_loader, frame_id, document_url, + response_status: host + .content_security_policy_response_status(identity.dispatch_scope()) + .unwrap_or(0), request_origin, network_partition_key: active_subresource_network_partition_key( host, diff --git a/moli-renderer-v8/src/page_task_queue/resource_completions.rs b/moli-renderer-v8/src/page_task_queue/resource_completions.rs index e1e467ffa5..ef45001f9c 100644 --- a/moli-renderer-v8/src/page_task_queue/resource_completions.rs +++ b/moli-renderer-v8/src/page_task_queue/resource_completions.rs @@ -1186,6 +1186,7 @@ mod tests { moli_dom::NodeId::new(7), Ok(ChildDocumentLoadOutcome::Loaded(Box::new( LoadedChildDocument { + response_status: None, response_links: Default::default(), final_url: Url::parse("https://example.test/child").unwrap(), policy_container: crate::document_runtime::DocumentPolicyContainer::default(), @@ -1212,6 +1213,7 @@ mod tests { ), Ok(PopupDocumentLoadOutcome::Loaded(Box::new( LoadedChildDocument { + response_status: None, response_links: Default::default(), final_url: Url::parse("https://example.test/popup").unwrap(), policy_container: crate::document_runtime::DocumentPolicyContainer::default(), diff --git a/moli-renderer-v8/src/runtime/owner/document_response.rs b/moli-renderer-v8/src/runtime/owner/document_response.rs index 28ea649591..0522db8070 100644 --- a/moli-renderer-v8/src/runtime/owner/document_response.rs +++ b/moli-renderer-v8/src/runtime/owner/document_response.rs @@ -121,6 +121,8 @@ impl RendererCreateStreamingRawPageRequest { history_policy_container, document_default_language, document_last_modified, + document_response_status: (!moli_url::is_about_blank(&final_url)) + .then_some(response_status), document_settings, network_offline, blocked_url_patterns, diff --git a/moli-renderer-v8/src/runtime/owner/page_creation.rs b/moli-renderer-v8/src/runtime/owner/page_creation.rs index 263bd41ff4..49f8e7dd23 100644 --- a/moli-renderer-v8/src/runtime/owner/page_creation.rs +++ b/moli-renderer-v8/src/runtime/owner/page_creation.rs @@ -937,6 +937,8 @@ impl RendererOwnerHandle { history_policy_container, document_default_language, document_last_modified, + document_response_status: (!moli_url::is_about_blank(&final_url)) + .then_some(response_status), document_settings, network_offline, blocked_url_patterns, diff --git a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs index 42ad0fa3c9..37cb40497c 100644 --- a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs @@ -1497,6 +1497,7 @@ impl PageVm { }, document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: moli_page_types::DocumentSettings { script_execution_disabled: self.script_execution_disabled(), ..self.document_settings.clone() diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index 166d9728e4..be66727803 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -1100,6 +1100,7 @@ pub(crate) struct PageVmEnvConfig { pub(crate) document_policy_container: crate::document_runtime::DocumentPolicyContainer, pub(crate) document_default_language: Option, pub(crate) document_last_modified: Option, + pub(crate) document_response_status: Option, pub(crate) document_settings: moli_page_types::DocumentSettings, pub(crate) network_offline: bool, pub(crate) blocked_url_patterns: Vec, @@ -1138,11 +1139,14 @@ impl PageVmEnvConfig { .collect() } - pub(crate) fn apply_navigation_response_headers( + pub(crate) fn apply_navigation_response_metadata( &mut self, final_url: &Url, + status: u16, headers: &[(String, Vec)], ) { + // Blank Documents inherit policy, but have no fetched response. + self.document_response_status = (!moli_url::is_about_blank(final_url)).then_some(status); self.about_document_state = self .about_document_state .take() @@ -4672,6 +4676,9 @@ impl PageVm { self.vm_mut() .document_runtime .set_document_source_last_modified(env.document_last_modified); + self.vm_mut() + .document_runtime + .set_document_source_response_status(env.document_response_status); } self.vm_mut() .set_stored_document_start_scripts(&env.document_start_scripts); diff --git a/moli-renderer-v8/src/runtime/page_vm/test_support.rs b/moli-renderer-v8/src/runtime/page_vm/test_support.rs index f49843a4bd..38c457582f 100644 --- a/moli-renderer-v8/src/runtime/page_vm/test_support.rs +++ b/moli-renderer-v8/src/runtime/page_vm/test_support.rs @@ -729,6 +729,7 @@ fn minimal_test_page_vm_env_config() -> PageVmEnvConfig { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/child_document_completion.rs b/moli-renderer-v8/src/runtime/page_vm/tests/child_document_completion.rs index 606386c424..4c4f758792 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/child_document_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/child_document_completion.rs @@ -988,6 +988,7 @@ pub(super) fn stale_loaded_completion( ChildDocumentLoadNetworkAttribution::new(frame_id.to_owned(), None, loader_id), Ok(ChildDocumentLoadOutcome::Loaded(Box::new( LoadedChildDocument { + response_status: None, response_links: Default::default(), final_url: Url::parse(request_url).expect("final URL"), policy_container: crate::document_runtime::DocumentPolicyContainer::default(), diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/extracted/page_task_dispatch.rs b/moli-renderer-v8/src/runtime/page_vm/tests/extracted/page_task_dispatch.rs index cdec1b0b52..18c6449dfd 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/extracted/page_task_dispatch.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/extracted/page_task_dispatch.rs @@ -25,6 +25,7 @@ fn default_runtime_hooks_reject_direct_no_owner_page_vm_construction() { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index ee5e79b316..44c752da39 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -709,6 +709,7 @@ fn test_page_vm_with_loader_dom_host_hooks_and_response_referrer_policy( }, document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/popup_document_completion.rs b/moli-renderer-v8/src/runtime/page_vm/tests/popup_document_completion.rs index fd76136652..c31e60a47a 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/popup_document_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/popup_document_completion.rs @@ -77,6 +77,7 @@ fn loaded_popup_completion( target, Ok(PopupDocumentLoadOutcome::Loaded(Box::new( LoadedChildDocument { + response_status: None, response_links: Default::default(), final_url: Url::parse(final_url).expect("popup completion final URL"), policy_container: crate::document_runtime::DocumentPolicyContainer::default(), diff --git a/moli-renderer-v8/src/runtime/phase_one/mod.rs b/moli-renderer-v8/src/runtime/phase_one/mod.rs index 15e9ead0f3..5b6928f0fb 100644 --- a/moli-renderer-v8/src/runtime/phase_one/mod.rs +++ b/moli-renderer-v8/src/runtime/phase_one/mod.rs @@ -695,6 +695,7 @@ mod tests { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/phase_one/streaming.rs b/moli-renderer-v8/src/runtime/phase_one/streaming.rs index c15d4dc8f2..17dc86ea83 100644 --- a/moli-renderer-v8/src/runtime/phase_one/streaming.rs +++ b/moli-renderer-v8/src/runtime/phase_one/streaming.rs @@ -277,7 +277,7 @@ impl ConcurrentParseTimeRuntime { ) -> Result { debug_assert!(!response_headers_indicate_download(&response_headers)); let mut env = env.clone(); - env.apply_navigation_response_headers(&final_url, &response_headers); + env.apply_navigation_response_metadata(&final_url, response_status, &response_headers); let content_type = content_type.or_else(|| { moli_web_mime::navigation_response_mime_type_from_headers(&response_headers) @@ -1313,6 +1313,7 @@ mod tests { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_blocking_scripts.rs b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_blocking_scripts.rs index bb299d6c99..96c5aa5b71 100644 --- a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_blocking_scripts.rs +++ b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_blocking_scripts.rs @@ -657,6 +657,7 @@ fn parser_owner_boundary_with_live_backend_queues_document_turn_before_runtime_w document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -951,6 +952,7 @@ fn parser_connected_head_script_does_not_push_later_head_tokens_into_body() { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -1132,6 +1134,7 @@ fn parser_connected_external_head_script_with_live_head_and_body_mutation_keeps_ document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -1737,6 +1740,7 @@ fn parser_owner_body_stylesheet_pause_preserves_unconsumed_tail_on_live_page_vm( document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_script_handoffs.rs b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_script_handoffs.rs index cdf461f2ea..37fe7d621d 100644 --- a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_script_handoffs.rs +++ b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/parser_script_handoffs.rs @@ -354,6 +354,7 @@ fn external_async_handoff_marks_parser_stream_already_started_on_live_backend() document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -481,6 +482,7 @@ fn blocking_classic_handoff_registers_parser_owned_handle_on_live_backend() { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -622,6 +624,7 @@ fn non_async_post_parse_handoff_registers_pending_before_source_and_seals_withou document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -797,6 +800,7 @@ fn parser_owned_external_module_handoff_starts_pending_script_tree_root_fetch() document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -1117,6 +1121,7 @@ fn parser_owned_inline_importmap_handoff_registers_parser_owned_handle_on_live_b document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), @@ -1285,6 +1290,7 @@ fn parser_owner_style_import_handoff_is_stylesheet_gated_on_live_page_vm() { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs index b6b351ae42..df96f3eada 100644 --- a/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs +++ b/moli-renderer-v8/src/runtime/phase_one/tests/extracted/tree_mutations_and_document_write.rs @@ -1142,6 +1142,7 @@ fn parser_connected_head_document_write_keeps_later_head_tokens_in_head() { document_policy_container: Default::default(), document_default_language: None, document_last_modified: None, + document_response_status: None, document_settings: Default::default(), network_offline: false, blocked_url_patterns: Vec::new(), diff --git a/moli-renderer-v8/src/types.rs b/moli-renderer-v8/src/types.rs index 6097d94a05..21aae0a86b 100644 --- a/moli-renderer-v8/src/types.rs +++ b/moli-renderer-v8/src/types.rs @@ -1759,6 +1759,7 @@ impl ChildModulepreloadFetchCompletion { #[derive(Debug)] pub(super) struct LoadedChildDocument { + pub(super) response_status: Option, pub(super) response_links: crate::response_links::DocumentResponseLinks, pub(super) final_url: Url, pub(super) policy_container: DocumentPolicyContainer, @@ -1787,6 +1788,7 @@ impl ChildDocumentLoadOutcome { >, ) -> Self { Self::BlockedResponse(Box::new(LoadedChildDocument { + response_status: None, final_url, policy_container: DocumentPolicyContainer { required_content_security_policy,