From 86d5b8485f9dbf74d7cbb6e033ec83046992fd5e Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 11 Sep 2026 20:42:08 +0800 Subject: [PATCH] fix(xhr): preserve response document URLs and requester origins Parse response documents with the final response URL in the XHR creation realm. Inherit the requester origin separately for CORS responses, and keep relative URL resolution consistent through base insertion/removal/adoption. Validated with cargo fmt, workspace clippy, and nextest (17991 passed, 13 skipped). All 935 contract checks pass; the 75-case WPT suite gains five passing subtests with no regressions. Chromium passes all 61 new checks. --- moli-renderer-v8/src/dom_parser.rs | 23 +---- .../src/network_host/xhr/delivery/response.rs | 85 +++++++++++++++---- .../src/script_vm/tests/dom_xhr/xhr.rs | 85 +++++++++++++++++++ 3 files changed, 155 insertions(+), 38 deletions(-) diff --git a/moli-renderer-v8/src/dom_parser.rs b/moli-renderer-v8/src/dom_parser.rs index 88c712d8fb..5d6cee8427 100644 --- a/moli-renderer-v8/src/dom_parser.rs +++ b/moli-renderer-v8/src/dom_parser.rs @@ -242,28 +242,7 @@ pub(crate) fn install_dom_parser_template_bindings<'s>( ); } -pub(super) fn parse_detached_document_from_string<'s>( - scope: &mut v8::PinScope<'s, '_>, - source: &str, - mime: &str, -) -> Option> { - let is_html = is_html_document_mime(mime); - let is_xml = is_dom_parser_xml_mime(mime); - if !is_html && !is_xml { - return None; - } - - let host_ptr = context_host_ptr_from_global_bridge(scope)?; - let runtime = unsafe { &*host_ptr }; - parse_detached_document_from_string_with_url( - scope, - runtime.document_url().clone(), - source, - mime, - ) -} - -fn parse_detached_document_from_string_with_url<'s>( +pub(super) fn parse_detached_document_from_string_with_url<'s>( scope: &mut v8::PinScope<'s, '_>, document_url: Url, source: &str, diff --git a/moli-renderer-v8/src/network_host/xhr/delivery/response.rs b/moli-renderer-v8/src/network_host/xhr/delivery/response.rs index 3a085bff3a..98021e87b1 100644 --- a/moli-renderer-v8/src/network_host/xhr/delivery/response.rs +++ b/moli-renderer-v8/src/network_host/xhr/delivery/response.rs @@ -223,8 +223,13 @@ fn apply_xhr_response_body( XmlHttpRequestResponseType::Document => { let mime = xhr_response_mime_essence(scope, xhr, &head.headers) .unwrap_or_else(|| "text/html".to_owned()); - let document = - parse_xhr_response_document(scope, body_text.as_deref().unwrap_or(""), Some(&mime)); + let document = parse_xhr_response_document( + scope, + xhr, + &head, + body_text.as_deref().unwrap_or(""), + Some(&mime), + ); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); document } @@ -242,7 +247,7 @@ fn apply_xhr_response_body( let document = parse_default_xhr_response_xml( scope, xhr, - &head.headers, + &head, body_text.as_deref().unwrap_or(""), ); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); @@ -283,14 +288,13 @@ fn apply_xhr_response_text( XmlHttpRequestResponseType::Document => { let mime = xhr_response_mime_essence(scope, xhr, &head.headers) .unwrap_or_else(|| "text/html".to_owned()); - let document = parse_xhr_response_document(scope, &body_text, Some(&mime)); + let document = parse_xhr_response_document(scope, xhr, &head, &body_text, Some(&mime)); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); document } XmlHttpRequestResponseType::Default | XmlHttpRequestResponseType::Text => { if response_type == XmlHttpRequestResponseType::Default { - let document = - parse_default_xhr_response_xml(scope, xhr, &head.headers, &body_text); + let document = parse_default_xhr_response_xml(scope, xhr, &head, &body_text); set_xhr_state_value(scope, xhr, XHR_RESPONSE_XML_SLOT, document); } v8_string(scope, &body_text) @@ -416,11 +420,7 @@ fn set_xhr_response_head( XHR_STATUS_TEXT_SLOT, status_text.unwrap_or_else(|| http_status_text(head.status)), ); - let response_url = head - .redirect_chain - .last() - .map(|redirect| &redirect.to_url) - .unwrap_or(&head.final_url); + let response_url = xhr_response_url(head); set_xhr_state_string(scope, xhr, XHR_RESPONSE_URL_SLOT, response_url.as_str()); // All delivery paths expose the same filtered header list, including @@ -593,15 +593,24 @@ fn xhr_response_mime_essence( fn parse_default_xhr_response_xml<'s>( scope: &mut v8::PinScope<'s, '_>, xhr: v8::Local<'_, v8::Object>, - headers: &[(String, String)], + head: &moli_fetch::ResponseHead, body_text: &str, ) -> v8::Local<'s, v8::Value> { - let mime = xhr_response_mime_essence(scope, xhr, headers); - parse_xhr_response_document(scope, body_text, mime.as_deref()) + let mime = xhr_response_mime_essence(scope, xhr, &head.headers); + parse_xhr_response_document(scope, xhr, head, body_text, mime.as_deref()) +} + +fn xhr_response_url(head: &moli_fetch::ResponseHead) -> &url::Url { + head.redirect_chain + .last() + .map(|redirect| &redirect.to_url) + .unwrap_or(&head.final_url) } fn parse_xhr_response_document<'s>( scope: &mut v8::PinScope<'s, '_>, + xhr: v8::Local<'_, v8::Object>, + head: &moli_fetch::ResponseHead, body_text: &str, mime: Option<&str>, ) -> v8::Local<'s, v8::Value> { @@ -610,11 +619,55 @@ fn parse_xhr_response_document<'s>( else { return v8::null(scope).into(); }; - dom_parser::parse_detached_document_from_string(scope, body_text, mime) - .map(|value| value.into()) + build_xhr_response_document(scope, xhr, xhr_response_url(head).clone(), body_text, mime) + .map(Into::into) .unwrap_or_else(|| v8::null(scope).into()) } +fn build_xhr_response_document<'s>( + scope: &mut v8::PinScope<'s, '_>, + xhr: v8::Local<'_, v8::Object>, + response_url: url::Url, + body_text: &str, + mime: &str, +) -> Option> { + let xhr = v8::Global::new(scope, xhr); + let xhr = v8::Local::new(scope, xhr); + let context = xhr.get_creation_context(scope)?; + let scope = &mut v8::ContextScope::new(scope, context); + let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host = unsafe { &*host_ptr }; + let binding = xhr_execution_context_binding(scope, host, xhr)?; + let origin_document_handle = match binding.dispatch_scope() { + crate::native_bridge::OwnerDispatchScope::Top => host.document_handle(), + crate::native_bridge::OwnerDispatchScope::Child(handle) => { + host.child_browsing_context_document_handle(handle)? + } + crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => { + host.lightweight_popup_document_handle(popup_id)? + } + }; + let origin_document = unsafe { &mut *host_ptr }.native_bridge_mut().wrap_handle( + scope, + host_ptr, + origin_document_handle, + )?; + let document = dom_parser::parse_detached_document_from_string_with_url( + scope, + response_url, + body_text, + mime, + )?; + // The response URL controls relative URL resolution; its origin does not + // replace the XHR environment's origin (including for CORS responses). + crate::native_bridge::document::inherit_detached_document_origin( + scope, + document, + origin_document, + ); + Some(document) +} + fn xhr_response_blob_mime_type( scope: &mut v8::PinScope<'_, '_>, xhr: v8::Local<'_, v8::Object>, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index da5a7c3303..4c4db32035 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs @@ -1079,6 +1079,91 @@ fn xml_http_request_default_response_type_parses_response_xml_for_document_mime( assert_eq!(result, "4|true|html|true|false|true"); } +#[test] +fn xml_http_request_response_document_uses_response_url_and_requester_origin() { + for child_realm in [false, true] { + for (mime, response_type) in [ + ("application/xml", ""), + ("application/xml", "document"), + ("text/html", "document"), + ] { + let mut vm = new_parsed_test_vm( + "https://requester.example/page/index.html", + "", + ); + vm.eval(&format!( + r#"(() => {{ + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + globalThis.__originRealm = {child_realm} ? frame.contentWindow : self; + globalThis.__documentXhr = new __originRealm.XMLHttpRequest(); + __documentXhr.open('GET', '/initial'); + __documentXhr.responseType = '{response_type}'; + }})()"# + )) + .expect("create XHR in its owning realm"); + let context_ptr: *const v8::Global = &vm.page_default_context; + vm.renderer_document_isolate.with_entered_renderer_document_isolate(move |isolate| { + let scope = std::pin::pin!(v8::HandleScope::new(isolate)); + let scope = &mut scope.init(); + let context = unsafe { v8::Local::new(scope, &*context_ptr) }; + let scope = &mut v8::ContextScope::new(scope, context); + let xhr = context.global(scope).get(scope, v8str(scope, "__documentXhr").into()) + .and_then(|value| v8::Local::::try_from(value).ok()).unwrap(); + crate::network_host::apply_xhr_response_body_source( + scope, + xhr, + moli_fetch::ResponseHead { + final_url: Url::parse("https://response.example/resource/doc#fragment").unwrap(), + status: 200, + headers: vec![("Content-Type".to_owned(), mime.to_owned())], + request_cookie_report: None, + cookie_set_reports: Vec::new(), + redirected: false, + redirect_chain: Vec::new(), + from_cache: false, + negotiated_http_version: None, + }, + moli_fetch::ResponseBody::materialized_bytes(br#"link"#.to_vec()), + ); + Ok(()) + }).expect("deliver response from a different origin in the parent realm"); + let result = vm + .eval( + r#"(() => { + const doc = __documentXhr.responseXML; + const check = (value, message) => { if (!value) throw new Error(message); }; + const url = 'https://response.example/resource/doc#fragment'; + check(doc instanceof __originRealm.Document, 'XHR creation realm'); + check(doc.URL === url && doc.documentURI === url, 'response URL metadata'); + check(__documentXhr.responseURL === url.split('#')[0], 'responseURL serialization'); + check(doc.domain === 'requester.example', 'origin belongs to requester'); + check(doc.defaultView === null && doc.hidden, 'windowless response document'); + check(doc.baseURI === 'https://response.example/assets/', 'relative parsed base'); + const link = doc.getElementById('link'); + check(link.href === 'https://response.example/assets/child', 'relative link'); + doc.querySelector('base').remove(); + check(doc.baseURI === url && link.baseURI === url, 'base removal'); + const base = doc.createElementNS('http://www.w3.org/1999/xhtml', 'base'); + base.href = '../changed/'; + doc.documentElement.appendChild(base); + check(doc.baseURI === 'https://response.example/changed/', 'base insertion'); + document.head.appendChild(base); + check(doc.baseURI === url, 'base adoption'); + __documentXhr.open('GET', '/next'); + check(__documentXhr.responseXML === null && doc.URL === url, 'XHR reuse'); + return 'ok'; + })()"#, + ) + .expect("response document metadata should remain coherent"); + assert_eq!( + result, "ok", + "child_realm={child_realm}, mime={mime}, response_type={response_type}" + ); + } + } +} + #[test] fn xml_http_request_serializes_document_bodies_and_limits_charset_rewriting_to_text() { let vm = new_storage_test_vm("https://xhr-document-body.test/");