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 <tvanderlippe@gmail.com>

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
This commit is contained in:
Tim van der Lippe 2025-05-04 13:50:33 +02:00 committed by GitHub
parent 43edab336a
commit c00e0aae61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 142 additions and 107 deletions

View file

@ -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::XPathEvaluatorBinding::XPathEvaluatorMethods;
use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; 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::error::{Error, ErrorInfo, ErrorResult, Fallible};
use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId};
use crate::dom::bindings::num::Finite; 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::touchevent::TouchEvent as DomTouchEvent;
use crate::dom::touchlist::TouchList; use crate::dom::touchlist::TouchList;
use crate::dom::treewalker::TreeWalker; use crate::dom::treewalker::TreeWalker;
use crate::dom::trustedhtml::TrustedHTML;
use crate::dom::types::VisibilityStateEntry; use crate::dom::types::VisibilityStateEntry;
use crate::dom::uievent::UIEvent; use crate::dom::uievent::UIEvent;
use crate::dom::virtualmethods::vtable_for; use crate::dom::virtualmethods::vtable_for;
@ -3818,6 +3821,90 @@ impl Document {
.Performance() .Performance()
.queue_entry(entry.upcast::<PerformanceEntry>(), can_gc); .queue_entry(entry.upcast::<PerformanceEntry>(), can_gc);
} }
/// <https://html.spec.whatwg.org/multipage/#document-write-steps>
fn write(
&self,
text: Vec<TrustedHTMLOrString>,
line_feed: bool,
containing_class: &str,
field: &str,
can_gc: CanGc,
) -> ErrorResult {
// Step 1: Let string be the empty string.
let mut strings: Vec<String> = 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 { fn is_character_value_key(key: &Key) -> bool {
@ -6408,54 +6495,17 @@ impl DocumentMethods<crate::DomTypeHolder> for Document {
} }
// https://html.spec.whatwg.org/multipage/#dom-document-write // https://html.spec.whatwg.org/multipage/#dom-document-write
fn Write(&self, text: Vec<DOMString>, can_gc: CanGc) -> ErrorResult { fn Write(&self, text: Vec<TrustedHTMLOrString>, can_gc: CanGc) -> ErrorResult {
if !self.is_html_document() { // The document.write(...text) method steps are to run the document write steps
// Step 1. // with this, text, false, and "Document write".
return Err(Error::InvalidState); self.write(text, false, "Document", "write", can_gc)
}
// 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(())
} }
// https://html.spec.whatwg.org/multipage/#dom-document-writeln // https://html.spec.whatwg.org/multipage/#dom-document-writeln
fn Writeln(&self, mut text: Vec<DOMString>, can_gc: CanGc) -> ErrorResult { fn Writeln(&self, text: Vec<TrustedHTMLOrString>, can_gc: CanGc) -> ErrorResult {
text.push("\n".into()); // The document.writeln(...text) method steps are to run the document write steps
self.Write(text, can_gc) // with this, text, true, and "Document writeln".
self.write(text, true, "Document", "writeln", can_gc)
} }
// https://html.spec.whatwg.org/multipage/#dom-document-close // https://html.spec.whatwg.org/multipage/#dom-document-close

View file

@ -357,16 +357,14 @@ impl ServoParser {
} }
/// Steps 6-8 of <https://html.spec.whatwg.org/multipage/#document.write()> /// Steps 6-8 of <https://html.spec.whatwg.org/multipage/#document.write()>
pub(crate) fn write(&self, text: Vec<DOMString>, can_gc: CanGc) { pub(crate) fn write(&self, text: DOMString, can_gc: CanGc) {
assert!(self.can_write()); assert!(self.can_write());
if self.document.has_pending_parsing_blocking_script() { if self.document.has_pending_parsing_blocking_script() {
// There is already a pending parsing blocking script so the // There is already a pending parsing blocking script so the
// parser is suspended, we just append everything to the // parser is suspended, we just append everything to the
// script input and abort these steps. // script input and abort these steps.
for chunk in text { self.script_input.push_back(String::from(text).into());
self.script_input.push_back(String::from(chunk).into());
}
return; return;
} }
@ -376,9 +374,7 @@ impl ServoParser {
assert!(self.script_input.is_empty()); assert!(self.script_input.is_empty());
let input = BufferQueue::default(); let input = BufferQueue::default();
for chunk in text { input.push_back(String::from(text).into());
input.push_back(String::from(chunk).into());
}
let profiler_chan = self let profiler_chan = self
.document .document

View file

@ -2,13 +2,19 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::fmt;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::TrustedHTMLBinding::TrustedHTMLMethods; 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::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString; use crate::dom::bindings::str::DOMString;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::trustedtypepolicy::TrustedType;
use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory;
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
#[dom_struct] #[dom_struct]
@ -30,6 +36,37 @@ impl TrustedHTML {
pub(crate) fn new(data: String, global: &GlobalScope, can_gc: CanGc) -> DomRoot<Self> { pub(crate) fn new(data: String, global: &GlobalScope, can_gc: CanGc) -> DomRoot<Self> {
reflect_dom_object(Box::new(Self::new_inherited(data)), global, can_gc) 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<String> {
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<crate::DomTypeHolder> for TrustedHTML { impl TrustedHTMLMethods<crate::DomTypeHolder> for TrustedHTML {

View file

@ -129,9 +129,9 @@ partial /*sealed*/ interface Document {
[CEReactions, Throws] [CEReactions, Throws]
undefined close(); undefined close();
[CEReactions, Throws] [CEReactions, Throws]
undefined write(DOMString... text); undefined write((TrustedHTML or DOMString)... text);
[CEReactions, Throws] [CEReactions, Throws]
undefined writeln(DOMString... text); undefined writeln((TrustedHTML or DOMString)... text);
// user interaction // user interaction
readonly attribute Window?/*Proxy?*/ defaultView; readonly attribute Window?/*Proxy?*/ defaultView;

View file

@ -818553,7 +818553,7 @@
] ]
], ],
"block-string-assignment-to-Document-write.html": [ "block-string-assignment-to-Document-write.html": [
"0b16a9c4910070b2bcf829d15b1aba8b7be9d060", "c774dca4390955b3b91f1648adb18ad22bd6e355",
[ [
null, null,
{} {}

View file

@ -1,3 +0,0 @@
[Document-write-exception-order.xhtml]
[`document.write(string)` throws TypeError]
expected: FAIL

View file

@ -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

View file

@ -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

View file

@ -124,6 +124,9 @@
assert_equals(sink, "Document write"); assert_equals(sink, "Document write");
} else if (html === "assertSinkEqualsDocumentWriteLn") { } else if (html === "assertSinkEqualsDocumentWriteLn") {
assert_equals(sink, "Document writeln"); 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") return html.replace("Hi", "Quack")