mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
fix(trusted-types): gate document write arguments
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user