fix(trusted-types): enforce Element attribute sinks

This commit is contained in:
ldm0
2026-09-22 21:43:32 +08:00
parent 2332ca1f26
commit e0b2f6d9e4
9 changed files with 447 additions and 33 deletions
@@ -264,7 +264,7 @@ pub(crate) fn trusted_script_url_string_or_throw<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
requirements: TrustedTypesForScriptRequirements,
sink: &'static str,
sink: &str,
api_name: &'static str,
) -> Option<String> {
trusted_type_string_or_throw(
@@ -282,7 +282,7 @@ pub(crate) fn trusted_html_string_or_throw<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
requirements: TrustedTypesForScriptRequirements,
sink: &'static str,
sink: &str,
api_name: &'static str,
) -> Option<String> {
trusted_type_string_or_throw(
@@ -307,7 +307,7 @@ pub(crate) fn trusted_script_string_or_type_error<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
requirements: TrustedTypesForScriptRequirements,
sink: &'static str,
sink: &str,
api_name: &'static str,
) -> Option<String> {
trusted_type_string_or_throw(
@@ -324,7 +324,7 @@ pub(crate) fn trusted_script_string_or_type_error<'s>(
pub(crate) fn trusted_script_string_for_script_element_execution(
scope: &mut v8::PinScope<'_, '_>,
original: &str,
sink: &'static str,
sink: &str,
) -> Option<String> {
let default_value = {
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
@@ -572,7 +572,7 @@ fn trusted_type_string_or_throw<'s>(
value: v8::Local<'s, v8::Value>,
kind: TrustedTypeKind,
requirements: TrustedTypesForScriptRequirements,
sink: &'static str,
sink: &str,
api_name: &'static str,
error_kind: TrustedTypeErrorKind,
) -> Option<String> {
@@ -607,7 +607,7 @@ fn apply_default_trusted_type_policy<'s>(
scope: &mut v8::PinScope<'s, '_>,
input: &str,
kind: TrustedTypeKind,
sink: &'static str,
sink: &str,
error_kind: TrustedTypeErrorKind,
) -> Option<String> {
match apply_default_trusted_type_policy_outcome(scope, input, kind, sink) {
@@ -625,7 +625,7 @@ fn apply_default_trusted_type_policy_outcome<'s>(
scope: &mut v8::PinScope<'s, '_>,
input: &str,
kind: TrustedTypeKind,
sink: &'static str,
sink: &str,
) -> DefaultTrustedTypePolicyOutcome {
let global = scope.get_current_context().global(scope);
let Some(policy) = get_private_value(scope, global, TRUSTED_TYPES_DEFAULT_POLICY_SLOT)
@@ -637,7 +637,9 @@ fn apply_default_trusted_type_policy_outcome<'s>(
return DefaultTrustedTypePolicyOutcome::Exception;
};
let type_name = v8str(scope, kind.constructor_name());
let sink = v8str(scope, sink);
let Some(sink) = v8_string(scope, sink) else {
return DefaultTrustedTypePolicyOutcome::Exception;
};
let args = [input.into(), type_name.into(), sink.into()];
match invoke_policy_callback(scope, policy, kind, &args) {
TrustedTypePolicyCallbackOutcome::Missing => DefaultTrustedTypePolicyOutcome::Unavailable,
@@ -1240,7 +1242,7 @@ fn throw_trusted_type_error(
error_kind: TrustedTypeErrorKind,
api_name: &'static str,
kind: TrustedTypeKind,
sink: &'static str,
sink: &str,
) {
let message = format!(
"Failed to execute '{api_name}': This document requires '{}' assignment for the '{}' sink.",
@@ -233,11 +233,15 @@ pub(in crate::native_bridge) fn bridge_detached_set_attribute_callback<'a>(
);
return;
}
let Some(value) = args
.get(2)
.to_string(scope)
.map(|value| value.to_rust_string_lossy(scope))
else {
let runtime_and_handle = detached_surface_runtime_and_handle(scope, args.this(), element);
let Some(value) = super::super::super::element::trusted_attribute_value_string(
scope,
runtime_and_handle,
None,
&normalized,
args.get(2),
super::super::super::element::TrustedAttributeSetter::SetAttribute,
) else {
return;
};
with_detached_surface_attribute_reaction_scope(scope, args.this(), element, |scope| {
@@ -293,11 +297,15 @@ pub(in crate::native_bridge) fn bridge_detached_set_attribute_ns_callback<'a>(
);
return;
}
let Some(value) = args
.get(3)
.to_string(scope)
.map(|value| value.to_rust_string_lossy(scope))
else {
let runtime_and_handle = detached_surface_runtime_and_handle(scope, args.this(), element);
let Some(value) = super::super::super::element::trusted_attribute_value_string(
scope,
runtime_and_handle,
namespace.as_deref(),
&local_name,
args.get(3),
super::super::super::element::TrustedAttributeSetter::SetAttributeNs,
) else {
return;
};
with_detached_surface_attribute_reaction_scope(scope, args.this(), element, |scope| {
@@ -54,6 +54,9 @@ mod trusted_types;
pub(crate) use script_execution::{
inline_script_source_for_execution, prepare_inline_classic_frame_script_job_for_execution,
};
pub(in crate::native_bridge) use trusted_types::{
TrustedAttributeSetter, trusted_attribute_value_string,
};
use trusted_types::{
TrustedScriptElementSink, trusted_script_element_sink_string, trusted_script_url_sink_string,
};
@@ -22,22 +22,22 @@ struct AttributeNamespaceNameArgs {
#[derive(crate::webidl::WebIdlArgs)]
#[webidl(prefix = "Element setAttribute")]
struct SetAttributeArgs {
struct SetAttributeArgs<'s> {
#[webidl(required)]
name: String,
#[webidl(required)]
value: String,
#[webidl(required, converter = "raw")]
value: v8::Local<'s, v8::Value>,
}
#[derive(crate::webidl::WebIdlArgs)]
#[webidl(prefix = "Element setAttributeNS")]
struct SetAttributeNsArgs {
struct SetAttributeNsArgs<'s> {
#[webidl(required, nullable)]
namespace: Option<String>,
#[webidl(required)]
qualified_name: String,
#[webidl(required)]
value: String,
#[webidl(required, converter = "raw")]
value: v8::Local<'s, v8::Value>,
}
#[derive(crate::webidl::WebIdlArgs)]
@@ -12,11 +12,12 @@ use super::super::super::{
throw_dom_exception, validate_attribute_name, validate_qualified_name_and_namespace,
};
use super::super::{
element_has_attribute, remove_live_element_attribute_appending_to_current_reaction_queue,
TrustedAttributeSetter, element_has_attribute,
remove_live_element_attribute_appending_to_current_reaction_queue,
remove_live_element_attribute_ns_appending_to_current_reaction_queue,
set_live_element_attribute_appending_to_current_reaction_queue,
set_live_element_attribute_ns_appending_to_current_reaction_queue,
update_iframe_snapshot_navigation,
trusted_attribute_value_string, update_iframe_snapshot_navigation,
};
use super::{
AttributeNameArgs, AttributeNamespaceNameArgs, SetAttributeArgs, SetAttributeNsArgs,
@@ -88,12 +89,28 @@ pub(in crate::native_bridge) fn node_set_attribute_callback<'s>(
throw_invalid_attribute_name(scope);
return;
}
let runtime = unsafe { &*runtime_ptr };
let normalized_name = runtime
.dom_host()
.node(handle)
.and_then(Node::as_element)
.map(|element| element.normalized_attribute_name(&parsed.name))
.unwrap_or_else(|| parsed.name.clone());
let Some(value) = trusted_attribute_value_string(
scope,
Some((runtime_ptr, handle)),
None,
&normalized_name,
parsed.value,
TrustedAttributeSetter::SetAttribute,
) else {
rv.set_undefined();
return;
};
if parsed.name.eq_ignore_ascii_case("src")
&& unsafe { &*runtime_ptr }
.dom_host()
.is_html_element_named(handle, "iframe")
&& runtime.dom_host().is_html_element_named(handle, "iframe")
{
update_iframe_snapshot_navigation(scope, runtime_ptr, handle, &parsed.value);
update_iframe_snapshot_navigation(scope, runtime_ptr, handle, &value);
rv.set_undefined();
return;
}
@@ -103,7 +120,7 @@ pub(in crate::native_bridge) fn node_set_attribute_callback<'s>(
runtime_ptr,
handle,
&parsed.name,
&parsed.value,
&value,
);
});
rv.set_undefined();
@@ -136,6 +153,17 @@ pub(in crate::native_bridge) fn node_set_attribute_ns_callback<'s>(
return;
}
};
let Some(value) = trusted_attribute_value_string(
scope,
Some((runtime_ptr, handle)),
namespace.as_deref(),
&local_name,
parsed.value,
TrustedAttributeSetter::SetAttributeNs,
) else {
rv.set_undefined();
return;
};
custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
let _ = set_live_element_attribute_ns_appending_to_current_reaction_queue(
scope,
@@ -145,7 +173,7 @@ pub(in crate::native_bridge) fn node_set_attribute_ns_callback<'s>(
prefix.as_deref(),
&local_name,
&parsed.qualified_name,
&parsed.value,
&value,
);
});
rv.set_undefined();
@@ -1,4 +1,5 @@
use crate::{
context_bootstrap::WINDOW_EVENT_HANDLER_PROPERTIES,
document_runtime::EventTargetHandle,
util::{
context_host_ptr_from_global_bridge, node_wrapper_from_handle, throw_type_error, v8_string,
@@ -110,6 +111,25 @@ const DOCUMENT_EVENT_HANDLER_PROPERTIES: &[&str] = &[
"onpointerlockerror",
"onreadystatechange",
];
const ELEMENT_SPECIFIC_EVENT_HANDLER_PROPERTIES: &[&str] = &[
"onencrypted",
"onwaitingforkey",
"onbegin",
"onend",
"onrepeat",
];
pub(in crate::native_bridge::element) fn is_element_event_handler_content_attribute_name(
name: &str,
) -> bool {
GENERIC_EVENT_HANDLER_PROPERTIES
.iter()
.chain(WINDOW_EVENT_HANDLER_PROPERTIES)
.chain(ELEMENT_SPECIFIC_EVENT_HANDLER_PROPERTIES)
.copied()
.chain(["onmessageerror"])
.any(|candidate| candidate == name)
}
#[derive(Clone, Copy)]
pub(crate) enum GlobalEventHandlerOwner {
@@ -14,6 +14,7 @@ pub(crate) use body_window::compile_window_body_onmessageerror_attribute;
pub(super) use body_window::{
body_onmessageerror_getter_function, body_onmessageerror_setter_function,
};
pub(super) use generic::is_element_event_handler_content_attribute_name;
pub(crate) use generic::{
GlobalEventHandlerOwner, install_global_event_handler_template_bindings,
node_event_handler_getter_function, node_event_handler_setter_function,
@@ -1,6 +1,33 @@
use super::JsContextHost;
use crate::document_runtime::DomHandle;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::native_bridge) enum TrustedAttributeSetter {
SetAttribute,
SetAttributeNs,
}
impl TrustedAttributeSetter {
fn api_name(self) -> &'static str {
match self {
Self::SetAttribute => "setAttribute",
Self::SetAttributeNs => "setAttributeNS",
}
}
fn conversion_context(self) -> crate::webidl::Context {
match self {
Self::SetAttribute => crate::webidl::Context::argument("Element setAttribute", 2),
Self::SetAttributeNs => crate::webidl::Context::argument("Element setAttributeNS", 3),
}
}
}
enum TrustedAttributeSink {
Script(String),
ScriptUrl(&'static str),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::native_bridge::element) enum TrustedHtmlSink {
ElementInnerHtml,
@@ -126,6 +153,102 @@ pub(in crate::native_bridge::element) fn trusted_script_url_sink_string<'s>(
)
}
pub(in crate::native_bridge) fn trusted_attribute_value_string<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_and_handle: Option<(*mut JsContextHost, DomHandle)>,
attribute_namespace: Option<&str>,
local_name: &str,
value: v8::Local<'s, v8::Value>,
setter: TrustedAttributeSetter,
) -> Option<String> {
let sink = runtime_and_handle.and_then(|(runtime_ptr, handle)| {
let element = unsafe { &*runtime_ptr }
.dom_host()
.node(handle)
.and_then(|node| node.as_element())?;
let element_namespace = element.namespace();
if attribute_namespace.is_none()
&& matches!(
element_namespace,
"http://www.w3.org/1999/xhtml"
| "http://www.w3.org/2000/svg"
| "http://www.w3.org/1998/Math/MathML"
)
&& super::event_handlers::is_element_event_handler_content_attribute_name(local_name)
{
return Some((
runtime_ptr,
TrustedAttributeSink::Script(format!("Element {local_name}")),
));
}
let sink = match (
element_namespace,
element.local_name(),
attribute_namespace,
local_name,
) {
("http://www.w3.org/1999/xhtml", "script", None, "src") => {
Some(TrustedAttributeSink::ScriptUrl("HTMLScriptElement src"))
}
("http://www.w3.org/1999/xhtml", "embed", None, "src") => {
Some(TrustedAttributeSink::ScriptUrl("HTMLEmbedElement src"))
}
("http://www.w3.org/1999/xhtml", "object", None, "data") => {
Some(TrustedAttributeSink::ScriptUrl("HTMLObjectElement data"))
}
("http://www.w3.org/1999/xhtml", "object", None, "codebase") => Some(
TrustedAttributeSink::ScriptUrl("HTMLObjectElement codebase"),
),
(
"http://www.w3.org/2000/svg",
"script",
None | Some("http://www.w3.org/1999/xlink"),
"href",
) => Some(TrustedAttributeSink::ScriptUrl("SVGScriptElement href")),
_ => None,
}?;
Some((runtime_ptr, sink))
});
if let Some((runtime_ptr, sink)) = sink {
let requirements = unsafe { &*runtime_ptr }.trusted_types_for_script_requirements(scope);
return match sink {
TrustedAttributeSink::Script(sink) => {
crate::context_bootstrap::trusted_script_string_or_type_error(
scope,
value,
requirements,
&sink,
setter.api_name(),
)
}
TrustedAttributeSink::ScriptUrl(sink) => {
crate::context_bootstrap::trusted_script_url_string_or_throw(
scope,
value,
requirements,
sink,
setter.api_name(),
)
}
};
}
match crate::webidl::convert::<crate::webidl::DomString>(
scope,
value,
setter.conversion_context(),
) {
Ok(value) => Some(value.0),
Err(error) => {
crate::webidl::throw_error(scope, &error);
None
}
}
}
pub(crate) fn trusted_script_source_for_execution(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
@@ -958,6 +958,235 @@ fn script_src_enforces_trusted_script_url_and_applies_the_default_policy() {
);
}
#[test]
fn event_handler_attribute_writes_enforce_trusted_script_at_the_attribute_boundary() {
let mut vm = new_storage_test_vm("https://event-handler-attribute-trusted-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
globalThis.__eventHandlerAttributeRuns = [];
const root = document.body ||
(document.documentElement || document.appendChild(document.createElement("html")))
.appendChild(document.createElement("body"));
const policy = trustedTypes.createPolicy("event-handler-script", {
createScript: value => value,
createHTML: value => value
});
const trusted = document.createElement("button");
trusted.setAttribute(
"onclick",
policy.createScript("globalThis.__eventHandlerAttributeRuns.push('trusted')")
);
root.appendChild(trusted);
trusted.click();
const trustedNs = document.createElement("button");
trustedNs.setAttributeNS(
null,
"onclick",
policy.createScript("globalThis.__eventHandlerAttributeRuns.push('trusted-ns')")
);
root.appendChild(trustedNs);
trustedNs.click();
const unsuitable = document.createElement("button");
let unsuitableRejected = false;
try {
unsuitable.setAttribute(
"onclick",
policy.createHTML("globalThis.__eventHandlerAttributeRuns.push('unsuitable')")
);
} catch (error) {
unsuitableRejected = error instanceof TypeError;
}
const plain = document.createElement("button");
let plainRejected = false;
try {
plain.setAttribute(
"onclick",
"globalThis.__eventHandlerAttributeRuns.push('plain')"
);
} catch (error) {
plainRejected = error instanceof TypeError;
}
const plainNs = document.createElement("button");
let plainNsRejected = false;
try {
plainNs.setAttributeNS(
null,
"onclick",
"globalThis.__eventHandlerAttributeRuns.push('plain-ns')"
);
} catch (error) {
plainNsRejected = error instanceof TypeError;
}
const ordinary = document.createElement("div");
ordinary.setAttribute("data-onclick", "plain-data");
ordinary.setAttribute("ondoesnotexist", "plain-unknown");
ordinary.setAttributeNS("urn:test", "onclick", "plain-namespaced");
const uppercaseSvg = document.createElementNS("http://www.w3.org/2000/svg", "g");
uppercaseSvg.setAttributeNS(null, "ONCLICK", "plain-uppercase");
const elementSpecificRejected = ["onwaitingforkey", "onbegin"].map(name => {
try {
document.createElement("div").setAttribute(name, "plain-special");
return false;
} catch (error) {
return error instanceof TypeError;
}
});
const defaultCalls = [];
trustedTypes.createPolicy("default", {
createScript: (value, type, sink) => {
defaultCalls.push([value, type, sink]);
return "globalThis.__eventHandlerAttributeRuns.push('default')";
}
});
const defaulted = document.createElement("button");
defaulted.setAttribute("onclick", "default-input");
root.appendChild(defaulted);
defaulted.click();
return JSON.stringify({
runs: globalThis.__eventHandlerAttributeRuns,
unsuitableRejected,
unsuitableAttribute: unsuitable.getAttribute("onclick"),
plainRejected,
plainAttribute: plain.getAttribute("onclick"),
plainNsRejected,
plainNsAttribute: plainNs.getAttribute("onclick"),
ordinary: [
ordinary.getAttribute("data-onclick"),
ordinary.getAttribute("ondoesnotexist"),
ordinary.getAttributeNS("urn:test", "onclick"),
uppercaseSvg.getAttribute("ONCLICK")
],
elementSpecificRejected,
defaultedAttribute: defaulted.getAttribute("onclick"),
defaultCalls
});
})()
"#,
)
.expect("event handler attribute Trusted Types sink probe should evaluate");
assert_eq!(
result,
r#"{"runs":["trusted","trusted-ns","default"],"unsuitableRejected":true,"unsuitableAttribute":null,"plainRejected":true,"plainAttribute":null,"plainNsRejected":true,"plainNsAttribute":null,"ordinary":["plain-data","plain-unknown","plain-namespaced","plain-uppercase"],"elementSpecificRejected":[true,true],"defaultedAttribute":"globalThis.__eventHandlerAttributeRuns.push('default')","defaultCalls":[["default-input","TrustedScript","Element onclick"]]}"#
);
}
#[test]
fn url_attribute_writes_enforce_the_non_iframe_trusted_script_url_sink_table() {
let mut vm = new_storage_test_vm("https://url-attribute-trusted-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
const xlink = "http://www.w3.org/1999/xlink";
const svg = "http://www.w3.org/2000/svg";
const policy = trustedTypes.createPolicy("attribute-url", {
createScript: value => value,
createScriptURL: value => value
});
const trusted = value => policy.createScriptURL(value);
const htmlScript = document.createElement("script");
htmlScript.setAttribute("src", trusted("script.js"));
const htmlScriptNs = document.createElement("script");
htmlScriptNs.setAttributeNS(null, "src", trusted("script-ns.js"));
const svgScript = document.createElementNS(svg, "script");
svgScript.setAttribute("href", trusted("svg.js"));
const svgScriptXlink = document.createElementNS(svg, "script");
svgScriptXlink.setAttributeNS(xlink, "xlink:href", trusted("svg-xlink.js"));
const embed = document.createElement("embed");
embed.setAttribute("src", trusted("embed.js"));
const object = document.createElement("object");
object.setAttribute("data", trusted("object-data.js"));
object.setAttribute("codebase", trusted("object-codebase.js"));
const rejected = [];
for (const [element, setter] of [
[document.createElement("script"), element => element.setAttribute("src", "plain")],
[document.createElement("script"), element => element.setAttributeNS(null, "src", null)],
[document.createElementNS(svg, "script"), element => element.setAttribute("href", "plain")],
[document.createElementNS(svg, "script"), element => element.setAttributeNS(xlink, "xlink:href", policy.createScript("wrong"))],
[document.createElement("embed"), element => element.setAttribute("src", "plain")],
[document.createElement("object"), element => element.setAttribute("data", "plain")],
[document.createElement("object"), element => element.setAttribute("codebase", "plain")]
]) {
try {
setter(element);
rejected.push(false);
} catch (error) {
rejected.push(error instanceof TypeError);
}
}
const ordinary = document.createElement("script");
ordinary.setAttributeNS("urn:test", "src", "namespaced");
const uppercaseSvg = document.createElementNS(svg, "script");
uppercaseSvg.setAttributeNS(null, "HREF", "uppercase");
const div = document.createElement("div");
div.setAttribute("src", "plain-div");
const defaultCalls = [];
trustedTypes.createPolicy("default", {
createScriptURL: (value, type, sink) => {
defaultCalls.push([value, type, sink]);
return `safe-${value}`;
}
});
const defaultHtml = document.createElement("script");
defaultHtml.setAttribute("src", "default-html");
const defaultSvg = document.createElementNS(svg, "script");
defaultSvg.setAttributeNS(xlink, "xlink:href", "default-svg");
const defaultObject = document.createElement("object");
defaultObject.setAttribute("codebase", "default-object");
return JSON.stringify({
trustedValues: [
htmlScript.getAttribute("src"),
htmlScriptNs.getAttribute("src"),
svgScript.getAttribute("href"),
svgScriptXlink.getAttributeNS(xlink, "href"),
embed.getAttribute("src"),
object.getAttribute("data"),
object.getAttribute("codebase")
],
rejected,
ordinary: [
ordinary.getAttributeNS("urn:test", "src"),
uppercaseSvg.getAttribute("HREF"),
div.getAttribute("src")
],
defaultValues: [
defaultHtml.getAttribute("src"),
defaultSvg.getAttributeNS(xlink, "href"),
defaultObject.getAttribute("codebase")
],
defaultCalls
});
})()
"#,
)
.expect("URL attribute Trusted Types sink table probe should evaluate");
assert_eq!(
result,
r#"{"trustedValues":["script.js","script-ns.js","svg.js","svg-xlink.js","embed.js","object-data.js","object-codebase.js"],"rejected":[true,true,true,true,true,true,true],"ordinary":["namespaced","uppercase","plain-div"],"defaultValues":["safe-default-html","safe-default-svg","safe-default-object"],"defaultCalls":[["default-html","TrustedScriptURL","HTMLScriptElement src"],["default-svg","TrustedScriptURL","SVGScriptElement href"],["default-object","TrustedScriptURL","HTMLObjectElement codebase"]]}"#
);
}
#[test]
fn empty_default_policy_reports_each_rejected_element_sink() {
let mut vm = new_storage_test_vm("https://empty-default-policy.test/");