Skip to content

Commit 3a4b2a6

Browse files
committed
fix(security): keep bare selectorless hosts on pre-v22 schemas
calcPossibleSecurityContexts only promotes an unknown bare selector to :svg:/:math: form on the namespaced schema (20.3.22+, 21.2.14+, v22). Promoting unconditionally made <MyComp:animate [attr.to]> miss the bare animate|to key on 21.2.7-21.2.13, dropping ɵɵvalidateAttribute. Gate the promotion on the schema kind; host tags pass through verbatim on earlier versions, matching upstream.
1 parent 0324bb1 commit 3a4b2a6

3 files changed

Lines changed: 59 additions & 9 deletions

File tree

crates/oxc_angular_compiler/src/schema/dom_security_schema.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ pub fn strips_namespaced_svg_style(version: Option<crate::AngularVersion>) -> bo
128128
security_profile(version).strip_svg_style
129129
}
130130

131+
/// Whether this Angular version's schema keys keep `:svg:` / `:math:` prefixes.
132+
/// `calcPossibleSecurityContexts` only promotes bare selector elements to
133+
/// their `:svg:` / `:math:` forms on the namespaced schema.
134+
pub fn uses_namespaced_schema(version: Option<crate::AngularVersion>) -> bool {
135+
security_profile(version).namespaced
136+
}
137+
131138
/// Whether i18n must reject `iframe` `src` as a Trusted Types sink.
132139
pub fn rejects_iframe_src_i18n(version: Option<crate::AngularVersion>) -> bool {
133140
security_profile(version).iframe_src_i18n

crates/oxc_angular_compiler/src/schema/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ pub use dom_security_schema::{
1010
calc_security_context_for_unknown_element, get_security_context, get_security_context_for,
1111
host_binding_security_context, host_binding_security_context_for, is_known_element,
1212
rejects_iframe_src_i18n, strips_namespaced_svg_script, strips_namespaced_svg_style,
13+
uses_namespaced_schema,
1314
};
1415
pub use trusted_types_sinks::{is_trusted_types_sink, is_trusted_types_sink_at};

crates/oxc_angular_compiler/src/transform/html_to_r3.rs

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use crate::parser::expression::{BindingParser, find_comment_start};
3232
use crate::parser::html::{decode_entities_in_string, get_html_tag_definition, split_ns_name};
3333
use crate::schema::{
3434
get_security_context_for, is_known_element, is_trusted_types_sink_at,
35-
strips_namespaced_svg_script, strips_namespaced_svg_style,
35+
strips_namespaced_svg_script, strips_namespaced_svg_style, uses_namespaced_schema,
3636
};
3737
use crate::transform::control_flow::{parse_conditional_params, parse_defer_triggers};
3838
use crate::util::ParseError;
@@ -341,7 +341,7 @@ impl<'a> HtmlToR3Transform<'a> {
341341
Self::resolve_element_name(raw_name, parent_prefix)
342342
};
343343
let security_name = if element.is_component {
344-
Self::component_security_name(host_tag.as_deref())
344+
Self::component_security_name(host_tag.as_deref(), self.angular_version)
345345
} else {
346346
Self::security_lookup_name(&resolved_name)
347347
};
@@ -728,7 +728,8 @@ impl<'a> HtmlToR3Transform<'a> {
728728

729729
// `tagName === null` is not a Trusted Types sink. `full_name` is the class.
730730
let i18n_element_name = host_tag.as_deref();
731-
let security_name = Self::component_security_name(host_tag.as_deref());
731+
let security_name =
732+
Self::component_security_name(host_tag.as_deref(), self.angular_version);
732733
let (attributes, inputs, outputs, references, _variables, template_attr) =
733734
self.parse_attributes(&component.attrs, &security_name, i18n_element_name, false);
734735

@@ -916,15 +917,19 @@ impl<'a> HtmlToR3Transform<'a> {
916917
/// Security-schema name for a selectorless component, matching
917918
/// `calcPossibleSecurityContexts(component.tagName, ...)`.
918919
///
919-
/// A bare host tag that is not an HTML element is rewritten to its known
920-
/// `:svg:`/`:math:` form (`animate` → `:svg:animate`). `tagName === null`
921-
/// resolves over every known element upstream; the empty name reproduces
922-
/// that through the `*|attr` fallback (`src` → `NONE`, `innerHTML` →
923-
/// `HTML`).
924-
fn component_security_name(host_tag: Option<&str>) -> String {
920+
/// On the namespaced schema a bare host tag that is not an HTML element is
921+
/// rewritten to its known `:svg:`/`:math:` form (`animate` →
922+
/// `:svg:animate`); pre-v22 versions look up the bare tag (`animate|to`).
923+
/// `tagName === null` resolves over every known element upstream; the empty
924+
/// name reproduces that through the `*|attr` fallback (`src` → `NONE`,
925+
/// `innerHTML` → `HTML`).
926+
fn component_security_name(host_tag: Option<&str>, version: Option<AngularVersion>) -> String {
925927
let Some(tag) = host_tag else {
926928
return String::new();
927929
};
930+
if !uses_namespaced_schema(version) {
931+
return tag.to_string();
932+
}
928933
let lower = tag.to_ascii_lowercase();
929934
let (ns, local) = split_ns_name(&lower);
930935
if ns.is_none() && !is_known_element(local) {
@@ -5373,4 +5378,41 @@ mod security_tests {
53735378
"{contexts:?}"
53745379
);
53755380
}
5381+
5382+
#[test]
5383+
fn selectorless_bare_host_stays_bare_before_the_namespaced_schema() {
5384+
// Pre-v22 `calcPossibleSecurityContexts` looks the bare tag up:
5385+
// `animate|to` is `AttributeNoBinding` on 21.2.7 while `:svg:animate|to`
5386+
// is not a key.
5387+
let (_, contexts, _) = compile_selectorless_at(
5388+
r#"<MyComp:animate [attr.to]="value"></MyComp:animate>"#,
5389+
Some(AngularVersion::new(21, 2, 7)),
5390+
);
5391+
assert!(
5392+
contexts
5393+
.iter()
5394+
.any(|(name, ctx)| name == "to" && *ctx == SecurityContext::AttributeNoBinding),
5395+
"{contexts:?}"
5396+
);
5397+
5398+
// Same for bare MathML hosts: `mi|href` is a bare URL key pre-v22.
5399+
let (_, contexts, _) = compile_selectorless_at(
5400+
r#"<MyComp:mi [attr.href]="value"></MyComp:mi>"#,
5401+
Some(AngularVersion::new(21, 2, 7)),
5402+
);
5403+
assert!(
5404+
contexts.iter().any(|(name, ctx)| name == "href" && *ctx == SecurityContext::Url),
5405+
"{contexts:?}"
5406+
);
5407+
5408+
// v22 promotes the bare host to `:svg:animate` like upstream.
5409+
let (_, contexts, _) =
5410+
compile_selectorless(r#"<MyComp:animate [attr.to]="value"></MyComp:animate>"#);
5411+
assert!(
5412+
contexts
5413+
.iter()
5414+
.any(|(name, ctx)| name == "to" && *ctx == SecurityContext::AttributeNoBinding),
5415+
"{contexts:?}"
5416+
);
5417+
}
53765418
}

0 commit comments

Comments
 (0)