Skip to content

Commit 82c7bea

Browse files
committed
fix(security): route Angular 19.2.x through the backported schemas
The 19.2 release line received the same security backports as 20.3/21.x, but every 19.x target fell through to the legacy schema: - 19.2.17 hardened (MathML hrefs, a|xlink:href, attributeNoBinding) while keeping the legacy URL keys - 19.2.18-19.2.22 added script|href; iframe|src joined the Trusted Types sinks at 19.2.20 - 19.2.23+ has the namespaced schema including :svg:a|href, and 19.2.23 is the only 19.x tag where the preparser strips :svg:style
1 parent 3a4b2a6 commit 82c7bea

2 files changed

Lines changed: 136 additions & 13 deletions

File tree

crates/oxc_angular_compiler/src/schema/dom_security_schema.rs

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,24 @@ enum SchemaKind {
2525
/// 21.0.0 / 21.0.1 and everything below 20.3.15. Old URL set with
2626
/// `*|ping`, `*|cite`, `applet|code`, `media|src`, etc.
2727
Legacy,
28-
/// 20.3.15, 21.0.2–21.0.5. Adds MathML hrefs, `attributeName` no-binding,
29-
/// and iframe sandbox keys on top of the old URL set.
28+
/// 19.2.17, 20.3.15, 21.0.2–21.0.5. Adds MathML hrefs, `attributeName`
29+
/// no-binding, and iframe sandbox keys on top of the old URL set.
3030
V20_3_15,
3131
/// 21.0.6 only. The hardening without the legacy URL keys, and before
3232
/// `script|href` landed in 21.0.7.
3333
V21_0_6,
3434
/// 20.3.16–20.3.21 and 21.0.7 through 21.2.6. Adds `script|href`; the
3535
/// `ping`/`cite`/`applet`/`media` keys are gone from 21.0.6 on.
3636
V21_1,
37-
/// Same as `V21_1` but still carrying the legacy URL keys: only
38-
/// 20.3.16–20.3.21 (the key removal was never backported to 20.3).
37+
/// Same as `V21_1` but still carrying the legacy URL keys: 19.2.18–19.2.22
38+
/// and 20.3.16–20.3.21 (the key removal was never backported to either
39+
/// maintained line).
3940
V20_3_16,
4041
/// 21.2.7 through 21.2.13. Animation `to` / `from` / `values` are bare keys.
4142
V21_2_7,
4243
/// 21.2.14 only. Namespaced keys but no `:svg:a|href` yet.
4344
V21_2_14,
44-
/// 20.3.22+, 21.2.15+, 22+. Namespaced keys with `:svg:a|href`.
45+
/// 19.2.23+, 20.3.22+, 21.2.15+, 22+. Namespaced keys with `:svg:a|href`.
4546
/// `script|src` and `script|href` are gone.
4647
V22,
4748
}
@@ -52,12 +53,17 @@ struct SecurityProfile {
5253
namespaced: bool,
5354
/// The preparser strips `:svg:script` as well as `script`.
5455
strip_svg_script: bool,
55-
/// The preparser also strips `:svg:style`. Only 20.3.22 and 21.2.14 did
56-
/// this; it was reverted everywhere else.
56+
/// The preparser also strips `:svg:style`. Only 19.2.23, 20.3.22 and
57+
/// 21.2.14 did this; it was reverted everywhere else.
5758
strip_svg_style: bool,
58-
/// `iframe|src` joined Trusted Types sinks (20.3.18–21, 21.2.4+; never on
59-
/// the 21.0.x / 21.1.x lines).
59+
/// `iframe|src` joined Trusted Types sinks (19.2.20+, 20.3.18–21, 21.2.4+;
60+
/// never on the 21.0.x / 21.1.x lines).
6061
iframe_src_i18n: bool,
62+
/// `resolve_sanitizers` falls back to `ɵɵvalidateIframeAttribute` for
63+
/// security-sensitive iframe attributes with no other sanitizer. Upstream
64+
/// removed this when the iframe `attributeNoBinding` keys landed
65+
/// (19.2.17 / 20.3.15 / 21.0.2), so it only exists on the legacy schema.
66+
iframe_attr_validation: bool,
6167
}
6268

6369
fn security_profile(version: Option<crate::AngularVersion>) -> SecurityProfile {
@@ -69,6 +75,14 @@ fn security_profile(version: Option<crate::AngularVersion>) -> SecurityProfile {
6975
}
7076

7177
let (kind, iframe_src_i18n) = match version.major {
78+
19 if version.minor >= 2 => match version.patch {
79+
0..=16 => (SchemaKind::Legacy, false),
80+
17 => (SchemaKind::V20_3_15, false),
81+
18..=19 => (SchemaKind::V20_3_16, false),
82+
20..=22 => (SchemaKind::V20_3_16, true),
83+
// 19.2.23+ has the namespaced schema with `:svg:a|href`.
84+
_ => (SchemaKind::V22, true),
85+
},
7286
20 if version.minor >= 3 => match version.patch {
7387
0..=14 => (SchemaKind::Legacy, false),
7488
15 => (SchemaKind::V20_3_15, false),
@@ -94,15 +108,17 @@ fn security_profile(version: Option<crate::AngularVersion>) -> SecurityProfile {
94108
};
95109

96110
let namespaced = matches!(kind, SchemaKind::V21_2_14 | SchemaKind::V22);
97-
// `:svg:style` stripping existed only in 20.3.22 and 21.2.14.
111+
// `:svg:style` stripping existed only in 19.2.23, 20.3.22 and 21.2.14.
98112
let strip_svg_style = matches!(kind, SchemaKind::V21_2_14)
99-
|| (version.major == 20 && version.minor == 3 && version.patch == 22);
113+
|| (version.major == 20 && version.minor == 3 && version.patch == 22)
114+
|| (version.major == 19 && version.minor == 2 && version.patch == 23);
100115
SecurityProfile {
101116
kind,
102117
namespaced,
103118
strip_svg_script: namespaced,
104119
strip_svg_style,
105120
iframe_src_i18n,
121+
iframe_attr_validation: matches!(kind, SchemaKind::Legacy),
106122
}
107123
}
108124

@@ -113,6 +129,7 @@ fn v22_profile() -> SecurityProfile {
113129
strip_svg_script: true,
114130
strip_svg_style: false,
115131
iframe_src_i18n: true,
132+
iframe_attr_validation: false,
116133
}
117134
}
118135

@@ -140,6 +157,24 @@ pub fn rejects_iframe_src_i18n(version: Option<crate::AngularVersion>) -> bool {
140157
security_profile(version).iframe_src_i18n
141158
}
142159

160+
/// Whether `resolve_sanitizers` applies the `ɵɵvalidateIframeAttribute`
161+
/// fallback. Upstream kept it on versions without the iframe
162+
/// `attributeNoBinding` schema keys (everything before 19.2.17 / 20.3.15 /
163+
/// 21.0.2) and dropped it once those keys covered the same attributes.
164+
pub fn uses_iframe_attr_validation(version: Option<crate::AngularVersion>) -> bool {
165+
security_profile(version).iframe_attr_validation
166+
}
167+
168+
/// Whether `attr_name` is a security-sensitive `<iframe>` attribute
169+
/// (`IFRAME_SECURITY_SENSITIVE_ATTRS`). The comparison is case-insensitive
170+
/// because `setAttribute` lowercases names.
171+
pub fn is_iframe_security_sensitive_attr(attr_name: &str) -> bool {
172+
matches!(
173+
attr_name.to_ascii_lowercase().as_str(),
174+
"sandbox" | "allow" | "allowfullscreen" | "referrerpolicy" | "csp" | "fetchpriority"
175+
)
176+
}
177+
143178
fn build_v22_schema(with_svg_a: bool) -> FxHashMap<String, SecurityContext> {
144179
let mut schema = FxHashMap::default();
145180

@@ -1331,4 +1366,91 @@ mod tests {
13311366
assert!(!strips_namespaced_svg_style(Some(crate::AngularVersion::new(20, 3, 23))));
13321367
assert!(!strips_namespaced_svg_style(None));
13331368
}
1369+
1370+
#[test]
1371+
fn v19_2_follows_the_same_backported_cutovers() {
1372+
// The 19.2 line received the same security backports as 20.3/21.x.
1373+
let v19_2_16 = Some(crate::AngularVersion::new(19, 2, 16));
1374+
assert_eq!(
1375+
get_security_context_for("media", "src", v19_2_16),
1376+
SecurityContext::ResourceUrl
1377+
);
1378+
assert_eq!(get_security_context_for("a", "xlink:href", v19_2_16), SecurityContext::None);
1379+
assert_eq!(get_security_context_for("script", "href", v19_2_16), SecurityContext::None);
1380+
1381+
// 19.2.17 hardened (MathML hrefs, attrNoBinding) but kept legacy keys.
1382+
let v19_2_17 = Some(crate::AngularVersion::new(19, 2, 17));
1383+
assert_eq!(get_security_context_for("a", "xlink:href", v19_2_17), SecurityContext::Url);
1384+
assert_eq!(get_security_context_for("mi", "href", v19_2_17), SecurityContext::Url);
1385+
assert_eq!(
1386+
get_security_context_for("iframe", "sandbox", v19_2_17),
1387+
SecurityContext::AttributeNoBinding
1388+
);
1389+
assert_eq!(
1390+
get_security_context_for("media", "src", v19_2_17),
1391+
SecurityContext::ResourceUrl
1392+
);
1393+
assert_eq!(get_security_context_for("script", "href", v19_2_17), SecurityContext::None);
1394+
assert!(!rejects_iframe_src_i18n(v19_2_17));
1395+
1396+
// 19.2.18+ added `script|href`; `iframe|src` joined the Trusted Types
1397+
// sinks at 19.2.20.
1398+
let v19_2_19 = Some(crate::AngularVersion::new(19, 2, 19));
1399+
assert_eq!(
1400+
get_security_context_for("script", "href", v19_2_19),
1401+
SecurityContext::ResourceUrl
1402+
);
1403+
assert!(!rejects_iframe_src_i18n(v19_2_19));
1404+
assert!(rejects_iframe_src_i18n(Some(crate::AngularVersion::new(19, 2, 20))));
1405+
1406+
// 19.2.23+ has the namespaced schema with `:svg:a|href`; `:svg:style`
1407+
// was stripped in exactly 19.2.23.
1408+
let v19_2_23 = Some(crate::AngularVersion::new(19, 2, 23));
1409+
assert!(uses_namespaced_schema(v19_2_23));
1410+
assert_eq!(get_security_context_for(":svg:a", "href", v19_2_23), SecurityContext::Url);
1411+
assert_eq!(get_security_context_for("script", "src", v19_2_23), SecurityContext::None);
1412+
assert!(strips_namespaced_svg_script(v19_2_23));
1413+
assert!(strips_namespaced_svg_style(v19_2_23));
1414+
let v19_2_24 = Some(crate::AngularVersion::new(19, 2, 24));
1415+
assert!(uses_namespaced_schema(v19_2_24));
1416+
assert!(!strips_namespaced_svg_style(v19_2_24));
1417+
1418+
// 19.0 / 19.1 are the legacy schema.
1419+
for version in [crate::AngularVersion::new(19, 0, 0), crate::AngularVersion::new(19, 1, 4)]
1420+
{
1421+
let version = Some(version);
1422+
assert!(!uses_namespaced_schema(version));
1423+
assert_eq!(
1424+
get_security_context_for("media", "src", version),
1425+
SecurityContext::ResourceUrl
1426+
);
1427+
}
1428+
}
1429+
1430+
#[test]
1431+
fn iframe_attr_validation_only_on_the_legacy_schema() {
1432+
assert!(uses_iframe_attr_validation(Some(crate::AngularVersion::new(19, 0, 0))));
1433+
assert!(uses_iframe_attr_validation(Some(crate::AngularVersion::new(19, 2, 16))));
1434+
assert!(!uses_iframe_attr_validation(Some(crate::AngularVersion::new(19, 2, 17))));
1435+
assert!(uses_iframe_attr_validation(Some(crate::AngularVersion::new(20, 3, 14))));
1436+
assert!(!uses_iframe_attr_validation(Some(crate::AngularVersion::new(20, 3, 15))));
1437+
assert!(uses_iframe_attr_validation(Some(crate::AngularVersion::new(21, 0, 1))));
1438+
assert!(!uses_iframe_attr_validation(Some(crate::AngularVersion::new(21, 0, 2))));
1439+
assert!(!uses_iframe_attr_validation(None));
1440+
assert!(!uses_iframe_attr_validation(Some(crate::AngularVersion::new(22, 0, 0))));
1441+
}
1442+
1443+
#[test]
1444+
fn iframe_security_sensitive_attrs_match_case_insensitively() {
1445+
for attr in
1446+
["sandbox", "allow", "allowfullscreen", "referrerpolicy", "csp", "fetchpriority"]
1447+
{
1448+
assert!(is_iframe_security_sensitive_attr(attr));
1449+
}
1450+
// `setAttribute` lowercases the name, so upstream compares lowercase.
1451+
assert!(is_iframe_security_sensitive_attr("SandBox"));
1452+
assert!(is_iframe_security_sensitive_attr("allowFullScreen"));
1453+
assert!(!is_iframe_security_sensitive_attr("src"));
1454+
assert!(!is_iframe_security_sensitive_attr("srcdoc"));
1455+
}
13341456
}

crates/oxc_angular_compiler/src/schema/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ mod trusted_types_sinks;
88

99
pub use dom_security_schema::{
1010
calc_security_context_for_unknown_element, get_security_context, get_security_context_for,
11-
host_binding_security_context, host_binding_security_context_for, is_known_element,
12-
rejects_iframe_src_i18n, strips_namespaced_svg_script, strips_namespaced_svg_style,
11+
host_binding_security_context, host_binding_security_context_for,
12+
is_iframe_security_sensitive_attr, is_known_element, rejects_iframe_src_i18n,
13+
strips_namespaced_svg_script, strips_namespaced_svg_style, uses_iframe_attr_validation,
1314
uses_namespaced_schema,
1415
};
1516
pub use trusted_types_sinks::{is_trusted_types_sink, is_trusted_types_sink_at};

0 commit comments

Comments
 (0)