@@ -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
6369fn 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+
143178fn 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}
0 commit comments