fix(trusted-types): gate document write arguments

This commit is contained in:
ldm0
2026-09-16 22:02:05 +08:00
parent 820cffe0fe
commit b653ee43cc
5 changed files with 194 additions and 21 deletions
@@ -3631,7 +3631,6 @@ svg/types/scripted/SVGLengthList-basics.html
svg/types/scripted/SVGList-parse-invalid-clears-items.html
svg/types/scripted/SVGMatrix-tentative.html
svg/types/scripted/SVGPoint.html
trusted-types/Document-write-appending-line-feed.html
trusted-types/Document-write.html
trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-TT-realm.html
trusted-types/Element-setAttribute-respects-Elements-node-documents-globals-CSP-after-adoption-from-non-TT-realm.html
@@ -7957,6 +7957,7 @@ trusted-types/DedicatedWorker-eval.html
trusted-types/DedicatedWorker-importScripts.html
trusted-types/DedicatedWorker-setTimeout-setInterval.html
trusted-types/Document-execCommand.html
trusted-types/Document-write-appending-line-feed.html
trusted-types/Element-insertAdjacentHTML.html
trusted-types/Element-outerHTML.html
trusted-types/Element-setAttribute.html
@@ -20,17 +20,18 @@ use crate::{
dom::native::{NativeDom, NodeData},
parser::HtmlParser,
util::{
call_object_method, node_wrapper_from_handle, utf16_next_scalar_boundary,
utf16_previous_scalar_boundary, utf16_replace_units_range_lossy, utf16_units, v8str,
call_object_method, get_private_value, node_wrapper_from_handle, set_private_value,
utf16_next_scalar_boundary, utf16_previous_scalar_boundary, utf16_replace_units_range_lossy,
utf16_units, v8_string, v8str,
},
webidl,
};
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "Document.write")]
struct DocumentWriteArgs {
#[webidl(variadic)]
text: Vec<String>,
const DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT: &str = "__moliDetachedDocumentWriteStreamOpen";
struct DocumentWriteInput {
text: String,
is_trusted: bool,
}
pub(in crate::native_bridge) fn node_document_write_callback<'s>(
@@ -64,9 +65,44 @@ fn node_document_write_or_writeln_callback<'s>(
rv.set_undefined();
return;
}
let Some(parsed) = webidl::parse_args::<DocumentWriteArgs>(scope, &args) else {
return;
let api_prefix = if append_newline {
"Document.writeln"
} else {
"Document.write"
};
let input = match document_write_input(scope, &args, api_prefix) {
Ok(input) => input,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let sink = if append_newline {
"Document writeln"
} else {
"Document write"
};
let api_name = if append_newline { "writeln" } else { "write" };
let mut html = input.text;
if !input.is_trusted {
let Some(value) = v8_string(scope, &html) else {
return;
};
let requirements = unsafe { &*runtime_ptr }.trusted_types_for_script_requirements(scope);
let Some(compliant) = crate::context_bootstrap::trusted_html_string_or_throw(
scope,
value.into(),
requirements,
sink,
api_name,
) else {
return;
};
html = compliant;
}
if append_newline {
html.push('\n');
}
if !is_html_document(unsafe { &*runtime_ptr }, handle) {
throw_dom_exception(
scope,
@@ -86,11 +122,19 @@ fn node_document_write_or_writeln_callback<'s>(
return;
}
if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() {
let mut html = parsed.text.concat();
if append_newline {
html.push('\n');
let document = args.this();
let stream_was_open = detached_document_write_stream_is_open(scope, document);
if !stream_was_open {
set_detached_document_write_stream_open(scope, document, true);
}
let wrote = if stream_was_open {
append_detached_html_document_body_html(scope, runtime_ptr, handle, &html)
} else {
set_detached_html_document_body_html(scope, runtime_ptr, handle, &html)
};
if !wrote && !stream_was_open {
set_detached_document_write_stream_open(scope, document, false);
}
append_detached_html_document_body_html(scope, runtime_ptr, handle, &html);
rv.set_undefined();
return;
}
@@ -107,15 +151,34 @@ fn node_document_write_or_writeln_callback<'s>(
clear_window_event_handlers(scope);
runtime.prepare_root_document_replacement(scope, runtime_ptr, handle);
}
for chunk in parsed.text {
let _ = runtime.write_html(scope, runtime_ptr, handle, &chunk);
}
if append_newline {
let _ = runtime.write_html(scope, runtime_ptr, handle, "\n");
}
let _ = runtime.write_html(scope, runtime_ptr, handle, &html);
rv.set_undefined();
}
fn document_write_input<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
api_prefix: &'static str,
) -> Result<DocumentWriteInput, webidl::WebIdlError> {
let mut text = String::new();
let mut is_trusted = true;
for index in 0..args.length() {
let value = args.get(index);
if let Some(value) = crate::context_bootstrap::trusted_html_value_string(scope, value) {
text.push_str(&value);
continue;
}
is_trusted = false;
let value = webidl::convert::<webidl::DomString>(
scope,
value,
webidl::Context::argument(api_prefix, (index + 1) as usize),
)?;
text.push_str(&value.0);
}
Ok(DocumentWriteInput { text, is_trusted })
}
fn current_script_ignores_document_write_without_parser_insertion_point(
runtime: &JsContextHost,
) -> bool {
@@ -181,7 +244,11 @@ pub(in crate::native_bridge) fn node_document_open_callback<'s>(
return;
}
if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() {
set_detached_html_document_body_html(scope, runtime_ptr, handle, "");
let document = args.this();
set_detached_document_write_stream_open(scope, document, true);
if !set_detached_html_document_body_html(scope, runtime_ptr, handle, "") {
set_detached_document_write_stream_open(scope, document, false);
}
rv.set(args.this().into());
return;
}
@@ -300,6 +367,7 @@ pub(in crate::native_bridge) fn node_document_close_callback<'s>(
return;
}
if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() {
set_detached_document_write_stream_open(scope, args.this(), false);
rv.set_undefined();
return;
}
@@ -308,6 +376,27 @@ pub(in crate::native_bridge) fn node_document_close_callback<'s>(
rv.set_undefined();
}
fn detached_document_write_stream_is_open<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, document, DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT)
.is_some_and(|value| value.boolean_value(scope))
}
fn set_detached_document_write_stream_open(
scope: &mut v8::PinScope<'_, '_>,
document: v8::Local<'_, v8::Object>,
open: bool,
) {
set_private_value(
scope,
document,
DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT,
v8::Boolean::new(scope, open).into(),
);
}
fn detached_html_document_body_handle(
runtime: &JsContextHost,
document_handle: DomHandle,
@@ -329,6 +418,9 @@ pub(in crate::native_bridge) fn set_detached_html_document_body_html(
let Some(body) = detached_html_document_body_handle(runtime, document_handle) else {
return false;
};
if html.is_empty() && runtime.dom_host().child_handles(body).next().is_none() {
return true;
}
runtime.set_inner_html(scope, runtime_ptr, body, html)
})
}
@@ -434,6 +434,86 @@ fn document_parse_html_unsafe_gates_converted_union_source() {
);
}
#[test]
fn document_write_gates_concatenated_union_values_before_writeln_newline() {
let mut vm = new_storage_test_vm("https://document-write-trusted-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
const errorName = callback => {
try {
callback();
return "none";
} catch (error) {
return error && error.name;
}
};
const custom = trustedTypes.createPolicy("document-write-custom", {
createHTML: value => `(${value})`
});
const doc = new DOMParser().parseFromString("<body></body>", "text/html");
const replacementDoc = new DOMParser().parseFromString(
custom.createHTML("seed"),
"text/html"
);
const reset = () => { doc.body.innerHTML = trustedTypes.emptyHTML; };
const blocked = errorName(() => doc.write("blocked"));
doc.write(custom.createHTML("1"), custom.createHTML("2"));
const allTrusted = doc.body.innerHTML;
replacementDoc.write(custom.createHTML("replacement"));
replacementDoc.writeln(custom.createHTML("tail"));
const replacesDetachedContent = replacementDoc.body.innerHTML;
reset();
const defaultCalls = [];
trustedTypes.createPolicy("default", {
createHTML: (value, type, sink) => {
defaultCalls.push([value, type, sink]);
return `[${value}]`;
}
});
doc.write("1", "2");
const strings = doc.body.innerHTML;
reset();
doc.write(custom.createHTML("1"), "2");
const mixed = doc.body.innerHTML;
reset();
doc.writeln("3", "4");
const stringLine = doc.body.innerHTML;
reset();
doc.writeln(custom.createHTML("3"), custom.createHTML("4"));
const trustedLine = doc.body.innerHTML;
reset();
doc.writeln();
const emptyLine = doc.body.innerHTML;
return JSON.stringify({
blocked,
allTrusted,
replacesDetachedContent,
strings,
mixed,
stringLine,
trustedLine,
emptyLine,
defaultCalls
});
})()
"#,
)
.expect("Document.write TrustedHTML union probe should evaluate");
assert_eq!(
result,
r#"{"blocked":"TypeError","allTrusted":"(1)(2)","replacesDetachedContent":"(replacement)(tail)\n","strings":"[12]","mixed":"[(1)2]","stringLine":"[34]\n","trustedLine":"(3)(4)\n","emptyLine":"\n","defaultCalls":[["12","TrustedHTML","Document write"],["(1)2","TrustedHTML","Document write"],["34","TrustedHTML","Document writeln"]]}"#
);
}
#[test]
fn script_elements_preserve_only_parser_or_trusted_script_source() {
let mut vm = new_storage_test_vm("https://script-source-trusted-types.test/");
@@ -99,6 +99,7 @@ fn detached_document_write_preserves_existing_noscript_text() {
r#"
(() => {
const doc = document.implementation.createHTMLDocument("");
doc.open();
const noscript = doc.createElement("noscript");
noscript.textContent = "<em>fallback&</em>";
doc.body.append(noscript);