From c00e0aae612d77ca69dbe0fccfc5bab808d0d69d Mon Sep 17 00:00:00 2001 From: Tim van der Lippe Date: Sun, 4 May 2025 13:50:33 +0200 Subject: [PATCH] Implement Trusted types document write sinks (#36824) Implements the Document.write algorithm covering Trusted HTML. Part of #36258 Signed-off-by: Tim van der Lippe Signed-off-by: Tim van der Lippe --- components/script/dom/document.rs | 142 ++++++++++++------ components/script/dom/servoparser/mod.rs | 10 +- components/script/dom/trustedhtml.rs | 37 +++++ .../script_bindings/webidls/Document.webidl | 4 +- tests/wpt/meta/MANIFEST.json | 2 +- .../Document-write-exception-order.xhtml.ini | 3 - ...ring-assignment-to-Document-write.html.ini | 42 ------ ...ypes-reporting-for-Document-write.html.ini | 6 - ...k-string-assignment-to-Document-write.html | 3 + 9 files changed, 142 insertions(+), 107 deletions(-) delete mode 100644 tests/wpt/meta/trusted-types/Document-write-exception-order.xhtml.ini delete mode 100644 tests/wpt/meta/trusted-types/block-string-assignment-to-Document-write.html.ini delete mode 100644 tests/wpt/meta/trusted-types/trusted-types-reporting-for-Document-write.html.ini diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 2baab15e1b8..9ce24038259 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -103,7 +103,9 @@ use crate::dom::bindings::codegen::Bindings::WindowBinding::{ }; use crate::dom::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods; use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; -use crate::dom::bindings::codegen::UnionTypes::{NodeOrString, StringOrElementCreationOptions}; +use crate::dom::bindings::codegen::UnionTypes::{ + NodeOrString, StringOrElementCreationOptions, TrustedHTMLOrString, +}; use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::num::Finite; @@ -184,6 +186,7 @@ use crate::dom::touch::Touch; use crate::dom::touchevent::TouchEvent as DomTouchEvent; use crate::dom::touchlist::TouchList; use crate::dom::treewalker::TreeWalker; +use crate::dom::trustedhtml::TrustedHTML; use crate::dom::types::VisibilityStateEntry; use crate::dom::uievent::UIEvent; use crate::dom::virtualmethods::vtable_for; @@ -3818,6 +3821,90 @@ impl Document { .Performance() .queue_entry(entry.upcast::(), can_gc); } + + /// + fn write( + &self, + text: Vec, + line_feed: bool, + containing_class: &str, + field: &str, + can_gc: CanGc, + ) -> ErrorResult { + // Step 1: Let string be the empty string. + let mut strings: Vec = Vec::with_capacity(text.len()); + // Step 2: Let isTrusted be false if text contains a string; otherwise true. + let mut is_trusted = true; + // Step 3: For each value of text: + for value in text { + match value { + // Step 3.1: If value is a TrustedHTML object, then append value's associated data to string. + TrustedHTMLOrString::TrustedHTML(trusted_html) => { + strings.push(trusted_html.to_string().to_owned()); + }, + TrustedHTMLOrString::String(str_) => { + // Step 2: Let isTrusted be false if text contains a string; otherwise true. + is_trusted = false; + // Step 3.2: Otherwise, append value to string. + strings.push(str_.into()); + }, + }; + } + let mut string = itertools::join(strings, ""); + // Step 4: If isTrusted is false, set string to the result of invoking the + // Get Trusted Type compliant string algorithm with TrustedHTML, + // this's relevant global object, string, sink, and "script". + if !is_trusted { + string = TrustedHTML::get_trusted_script_compliant_string( + &self.global(), + TrustedHTMLOrString::String(string.into()), + containing_class, + field, + can_gc, + )?; + } + // Step 5: If lineFeed is true, append U+000A LINE FEED to string. + if line_feed { + string.push('\n'); + } + // Step 6: If document is an XML document, then throw an "InvalidStateError" DOMException. + if !self.is_html_document() { + return Err(Error::InvalidState); + } + + // Step 7: If document's throw-on-dynamic-markup-insertion counter is greater than 0, + // then throw an "InvalidStateError" DOMException. + if self.throw_on_dynamic_markup_insertion_counter.get() > 0 { + return Err(Error::InvalidState); + } + + // Step 8: If document's active parser was aborted is true, then return. + if !self.is_active() || self.active_parser_was_aborted.get() { + return Ok(()); + } + + let parser = match self.get_current_parser() { + Some(ref parser) if parser.can_write() => DomRoot::from_ref(&**parser), + // Step 9: If the insertion point is undefined, then: + _ => { + // Step 9.1: If document's unload counter is greater than 0 or + // document's ignore-destructive-writes counter is greater than 0, then return. + if self.is_prompting_or_unloading() || + self.ignore_destructive_writes_counter.get() > 0 + { + return Ok(()); + } + // Step 9.2: Run the document open steps with document. + self.Open(None, None, can_gc)?; + self.get_current_parser().unwrap() + }, + }; + + // Steps 10-11. + parser.write(string.into(), can_gc); + + Ok(()) + } } fn is_character_value_key(key: &Key) -> bool { @@ -6408,54 +6495,17 @@ impl DocumentMethods for Document { } // https://html.spec.whatwg.org/multipage/#dom-document-write - fn Write(&self, text: Vec, can_gc: CanGc) -> ErrorResult { - if !self.is_html_document() { - // Step 1. - return Err(Error::InvalidState); - } - - // Step 2. - if self.throw_on_dynamic_markup_insertion_counter.get() > 0 { - return Err(Error::InvalidState); - } - - // Step 3 - what specifies the is_active() part here? - if !self.is_active() || self.active_parser_was_aborted.get() { - return Ok(()); - } - - let parser = match self.get_current_parser() { - Some(ref parser) if parser.can_write() => DomRoot::from_ref(&**parser), - _ => { - // Either there is no parser, which means the parsing ended; - // or script nesting level is 0, which means the method was - // called from outside a parser-executed script. - if self.is_prompting_or_unloading() || - self.ignore_destructive_writes_counter.get() > 0 - { - // Step 4. - return Ok(()); - } - // Step 5. - self.Open(None, None, can_gc)?; - self.get_current_parser().unwrap() - }, - }; - - // Step 7. - // TODO: handle reload override buffer. - - // Steps 6-8. - parser.write(text, can_gc); - - // Step 9. - Ok(()) + fn Write(&self, text: Vec, can_gc: CanGc) -> ErrorResult { + // The document.write(...text) method steps are to run the document write steps + // with this, text, false, and "Document write". + self.write(text, false, "Document", "write", can_gc) } // https://html.spec.whatwg.org/multipage/#dom-document-writeln - fn Writeln(&self, mut text: Vec, can_gc: CanGc) -> ErrorResult { - text.push("\n".into()); - self.Write(text, can_gc) + fn Writeln(&self, text: Vec, can_gc: CanGc) -> ErrorResult { + // The document.writeln(...text) method steps are to run the document write steps + // with this, text, true, and "Document writeln". + self.write(text, true, "Document", "writeln", can_gc) } // https://html.spec.whatwg.org/multipage/#dom-document-close diff --git a/components/script/dom/servoparser/mod.rs b/components/script/dom/servoparser/mod.rs index 5878573d552..3a1efdfb291 100644 --- a/components/script/dom/servoparser/mod.rs +++ b/components/script/dom/servoparser/mod.rs @@ -357,16 +357,14 @@ impl ServoParser { } /// Steps 6-8 of - pub(crate) fn write(&self, text: Vec, can_gc: CanGc) { + pub(crate) fn write(&self, text: DOMString, can_gc: CanGc) { assert!(self.can_write()); if self.document.has_pending_parsing_blocking_script() { // There is already a pending parsing blocking script so the // parser is suspended, we just append everything to the // script input and abort these steps. - for chunk in text { - self.script_input.push_back(String::from(chunk).into()); - } + self.script_input.push_back(String::from(text).into()); return; } @@ -376,9 +374,7 @@ impl ServoParser { assert!(self.script_input.is_empty()); let input = BufferQueue::default(); - for chunk in text { - input.push_back(String::from(chunk).into()); - } + input.push_back(String::from(text).into()); let profiler_chan = self .document diff --git a/components/script/dom/trustedhtml.rs b/components/script/dom/trustedhtml.rs index 07298601f2f..8508f28c150 100644 --- a/components/script/dom/trustedhtml.rs +++ b/components/script/dom/trustedhtml.rs @@ -2,13 +2,19 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::fmt; + use dom_struct::dom_struct; use crate::dom::bindings::codegen::Bindings::TrustedHTMLBinding::TrustedHTMLMethods; +use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrString; +use crate::dom::bindings::error::Fallible; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::str::DOMString; use crate::dom::globalscope::GlobalScope; +use crate::dom::trustedtypepolicy::TrustedType; +use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory; use crate::script_runtime::CanGc; #[dom_struct] @@ -30,6 +36,37 @@ impl TrustedHTML { pub(crate) fn new(data: String, global: &GlobalScope, can_gc: CanGc) -> DomRoot { reflect_dom_object(Box::new(Self::new_inherited(data)), global, can_gc) } + + pub(crate) fn get_trusted_script_compliant_string( + global: &GlobalScope, + value: TrustedHTMLOrString, + containing_class: &str, + field: &str, + can_gc: CanGc, + ) -> Fallible { + match value { + TrustedHTMLOrString::String(value) => { + let sink = format!("{} {}", containing_class, field); + TrustedTypePolicyFactory::get_trusted_type_compliant_string( + TrustedType::TrustedHTML, + global, + value.as_ref().to_owned(), + &sink, + "'script'", + can_gc, + ) + }, + + TrustedHTMLOrString::TrustedHTML(trusted_html) => Ok(trusted_html.to_string()), + } + } +} + +impl fmt::Display for TrustedHTML { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.data) + } } impl TrustedHTMLMethods for TrustedHTML { diff --git a/components/script_bindings/webidls/Document.webidl b/components/script_bindings/webidls/Document.webidl index 6131dbd15c7..4af7dcc2c7a 100644 --- a/components/script_bindings/webidls/Document.webidl +++ b/components/script_bindings/webidls/Document.webidl @@ -129,9 +129,9 @@ partial /*sealed*/ interface Document { [CEReactions, Throws] undefined close(); [CEReactions, Throws] - undefined write(DOMString... text); + undefined write((TrustedHTML or DOMString)... text); [CEReactions, Throws] - undefined writeln(DOMString... text); + undefined writeln((TrustedHTML or DOMString)... text); // user interaction readonly attribute Window?/*Proxy?*/ defaultView; diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index b0710945679..7b631710b77 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -818553,7 +818553,7 @@ ] ], "block-string-assignment-to-Document-write.html": [ - "0b16a9c4910070b2bcf829d15b1aba8b7be9d060", + "c774dca4390955b3b91f1648adb18ad22bd6e355", [ null, {} diff --git a/tests/wpt/meta/trusted-types/Document-write-exception-order.xhtml.ini b/tests/wpt/meta/trusted-types/Document-write-exception-order.xhtml.ini deleted file mode 100644 index b779a38f615..00000000000 --- a/tests/wpt/meta/trusted-types/Document-write-exception-order.xhtml.ini +++ /dev/null @@ -1,3 +0,0 @@ -[Document-write-exception-order.xhtml] - [`document.write(string)` throws TypeError] - expected: FAIL diff --git a/tests/wpt/meta/trusted-types/block-string-assignment-to-Document-write.html.ini b/tests/wpt/meta/trusted-types/block-string-assignment-to-Document-write.html.ini deleted file mode 100644 index 78fec0f216a..00000000000 --- a/tests/wpt/meta/trusted-types/block-string-assignment-to-Document-write.html.ini +++ /dev/null @@ -1,42 +0,0 @@ -[block-string-assignment-to-Document-write.html] - [`document.write(string)` throws] - expected: FAIL - - [`document.write(string, string)` throws] - expected: FAIL - - [`document.write(string, TrustedHTML)` throws] - expected: FAIL - - [`document.writeln(string)` throws] - expected: FAIL - - [`document.writeln(string, string)` throws] - expected: FAIL - - [`document.writeln(string, TrustedHTML)` throws] - expected: FAIL - - [`document.write(null)` throws] - expected: FAIL - - [`document.writeln(null)` throws] - expected: FAIL - - [`document.write(string)` observes default policy] - expected: FAIL - - [`document.write(string, string)` observes default policy] - expected: FAIL - - [`document.write(string, TrustedHTML)` observes default policy] - expected: FAIL - - [`document.writeln(string)` observes default policy] - expected: FAIL - - [`document.writeln(string, string)` observes default policy] - expected: FAIL - - [`document.writeln(string, TrustedHTML)` observes default policy] - expected: FAIL diff --git a/tests/wpt/meta/trusted-types/trusted-types-reporting-for-Document-write.html.ini b/tests/wpt/meta/trusted-types/trusted-types-reporting-for-Document-write.html.ini deleted file mode 100644 index fa2ca7fa336..00000000000 --- a/tests/wpt/meta/trusted-types/trusted-types-reporting-for-Document-write.html.ini +++ /dev/null @@ -1,6 +0,0 @@ -[trusted-types-reporting-for-Document-write.html] - [Violation report for plain string for write() with at least one plain string argument.] - expected: FAIL - - [Violation report for plain string for writeln() with at least one plain string argument.] - expected: FAIL diff --git a/tests/wpt/tests/trusted-types/block-string-assignment-to-Document-write.html b/tests/wpt/tests/trusted-types/block-string-assignment-to-Document-write.html index 0b16a9c4910..c774dca4390 100644 --- a/tests/wpt/tests/trusted-types/block-string-assignment-to-Document-write.html +++ b/tests/wpt/tests/trusted-types/block-string-assignment-to-Document-write.html @@ -124,6 +124,9 @@ assert_equals(sink, "Document write"); } else if (html === "assertSinkEqualsDocumentWriteLn") { assert_equals(sink, "Document writeln"); + } else if (html === "assertSinkEqualsDocumentWriteLn\n") { + // Ensure that new line characters aren't incorrectly added prior to processing + assert_unreached(`Should not process any other HTML, but got "${html}"`); } return html.replace("Hi", "Quack")