LibWeb: Implement slot validation for HTMLScriptElement

This should be the last section missing in the TrustedType spec.
This commit is contained in:
Tete17 2025-10-29 15:20:33 +01:00 committed by Jelle Raaijmakers
parent 1d1182cad8
commit 901afee50b
Notes: github-actions[bot] 2025-12-01 08:55:48 +00:00
6 changed files with 567 additions and 33 deletions

View file

@ -82,6 +82,31 @@ void HTMLScriptElement::attribute_changed(FlyString const& name, Optional<String
}
}
// https://www.w3.org/TR/trusted-types/#prepare-script-text
WebIDL::ExceptionOr<void> HTMLScriptElement::prepare_script_text()
{
// 1. Let sink be "HTMLScriptElement text" if script is an HTMLScriptElement; otherwise "SVGScriptElement text".
constexpr auto sink = TrustedTypes::InjectionSink::HTMLScriptElement_text;
// 2. If scripts script text value is not equal to its child text content,
// set scripts script text to the result of executing get Trusted Type compliant string, with the following arguments:
// TrustedScriptURL as expectedType,
// scripts Documents relevant global object as global,
// scripts child text content attribute value as input,
// sink,
// 'script' as sinkGroup.
if (m_script_text != child_text_content()) {
m_script_text = TRY(TrustedTypes::get_trusted_type_compliant_string(
TrustedTypes::TrustedTypeName::TrustedScriptURL,
HTML::relevant_global_object(document()),
child_text_content(),
sink,
TrustedTypes::Script.to_string()));
}
return {};
}
void HTMLScriptElement::begin_delaying_document_load_event(DOM::Document& document)
{
// https://html.spec.whatwg.org/multipage/scripting.html#concept-script-script
@ -170,6 +195,7 @@ void HTMLScriptElement::execute_script()
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load));
}
// https://w3c.github.io/trusted-types/dist/spec/#slot-value-verification
// https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script
// https://whatpr.org/html/9893/scripting.html#prepare-a-script
void HTMLScriptElement::prepare_script()
@ -191,22 +217,26 @@ void HTMLScriptElement::prepare_script()
m_force_async = true;
}
// 5. Let source text be el's child text content.
auto source_text = child_text_content();
// 5. Execute the Prepare the script text algorithm on el. If that algorithm threw an error, then return.
if (prepare_script_text().is_exception())
return;
// 6. Let source text be els script text value.
auto source_text = m_script_text;
auto source_text_utf8 = source_text.to_utf8_but_should_be_ported_to_utf16();
// 6. If el has no src attribute, and source text is the empty string, then return.
// 7. If el has no src attribute, and source text is the empty string, then return.
if (!has_attribute(HTML::AttributeNames::src) && source_text.is_empty()) {
return;
}
// 7. If el is not connected, then return.
// 8. If el is not connected, then return.
if (!is_connected()) {
dbgln("HTMLScriptElement: Refusing to run script because the element is not connected.");
return;
}
// 8. If any of the following are true:
// 9. If any of the following are true:
// - el has a type attribute whose value is the empty string;
// - el has no type attribute but it has a language attribute and that attribute's value is the empty string; or
// - el has neither a type attribute nor a language attribute
@ -230,61 +260,61 @@ void HTMLScriptElement::prepare_script()
script_block_type = MUST(String::formatted("text/{}", maybe_language_attribute.value()));
}
// 9. If the script block's type string is a JavaScript MIME type essence match,
// 10. If the script block's type string is a JavaScript MIME type essence match,
if (MimeSniff::is_javascript_mime_type_essence_match(script_block_type)) {
// then set el's type to "classic".
m_script_type = ScriptType::Classic;
}
// 10. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "module",
// 11. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "module",
else if (script_block_type.equals_ignoring_ascii_case("module"sv)) {
// then set el's type to "module".
m_script_type = ScriptType::Module;
}
// 11. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "importmap",
// 12. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "importmap",
else if (script_block_type.equals_ignoring_ascii_case("importmap"sv)) {
// then set el's type to "importmap".
m_script_type = ScriptType::ImportMap;
}
// FIXME: 12. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "speculationrules", then set el's type to "speculationrules".
// 13. Otherwise, return. (No script is executed, and el's type is left as null.)
// FIXME: 13. Otherwise, if the script block's type string is an ASCII case-insensitive match for the string "speculationrules", then set el's type to "speculationrules".
// 14. Otherwise, return. (No script is executed, and el's type is left as null.)
else {
VERIFY(m_script_type == ScriptType::Null);
return;
}
// 14. If parser document is non-null, then set el's parser document back to parser document and set el's force async to false.
// 15. If parser document is non-null, then set el's parser document back to parser document and set el's force async to false.
if (parser_document) {
m_parser_document = parser_document;
m_force_async = false;
}
// 15. Set el's already started to true.
// 16. Set el's already started to true.
m_already_started = true;
// 16. Set el's preparation-time document to its node document.
// 17. Set el's preparation-time document to its node document.
m_preparation_time_document = &document();
// 17. If parser document is non-null, and parser document is not equal to el's preparation-time document, then return.
// 18. If parser document is non-null, and parser document is not equal to el's preparation-time document, then return.
if (parser_document != nullptr && parser_document != m_preparation_time_document) {
dbgln("HTMLScriptElement: Refusing to run script because the parser document is not the same as the preparation time document.");
return;
}
// 18. If scripting is disabled for el, then return.
// 19. If scripting is disabled for el, then return.
if (is_scripting_disabled()) {
dbgln("HTMLScriptElement: Refusing to run script because scripting is disabled.");
return;
}
// 19. If el has a nomodule content attribute and its type is "classic", then return.
// 20. If el has a nomodule content attribute and its type is "classic", then return.
if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::nomodule)) {
dbgln("HTMLScriptElement: Refusing to run classic script because it has the nomodule attribute.");
return;
}
// FIXME: 20. Let cspType be "script speculationrules" if el's type is "speculationrules"; otherwise, "script".
// FIXME: 21. Let cspType be "script speculationrules" if el's type is "speculationrules"; otherwise, "script".
// 21. If el does not have a src content attribute, and the Should element's inline behavior be blocked by Content
// 22. If el does not have a src content attribute, and the Should element's inline behavior be blocked by Content
// Security Policy? algorithm returns "Blocked" when given el, cspType, and source text, then return [CSP]
if (!has_attribute(AttributeNames::src)
&& ContentSecurityPolicy::should_elements_inline_type_behavior_be_blocked_by_content_security_policy(realm(), *this, ContentSecurityPolicy::Directives::Directive::InlineType::Script, source_text_utf8) == ContentSecurityPolicy::Directives::Directive::Result::Blocked) {
@ -292,7 +322,7 @@ void HTMLScriptElement::prepare_script()
return;
}
// 22. If el has an event attribute and a for attribute, and el's type is "classic", then:
// 23. If el has an event attribute and a for attribute, and el's type is "classic", then:
if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::event) && has_attribute(HTML::AttributeNames::for_)) {
// 1. Let for be the value of el's' for attribute.
auto for_ = get_attribute_value(HTML::AttributeNames::for_);
@ -318,7 +348,7 @@ void HTMLScriptElement::prepare_script()
}
}
// 23. If el has a charset attribute, then let encoding be the result of getting an encoding from the value of the charset attribute.
// 24. If el has a charset attribute, then let encoding be the result of getting an encoding from the value of the charset attribute.
// If el does not have a charset attribute, or if getting an encoding failed, then let encoding be el's node document's the encoding.
Optional<String> encoding;
@ -334,34 +364,34 @@ void HTMLScriptElement::prepare_script()
VERIFY(encoding.has_value());
// 24. Let classic script CORS setting be the current state of el's crossorigin content attribute.
// 25. Let classic script CORS setting be the current state of el's crossorigin content attribute.
auto classic_script_cors_setting = m_crossorigin;
// 25. Let module script credentials mode be the CORS settings attribute credentials mode for el's crossorigin content attribute.
// 26. Let module script credentials mode be the CORS settings attribute credentials mode for el's crossorigin content attribute.
auto module_script_credential_mode = cors_settings_attribute_credentials_mode(m_crossorigin);
// 26. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value.
// 27. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value.
auto cryptographic_nonce = m_cryptographic_nonce;
// 27. If el has an integrity attribute, then let integrity metadata be that attribute's value.
// 28. If el has an integrity attribute, then let integrity metadata be that attribute's value.
// Otherwise, let integrity metadata be the empty string.
String integrity_metadata;
if (auto maybe_integrity = attribute(HTML::AttributeNames::integrity); maybe_integrity.has_value()) {
integrity_metadata = *maybe_integrity;
}
// 28. Let referrer policy be the current state of el's referrerpolicy content attribute.
// 29. Let referrer policy be the current state of el's referrerpolicy content attribute.
auto referrer_policy = m_referrer_policy;
// 29. Let fetch priority be the current state of el's fetchpriority content attribute.
// 30. Let fetch priority be the current state of el's fetchpriority content attribute.
auto fetch_priority = Fetch::Infrastructure::request_priority_from_string(get_attribute_value(HTML::AttributeNames::fetchpriority)).value_or(Fetch::Infrastructure::Request::Priority::Auto);
// 30. Let parser metadata be "parser-inserted" if el is parser-inserted, and "not-parser-inserted" otherwise.
// 31. Let parser metadata be "parser-inserted" if el is parser-inserted, and "not-parser-inserted" otherwise.
auto parser_metadata = is_parser_inserted()
? Fetch::Infrastructure::Request::ParserMetadata::ParserInserted
: Fetch::Infrastructure::Request::ParserMetadata::NotParserInserted;
// 31. Let options be a script fetch options whose cryptographic nonce is cryptographic nonce,
// 32. Let options be a script fetch options whose cryptographic nonce is cryptographic nonce,
// integrity metadata is integrity metadata, parser metadata is parser metadata,
// credentials mode is module script credentials mode, referrer policy is referrer policy,
// and fetch priority is fetch priority.
@ -374,10 +404,10 @@ void HTMLScriptElement::prepare_script()
.fetch_priority = move(fetch_priority),
};
// 32. Let settings object be el's node document's relevant settings object.
// 33. Let settings object be el's node document's relevant settings object.
auto& settings_object = document().relevant_settings_object();
// 33. If el has a src content attribute, then:
// 34. If el has a src content attribute, then:
if (has_attribute(HTML::AttributeNames::src)) {
// 1. If el's type is "importmap" or "speculationrules", then:
// FIXME: Add "speculationrules" support.
@ -452,7 +482,7 @@ void HTMLScriptElement::prepare_script()
}
}
// 34. If el does not have a src content attribute:
// 35. If el does not have a src content attribute:
if (!has_attribute(HTML::AttributeNames::src)) {
// 1. Let base URL be el's node document's document base URL.
auto base_url = document().base_url();
@ -498,7 +528,7 @@ void HTMLScriptElement::prepare_script()
// FIXME: -> "speculationrules"
}
// 35. If el's type is "classic" and el has a src attribute, or el's type is "module":
// 36. If el's type is "classic" and el has a src attribute, or el's type is "module":
if ((m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::src)) || m_script_type == ScriptType::Module) {
// 1. Assert: el's result is "uninitialized".
// FIXME: I believe this step to be a spec bug, and it should be removed: https://github.com/whatwg/html/issues/8534
@ -572,7 +602,7 @@ void HTMLScriptElement::prepare_script()
}
}
// 36. Otherwise:
// 37. Otherwise:
else {
// 1. Assert: el's result is not "uninitialized".
VERIFY(!m_result.has<ResultState::Uninitialized>());

View file

@ -92,6 +92,9 @@ private:
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
// https://www.w3.org/TR/trusted-types/#prepare-script-text
WebIDL::ExceptionOr<void> prepare_script_text();
// https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element
void prepare_script();

View file

@ -0,0 +1,39 @@
Harness status: OK
Found 34 tests
34 Pass
Pass The HTMLScriptElement is initially trusted.
Pass Script source set via TrustedScript sink HTMLScriptElement.innerText keeps trustworthiness.
Pass Script source set via HTMLElement.innerText drops trustworthiness.
Pass Script source set via TrustedScript sink HTMLScriptElement.textContent keeps trustworthiness.
Pass Script source set Node.textContent drops trustworthiness.
Pass Script source set via TrustedScript sink HTMLScriptElement.text keeps trustworthiness.
Pass Script source set via TrustedHTML sink Element.innerHTML drops trustworthiness.
Pass Script source set via TrustedHTML sink Element.setHTMLUnsafe() drops trustworthiness.
Pass Splitting script source via Text.splitText() keeps trustworthiness.
Pass Normalizing script source via Element.normalize() keeps trustworthiness.
Pass Script source set via Node.nodeValue drops trustworthiness.
Pass Setting script source via CharacterData.data drops trustworthiness.
Pass Setting script source via CharacterData.appendData() drops trustworthiness.
Pass Setting script source via CharacterData.insertData() drops trustworthiness.
Pass Setting script source via CharacterData.replaceData() drops trustworthiness.
Pass Setting script source via CharacterData.deleteData() drops trustworthiness.
Pass Setting script source via CharacterData.before() drops trustworthiness.
Pass Setting script source via CharacterData.after() drops trustworthiness.
Pass Setting script source via CharacterData.remove() drops trustworthiness.
Pass Setting script source via CharacterData.replaceWith() drops trustworthiness.
Pass Setting script source via Node.appendChild() drops trustworthiness.
Pass Setting script source via Node.insertBefore() drops trustworthiness.
Pass Setting script source via Node.replaceChild() drops trustworthiness.
Pass Setting script source via Node.removeChild() drops trustworthiness.
Pass Setting script source via Element.prepend() drops trustworthiness.
Pass Setting script source via Element.append() drops trustworthiness.
Pass Setting script source via Element.replaceChildren() drops trustworthiness.
Pass Setting script source via Element.moveBefore() drops trustworthiness.
Pass Setting script source via TrustedHTML sink Node.insertAdjacentHTML() drops trustworthiness.
Pass Setting script source via Node.insertAdjacentText() drops trustworthiness.
Pass Setting script source via Range.insertNode() drops trustworthiness.
Pass Setting script source via Range.deleteContents() drops trustworthiness.
Pass Cloning a script via Node.cloneNode() drops trustworthiness.
Pass Cloning a script via Range.cloneContents() drops trustworthiness.

View file

@ -0,0 +1,351 @@
<!DOCTYPE html>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="support/namespaces.js"></script>
<script src="support/passthroughpolicy.js"></script>
<script src="support/script-messages.js"></script>
<link rel="help" href="https://w3c.github.io/trusted-types/dist/spec/#enforcement-in-scripts">
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script';">
<!-- This test modifies the source of a new (initially disconnected)
HTMLScriptElement by various DOM APIs and verifies whether it will
remain trustworthy. This can be done by checking whether the script
is actually executed after insertion, because this page enforces
Trusted Types without defining any default policy. -->
<div id="container"></div>
<script>
promise_test(async t => {
let message = await script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(LOG_RUN_MESSAGE);
container.appendChild(script);
});
assert_equals(message, "RUN");
}, "The HTMLScriptElement is initially trusted.");
promise_test(async t => {
await promise_rejects_js(t, TypeError, script_messages_for(_ => {
document.createElement("script").innerText = LOG_RUN_MESSAGE;
}), "TrustedScript required.");
let message = await script_message_for(_ => {
let script = document.createElement("script");
script.innerText = passthroughpolicy.createScript(LOG_RUN_MESSAGE);
container.appendChild(script);
});
assert_equals(message, "RUN");
}, "Script source set via TrustedScript sink HTMLScriptElement.innerText keeps trustworthiness.");
promise_test(async t => {
let script = document.createElement("script");
Object.getOwnPropertyDescriptor(HTMLElement.prototype, "innerText").set.call(script, LOG_RUN_MESSAGE);
assert_equals(script.innerText, LOG_RUN_MESSAGE, "TrustedScript not required.");
await no_script_message_for(_ => {
container.appendChild(script);
});
}, "Script source set via HTMLElement.innerText drops trustworthiness.");
promise_test(async t => {
await promise_rejects_js(t, TypeError, script_messages_for(_ => {
document.createElement("script").textContent = LOG_RUN_MESSAGE;
}), "TrustedScript required.");
let message = await script_message_for(_ => {
let script = document.createElement("script");
script.textContent = passthroughpolicy.createScript(LOG_RUN_MESSAGE);
container.appendChild(script);
});
assert_equals(message, "RUN");
}, "Script source set via TrustedScript sink HTMLScriptElement.textContent keeps trustworthiness.");
promise_test(async t => {
let script = document.createElement("script");
Object.getOwnPropertyDescriptor(Node.prototype, "textContent").set.call(script, LOG_RUN_MESSAGE);
assert_equals(script.textContent, LOG_RUN_MESSAGE, "TrustedScript not required.");
await no_script_message_for(_ => {
container.appendChild(script);
});
}, "Script source set Node.textContent drops trustworthiness.");
promise_test(async t => {
await promise_rejects_js(t, TypeError, script_messages_for(_ => {
document.createElement("script").text = LOG_RUN_MESSAGE;
}), "TrustedScript required.");
let message = await script_message_for(_ => {
let script = document.createElement("script");
script.text = passthroughpolicy.createScript(LOG_RUN_MESSAGE);
container.appendChild(script);
});
assert_equals(message, "RUN");
}, "Script source set via TrustedScript sink HTMLScriptElement.text keeps trustworthiness.");
promise_test(async t => {
await promise_rejects_js(t, TypeError, script_messages_for(_ => {
document.createElement("script").innerHTML = LOG_RUN_MESSAGE;
}), "TrustedHTML required.");
await no_script_message_for(_ => {
let script = document.createElement("script");
script.innerHTML = passthroughpolicy.createHTML(LOG_RUN_MESSAGE);
container.appendChild(script);
});
}, "Script source set via TrustedHTML sink Element.innerHTML drops trustworthiness.");
promise_test(async t => {
await promise_rejects_js(t, TypeError, script_messages_for(_ => {
document.createElement("script").setHTMLUnsafe(LOG_RUN_MESSAGE);
}), "TrustedHTML required.");
await no_script_message_for(_ => {
let script = document.createElement("script");
script.setHTMLUnsafe(passthroughpolicy.createHTML(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Script source set via TrustedHTML sink Element.setHTMLUnsafe() drops trustworthiness.");
if (HTMLScriptElement.prototype.setHTML) {
promise_test(async t => {
// https://wicg.github.io/sanitizer-api/#set-and-filter-html
let script = document.createElement("script");
script.setHTML(LOG_RUN_MESSAGE);
assert_equals(script.text, "");
}, "Script source cannot be set via Element.setHTML().");
}
promise_test(async t => {
let message = await script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`${LOG_RUN_MESSAGE};;;`);
script.firstChild.splitText(3);
container.appendChild(script);
});
assert_equals(message, "RUN");
}, "Splitting script source via Text.splitText() keeps trustworthiness.");
promise_test(async t => {
let message = await script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`${LOG_RUN_MESSAGE};;;`);
script.firstChild.splitText(3);
script.normalize();
container.appendChild(script);
});
assert_equals(message, "RUN");
}, "Normalizing script source via Element.normalize() keeps trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.nodeValue = LOG_RUN_MESSAGE;
container.appendChild(script);
});
}, "Script source set via Node.nodeValue drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.data = LOG_RUN_MESSAGE;
container.appendChild(script);
});
}, "Setting script source via CharacterData.data drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.appendData(LOG_RUN_MESSAGE);
container.appendChild(script);
});
}, "Setting script source via CharacterData.appendData() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.insertData(0, LOG_RUN_MESSAGE);
container.appendChild(script);
});
}, "Setting script source via CharacterData.insertData() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.replaceData(0, 0, LOG_RUN_MESSAGE);
container.appendChild(script);
});
}, "Setting script source via CharacterData.replaceData() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`//${LOG_RUN_MESSAGE}`);
script.firstChild.deleteData(0, 2);
container.appendChild(script);
});
}, "Setting script source via CharacterData.deleteData() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.before(LOG_RUN_MESSAGE);
container.appendChild(script);
});
}, "Setting script source via CharacterData.before() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.after(LOG_RUN_MESSAGE);
container.appendChild(script);
});
}, "Setting script source via CharacterData.after() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`;;;${LOG_RUN_MESSAGE}`);
script.firstChild.splitText(3);
script.firstChild.remove();
container.appendChild(script);
});
}, "Setting script source via CharacterData.remove() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.firstChild.replaceWith(document.createTextNode(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Setting script source via CharacterData.replaceWith() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.appendChild(document.createTextNode(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Setting script source via Node.appendChild() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.insertBefore(document.createTextNode(LOG_RUN_MESSAGE), script.firstChild);
container.appendChild(script);
});
}, "Setting script source via Node.insertBefore() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.replaceChild(document.createTextNode(LOG_RUN_MESSAGE), script.firstChild);
container.appendChild(script);
});
}, "Setting script source via Node.replaceChild() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`;;;${LOG_RUN_MESSAGE}`);
script.firstChild.splitText(3);
script.removeChild(script.firstChild);
container.appendChild(script);
});
}, "Setting script source via Node.removeChild() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.prepend(document.createTextNode(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Setting script source via Element.prepend() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.append(document.createTextNode(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Setting script source via Element.append() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.replaceChildren(document.createTextNode(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Setting script source via Element.replaceChildren() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
let text = document.createTextNode(LOG_RUN_MESSAGE);
// Per https://dom.spec.whatwg.org/#move, step 1, moveBefore requires
// the two nodes to have the same shadow-including root.
let root = document.createElement("div");
root.appendChild(script);
root.appendChild(text);
script.moveBefore(text, script.firstChild);
container.appendChild(script);
});
}, "Setting script source via Element.moveBefore() drops trustworthiness.");
promise_test(async t => {
await promise_rejects_js(t, TypeError, script_messages_for(_ => {
document.createElement("script").insertAdjacentHTML("afterbegin", LOG_RUN_MESSAGE);
}), "TrustedHTML required.");
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.insertAdjacentHTML("afterbegin", passthroughpolicy.createHTML(LOG_RUN_MESSAGE));
container.appendChild(script);
});
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.insertAdjacentHTML("beforeend", passthroughpolicy.createHTML(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Setting script source via TrustedHTML sink Node.insertAdjacentHTML() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.insertAdjacentText("afterbegin", LOG_RUN_MESSAGE);
container.appendChild(script);
});
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(";");
script.insertAdjacentText("beforeend", LOG_RUN_MESSAGE);
container.appendChild(script);
});
}, "Setting script source via Node.insertAdjacentText() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`;`);
let range = new Range();
range.selectNode(script.firstChild);
range.insertNode(document.createTextNode(LOG_RUN_MESSAGE));
container.appendChild(script);
});
}, "Setting script source via Range.insertNode() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`//;;;${LOG_RUN_MESSAGE}`);
script.firstChild.splitText(2);
let range = new Range();
range.setStart(script.firstChild, 0);
range.setEnd(script.lastChild, 3);
range.deleteContents();
container.appendChild(script);
});
}, "Setting script source via Range.deleteContents() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let script = create_html_script_with_trusted_source_text(`${LOG_RUN_MESSAGE}`);
let clone = script.cloneNode(true);
container.appendChild(clone);
});
}, "Cloning a script via Node.cloneNode() drops trustworthiness.");
promise_test(async t => {
await no_script_message_for(_ => {
let div = document.createElement("div");
let script = create_html_script_with_trusted_source_text(`${LOG_RUN_MESSAGE}`);
div.appendChild(script);
let range = new Range();
range.selectNode(script);
let documentFragment = range.cloneContents();
container.appendChild(documentFragment.firstElementChild);
});
}, "Cloning a script via Range.cloneContents() drops trustworthiness.");
</script>
</body>

View file

@ -0,0 +1,7 @@
'use strict'
const passthroughpolicy = trustedTypes.createPolicy("passthroughpolicy", {
createHTML: s => s,
createScript: s => s,
createScriptURL: s => s,
});

View file

@ -0,0 +1,104 @@
'use strict'
const LOG_RUN_MESSAGE = `window.log_message("RUN")`;
function create_html_script_with_trusted_source_text(source_text) {
let script = document.createElement("script");
script.text = passthroughpolicy.createScript(source_text);
return script;
}
function create_html_script_with_untrusted_source_text(source_text) {
let script = document.createElement("script");
// Setting script source via Node.appendChild() drops trustworthiness.
script.appendChild(document.createTextNode(source_text));
return script;
}
function create_svg_script_with_trusted_source_text(source_text) {
// SVGScriptElement has no API to set its source while preserving its
// trustworthiness. For now, we just expect a <script type="unknown"> tag
// with the desired source to already be present in the page, so we can just
// reuse it. See https://github.com/w3c/trusted-types/issues/512
let script =
Array.from(document.querySelectorAll("svg script[type='unknown']")).
find(script => script.textContent === source_text);
assert_true(!!script, `<script type="unknown">${source_text}</script> not found!`);
script.remove();
script.removeAttribute("type");
return script;
}
function create_svg_script_with_untrusted_source_text(source_text) {
let script = document.createElementNS(NSURI_SVG, "script")
// Setting script source via Node.appendChild() drops trustworthiness.
script.appendChild(document.createTextNode(source_text));
return script;
}
// A generic helper that runs function fn and returns a promise resolving with
// an array of received messages. A script forcing a "done" message is appended
// after calling fn, to make sure that all the messages reported by fn have been
// delivered.
function script_messages_for(fn) {
return new Promise(async (resolve, reject) => {
// Listen for messages.
let messages = [];
let exception = null;
window.log_message = message => {
if (message === "DONE") {
window.log_message = null;
if (exception) {
reject(exception);
} else {
resolve(messages);
}
} else {
messages.push(message);
}
};
// Execute the function.
try {
await fn();
} catch(e) {
exception = e;
}
// Indicate the last message.
// This is done by appending an inline script to make sure it is executed
// after processing any previously inserted inline script. Additionally, we
// delay by a double requestAnimationFrame to work around incompatible
// interop bugs:
// - WebKit/Chromium seems to give lower priority to module, so it looks
// like the appended script should have type="module" here to work with
// tests for inline modules.
// - but Firefox does not allow type="importmaps" after a type="module" so
// making the appended script a module would make importmap tests fail...
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1916277#c4
requestAnimationFrame(_ => requestAnimationFrame(_ => {
let script = create_html_script_with_trusted_source_text(`window.log_message("DONE")`);
script.setAttribute("nonce", "script-messages");
document.body.appendChild(script);
}));
});
}
async function script_message_for(fn) {
let messages = await script_messages_for(fn);
assert_equals(messages.length, 1, `Number of messages (${messages})`);
return messages[0];
}
async function no_script_message_for(fn) {
let messages = await script_messages_for(fn);
assert_equals(messages.length, 0, `Number of messages (${messages})`);
}
async function base64_hash_for_inline_script(source_text, algorithm) {
const encoder = new TextEncoder();
const data = encoder.encode(source_text);
const hashBuffer = await window.crypto.subtle.digest(algorithm, data);
const base64Array = (new Uint8Array(hashBuffer)).toBase64();
return base64Array.toString();
}