fix(svg): reflect animated strings through trusted types

This commit is contained in:
ldm0
2026-09-09 06:38:21 +08:00
parent 28f1e852f2
commit dfbcf3e88a
11 changed files with 424 additions and 2 deletions
@@ -3736,7 +3736,6 @@ trusted-types/trusted-types-reporting-for-ServiceWorker-ServiceWorkerContainer-r
trusted-types/trusted-types-reporting-for-SharedWorker-ServiceWorkerContainer-register.https.html
trusted-types/trusted-types-reporting.html
trusted-types/trusted-types-secondary-document.html
trusted-types/trusted-types-svg-script-set-href.html
uievents/order-of-events/focus-events/focus-automated-blink-webkit.html
url/a-element.html?exclude=(file|javascript|mailto)
url/a-element.html?include=file
@@ -7913,9 +7913,11 @@ trusted-types/trusted-types-reporting-for-Window-SharedWorker-constructor.html
trusted-types/trusted-types-reporting-for-Window-eval.html
trusted-types/trusted-types-reporting-for-Window-function-constructor.html
trusted-types/trusted-types-reporting-for-Window-setTimeout-setInterval.html
trusted-types/trusted-types-reporting.html
trusted-types/trusted-types-sandbox-allow-scripts.html
trusted-types/trusted-types-sandbox-no-allow-scripts.html
trusted-types/trusted-types-source-file-path.html
trusted-types/trusted-types-svg-script-set-href.html
trusted-types/trusted-types-tojson.html
trusted-types/tt-block-eval.html
uievents/constructors/inputevent-constructor.html
@@ -1696,6 +1696,7 @@ pub(crate) fn finish_context_bootstrap(
("SVGLength", "SVGLength"),
("SVGNumber", "SVGNumber"),
("SVGRect", "SVGRect"),
("SVGAnimatedString", "SVGAnimatedString"),
("SVGAnimatedLength", "SVGAnimatedLength"),
("SVGLengthList", "SVGLengthList"),
("SVGAnimatedLengthList", "SVGAnimatedLengthList"),
@@ -113,6 +113,11 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGAnimatedString",
parent: None,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
name: "SVGAnimatedLength",
parent: None,
@@ -499,6 +499,26 @@ struct SvgLengthTemplateAccessorsDeclaration {
value_as_string: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGAnimatedString", enumerable)]
struct SvgAnimatedStringTemplateAccessorsDeclaration {
#[webapi(
accessor_property = "baseVal",
getter = svg_animated_string_getter,
setter = svg_animated_string_setter,
data = callback_data_index_value(scope, 0)
)]
base_val: (),
#[webapi(
accessor_property = "animVal",
getter = svg_animated_string_getter,
data = callback_data_index_value(scope, 1)
)]
anim_val: (),
}
#[allow(dead_code)]
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGAnimatedLength", enumerable)]
struct SvgAnimatedLengthTemplateAccessorsDeclaration {
@@ -876,6 +896,23 @@ struct SvgGraphicsBoxElementPrototypeAccessorsDeclaration {
height: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGElement", enumerable)]
struct SvgElementPrototypeAccessorsDeclaration {
#[webapi(
accessor_property = "className",
getter = svg_element_class_name_getter
)]
class_name: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "SVGURIReference", enumerable)]
struct SvgUriReferencePrototypeAccessorsDeclaration {
#[webapi(accessor_property = "href", getter = svg_uri_href_getter)]
href: (),
}
pub(super) fn install_svg_length_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -902,6 +939,14 @@ pub(super) fn install_svg_number_bindings<'s>(
SvgNumberTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
}
pub(super) fn install_svg_animated_string_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
let proto = template.prototype_template(scope);
SvgAnimatedStringTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
}
pub(super) fn install_svg_animated_length_list_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -1068,6 +1113,9 @@ pub(super) fn install_svg_element_accessor_bindings<'s>(
SvgPatternElementPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
}
"SVGGradientElement" => {
SvgGradientElementPrototypeAccessorsDeclaration::initialize_prototype_template(
@@ -1094,7 +1142,28 @@ pub(super) fn install_svg_element_accessor_bindings<'s>(
scope, prototype,
);
}
"SVGImageElement" | "SVGUseElement" | "SVGForeignObjectElement" => {
"SVGElement" => {
SvgElementPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
}
"SVGAElement"
| "SVGLinearGradientElement"
| "SVGRadialGradientElement"
| "SVGScriptElement" => {
SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
}
"SVGImageElement" | "SVGUseElement" => {
SvgGraphicsBoxElementPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
SvgUriReferencePrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
}
"SVGForeignObjectElement" => {
SvgGraphicsBoxElementPrototypeAccessorsDeclaration::initialize_prototype_template(
scope, prototype,
);
@@ -3,6 +3,18 @@ use super::*;
use crate::util::serialize_v8_iter_array;
use moli_webapi_declare::WebApiObject;
#[derive(WebApiObject)]
#[webapi(
interface = "SVGAnimatedString",
own_to_string_tag = "SVGAnimatedString"
)]
struct SvgAnimatedStringObjectDeclaration {
#[webapi(slot = SVG_ANIMATED_STRING_BASE_VAL_SLOT)]
base_val: String,
#[webapi(slot = SVG_ANIMATED_STRING_ANIM_VAL_SLOT)]
anim_val: String,
}
#[derive(WebApiObject)]
#[webapi(
interface = "SVGAnimatedNumber",
@@ -250,6 +262,19 @@ struct SvgTransformObjectDeclaration<'scope> {
set_skew_y: (),
}
pub(super) fn build_svg_animated_string_for_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
attribute: &str,
) -> v8::Local<'s, v8::Object> {
let value = svg_owner_attribute_value(scope, owner, attribute).unwrap_or_default();
let object = SvgAnimatedStringObjectDeclaration::new(value.clone(), value)
.bind(scope)
.expect("SVGAnimatedString declaration should bind");
set_svg_animated_string_owner_attribute(scope, object, owner, attribute);
object
}
pub(super) fn build_svg_animated_length_list<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> v8::Local<'s, v8::Object> {
@@ -1787,6 +1812,69 @@ pub(super) fn sync_svg_animated_length_from_owner_attribute<'s>(
}
}
pub(super) fn set_svg_animated_string_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
animated: v8::Local<'s, v8::Object>,
owner: v8::Local<'s, v8::Object>,
attribute: &str,
) {
set_private_value(
scope,
animated,
SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT,
owner.into(),
);
set_private_value(
scope,
animated,
SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT,
v8_string(scope, attribute)
.unwrap_or_else(|| v8str(scope, ""))
.into(),
);
}
pub(super) fn sync_svg_animated_string_from_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
animated: v8::Local<'s, v8::Object>,
) {
let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return;
};
let Some(attribute) =
get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT)
.and_then(|value| value.to_string(scope))
.map(|value| value.to_rust_string_lossy(scope))
.filter(|value| !value.is_empty())
else {
return;
};
let value = svg_owner_attribute_value(scope, owner, &attribute).unwrap_or_default();
set_svg_animated_string_values(scope, animated, &value);
}
pub(super) fn set_svg_animated_string_values(
scope: &mut v8::PinScope<'_, '_>,
animated: v8::Local<'_, v8::Object>,
value: &str,
) {
let value = v8_string(scope, value).unwrap_or_else(|| v8str(scope, ""));
set_private_value(
scope,
animated,
SVG_ANIMATED_STRING_BASE_VAL_SLOT,
value.into(),
);
set_private_value(
scope,
animated,
SVG_ANIMATED_STRING_ANIM_VAL_SLOT,
value.into(),
);
}
pub(super) fn set_svg_animated_number_owner_attribute<'s>(
scope: &mut v8::PinScope<'s, '_>,
animated: v8::Local<'s, v8::Object>,
@@ -46,6 +46,42 @@ fn require_svg_receiver<'s>(
false
}
pub(super) fn svg_element_class_name_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'_, v8::Value>,
) {
svg_animated_string_attribute_getter(scope, args, rv, SVG_ELEMENT_CLASS_NAME_SLOT, "class");
}
pub(super) fn svg_uri_href_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'_, v8::Value>,
) {
svg_animated_string_attribute_getter(scope, args, rv, SVG_URI_HREF_SLOT, "href");
}
fn svg_animated_string_attribute_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
slot: &str,
attribute: &str,
) {
let owner = args.this();
if let Some(value) = get_private_value(scope, owner, slot) {
if let Ok(animated) = v8::Local::<v8::Object>::try_from(value) {
sync_svg_animated_string_from_owner_attribute(scope, animated);
}
rv.set(value);
return;
}
let value = build_svg_animated_string_for_attribute(scope, owner, attribute);
set_private_value(scope, owner, slot, value.into());
rv.set(value.into());
}
pub(super) fn svg_rect_animated_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -282,6 +318,83 @@ pub(super) fn svg_text_positioning_list_getter<'s>(
rv.set(value);
}
pub(super) fn svg_animated_string_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(name) = callback_data_item(
scope,
&args,
SVG_ANIMATED_ACCESSOR_NAMES,
"SVGAnimatedString attributes",
) else {
rv.set_undefined();
return;
};
if !require_svg_receiver(
scope,
args.this(),
SVG_ANIMATED_STRING_BASE_VAL_SLOT,
"SVGAnimatedString",
&format!("{name} getter"),
) {
return;
}
let slot = match name {
"baseVal" => SVG_ANIMATED_STRING_BASE_VAL_SLOT,
"animVal" => SVG_ANIMATED_STRING_ANIM_VAL_SLOT,
_ => {
rv.set_undefined();
return;
}
};
let animated = args.this();
sync_svg_animated_string_from_owner_attribute(scope, animated);
rv.set(
get_private_value(scope, animated, slot).unwrap_or_else(|| v8::String::empty(scope).into()),
);
}
pub(super) fn svg_animated_string_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
if !require_svg_receiver(
scope,
args.this(),
SVG_ANIMATED_STRING_BASE_VAL_SLOT,
"SVGAnimatedString",
"baseVal setter",
) {
return;
}
let animated = args.this();
let Some(owner) = get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return;
};
let Some(attribute) =
get_private_value(scope, animated, SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT)
.and_then(|value| value.to_string(scope))
.map(|value| value.to_rust_string_lossy(scope))
.filter(|value| !value.is_empty())
else {
return;
};
let Some(value) = crate::native_bridge::element::set_svg_animated_string_base_value(
scope,
owner,
&attribute,
args.get(0),
) else {
return;
};
set_svg_animated_string_values(scope, animated, &value);
}
pub(super) fn svg_animated_length_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -18,6 +18,12 @@ const SVG_GRAPHICS_TRANSFORM_SLOT: &str = "__moliSvgGraphicsTransform";
const SVG_PATTERN_TRANSFORM_SLOT: &str = "__moliSvgPatternTransform";
const SVG_GRADIENT_TRANSFORM_SLOT: &str = "__moliSvgGradientTransform";
const SVG_GEOMETRY_PATH_LENGTH_SLOT: &str = "__moliSvgGeometryPathLength";
const SVG_ELEMENT_CLASS_NAME_SLOT: &str = "__moliSvgElementClassName";
const SVG_URI_HREF_SLOT: &str = "__moliSvgUriHref";
const SVG_ANIMATED_STRING_BASE_VAL_SLOT: &str = "__moliSvgAnimatedStringBaseVal";
const SVG_ANIMATED_STRING_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedStringAnimVal";
const SVG_ANIMATED_STRING_OWNER_ELEMENT_SLOT: &str = "__moliSvgAnimatedStringOwnerElement";
const SVG_ANIMATED_STRING_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgAnimatedStringOwnerAttribute";
const SVG_ANIMATED_LENGTH_BASE_VAL_SLOT: &str = "__moliSvgAnimatedLengthBaseVal";
const SVG_ANIMATED_LENGTH_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedLengthAnimVal";
const SVG_ANIMATED_LENGTH_LIST_BASE_VAL_SLOT: &str = "__moliSvgAnimatedLengthListBaseVal";
@@ -246,6 +252,7 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>(
"SVGLength" => bindings::install_svg_length_bindings(scope, template),
"SVGNumber" => bindings::install_svg_number_bindings(scope, template),
"SVGRect" => rect::install_bindings(scope, template),
"SVGAnimatedString" => bindings::install_svg_animated_string_bindings(scope, template),
"SVGAnimatedLength" => bindings::install_svg_animated_length_bindings(scope, template),
"SVGLengthList" => bindings::install_svg_length_list_bindings(scope, template),
"SVGAnimatedLengthList" => {
@@ -54,6 +54,7 @@ mod trusted_types;
pub(crate) use script_execution::{
inline_script_source_for_execution, prepare_inline_classic_frame_script_job_for_execution,
};
pub(crate) use trusted_types::set_svg_animated_string_base_value;
pub(in crate::native_bridge) use trusted_types::{
TrustedAttributeSetter, trusted_attribute_string_value, trusted_attribute_value_string,
};
@@ -7,6 +7,7 @@ pub(in crate::native_bridge) enum TrustedAttributeSetter {
SetAttributeNs,
SetAttributeNode,
AttrValue,
SvgAnimatedStringBaseVal,
}
impl TrustedAttributeSetter {
@@ -16,6 +17,7 @@ impl TrustedAttributeSetter {
Self::SetAttributeNs => "setAttributeNS",
Self::SetAttributeNode => "setAttributeNode",
Self::AttrValue => "value",
Self::SvgAnimatedStringBaseVal => "baseVal",
}
}
@@ -27,6 +29,9 @@ impl TrustedAttributeSetter {
crate::webidl::Context::argument("Element setAttributeNode", 1)
}
Self::AttrValue => crate::webidl::Context::member("Attr", "value"),
Self::SvgAnimatedStringBaseVal => {
crate::webidl::Context::member("SVGAnimatedString", "baseVal")
}
}
}
}
@@ -276,6 +281,27 @@ pub(in crate::native_bridge) fn trusted_attribute_string_value<'s>(
)
}
pub(crate) fn set_svg_animated_string_base_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
attribute: &str,
value: v8::Local<'s, v8::Value>,
) -> Option<String> {
let (runtime_ptr, handle) =
crate::native_bridge::node_runtime_and_handle_from_object(scope, owner).ok()?;
let value = trusted_attribute_value_string(
scope,
Some((runtime_ptr, handle)),
None,
attribute,
value,
TrustedAttributeSetter::SvgAnimatedStringBaseVal,
)?;
let _ =
unsafe { &mut *runtime_ptr }.set_attribute(scope, runtime_ptr, handle, attribute, &value);
Some(value)
}
pub(crate) fn trusted_script_source_for_execution(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
@@ -1161,6 +1161,117 @@ fn url_attribute_writes_enforce_the_non_iframe_trusted_script_url_sink_table() {
);
}
#[test]
fn svg_script_href_base_val_uses_the_owner_reflected_trusted_script_url_sink() {
let mut vm = new_storage_test_vm("https://svg-script-href-trusted-types.test/");
vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]);
let result = vm
.eval(
r#"
(() => {
const violations = [];
document.addEventListener("securitypolicyviolation", event => {
violations.push({
blockedURI: event.blockedURI,
effectiveDirective: event.effectiveDirective,
sample: event.sample
});
});
globalThis.__svgScriptHrefViolations = violations;
const svgScript = document.createElementNS("http://www.w3.org/2000/svg", "script");
const href = svgScript.href;
let illegalConstructor = false;
try {
new SVGAnimatedString();
} catch (error) {
illegalConstructor = error instanceof TypeError;
}
let plainRejected = false;
try {
href.baseVal = "plain.js";
} catch (error) {
plainRejected = error instanceof TypeError;
}
const rejectedAttribute = svgScript.getAttribute("href");
const explicit = trustedTypes.createPolicy("svg-script-href", {
createScriptURL: value => value
});
href.baseVal = explicit.createScriptURL("trusted.js");
const trustedValues = [
svgScript.getAttribute("href"),
href.baseVal,
href.animVal
];
svgScript.setAttribute("href", explicit.createScriptURL("attribute.js"));
const externallySynced = [href.baseVal, href.animVal];
const className = svgScript.className;
className.baseVal = explicit.createScriptURL("trusted-class");
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
const useHref = use.href;
useHref.baseVal = explicit.createScriptURL("trusted-use");
const defaultCalls = [];
trustedTypes.createPolicy("default", {
createScriptURL: (value, type, sink) => {
defaultCalls.push([value, type, sink]);
return `safe-${value}`;
}
});
href.baseVal = "default-input";
className.baseVal = "plain-class";
useHref.baseVal = "plain-use";
return JSON.stringify({
shape: [
href instanceof SVGAnimatedString,
Object.prototype.toString.call(href),
svgScript.href === href,
illegalConstructor
],
plainRejected,
rejectedAttribute,
trustedValues,
externallySynced,
ordinaryAnimatedStrings: [
className instanceof SVGAnimatedString,
svgScript.className === className,
className.baseVal,
className.animVal,
svgScript.getAttribute("class"),
useHref instanceof SVGAnimatedString,
use.href === useHref,
useHref.baseVal,
useHref.animVal,
use.getAttribute("href")
],
defaulted: [svgScript.getAttribute("href"), href.baseVal, href.animVal],
defaultCalls,
violations
});
})()
"#,
)
.expect("SVGScriptElement href Trusted Types sink probe should evaluate");
assert_eq!(
result,
r#"{"shape":[true,"[object SVGAnimatedString]",true,true],"plainRejected":true,"rejectedAttribute":null,"trustedValues":["trusted.js","trusted.js","trusted.js"],"externallySynced":["attribute.js","attribute.js"],"ordinaryAnimatedStrings":[true,true,"plain-class","plain-class","plain-class",true,true,"plain-use","plain-use","plain-use"],"defaulted":["safe-default-input","safe-default-input","safe-default-input"],"defaultCalls":[["default-input","TrustedScriptURL","SVGScriptElement href"]],"violations":[]}"#
);
assert_eq!(
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm),
1
);
assert_eq!(
vm.eval("JSON.stringify(globalThis.__svgScriptHrefViolations)")
.expect("queued SVGScriptElement href violation should be observable"),
r#"[{"blockedURI":"trusted-types-sink","effectiveDirective":"require-trusted-types-for","sample":"SVGScriptElement href|plain.js"}]"#
);
}
#[test]
fn attached_attribute_mutations_recheck_trusted_types_after_domstring_conversion() {
let mut vm = new_storage_test_vm("https://attached-attribute-trusted-types.test/");