77//! Ported from Angular's `template/pipeline/src/phases/resolve_sanitizers.ts`.
88
99use oxc_str:: Ident ;
10+ use rustc_hash:: FxHashMap ;
1011
1112use crate :: ast:: r3:: SecurityContext ;
12- use crate :: ir:: ops:: { CreateOp , UpdateOp } ;
13+ use crate :: ir:: ops:: { CreateOp , UpdateOp , XrefId } ;
1314use crate :: pipeline:: compilation:: { ComponentCompilationJob , HostBindingCompilationJob } ;
1415use crate :: r3:: Identifiers ;
16+ use crate :: schema:: { is_iframe_security_sensitive_attr, uses_iframe_attr_validation} ;
1517
1618/// Map a security context to its sanitizer function name.
1719fn get_sanitizer_fn ( security_context : SecurityContext ) -> Option < & ' static str > {
@@ -45,12 +47,74 @@ fn get_trusted_value_fn(security_context: SecurityContext) -> Option<&'static st
4547 }
4648}
4749
50+ /// The element-or-container create ops upstream indexes with
51+ /// `createOpXrefMap`, reduced to what the iframe fallback reads: whether the
52+ /// owner op is an `elementStart` whose tag is `iframe`.
53+ ///
54+ /// Upstream's `isIframeElement` checks `op.kind === OpKind.ElementStart`, so
55+ /// self-closing `element()` ops are intentionally not matched.
56+ fn create_op_xref_map < ' a > ( ops : impl Iterator < Item = & ' a CreateOp < ' a > > ) -> FxHashMap < XrefId , bool > {
57+ let mut elements = FxHashMap :: default ( ) ;
58+ for op in ops {
59+ let ( xref, is_iframe) = match op {
60+ CreateOp :: ElementStart ( op) => ( op. xref , op. tag . as_str ( ) . eq_ignore_ascii_case ( "iframe" ) ) ,
61+ CreateOp :: Element ( op) => ( op. xref , false ) ,
62+ CreateOp :: ContainerStart ( op) => ( op. xref , false ) ,
63+ CreateOp :: Container ( op) => ( op. xref , false ) ,
64+ CreateOp :: Template ( op) => ( op. xref , false ) ,
65+ CreateOp :: Conditional ( op) => ( op. xref , false ) ,
66+ CreateOp :: ConditionalBranch ( op) => ( op. xref , false ) ,
67+ CreateOp :: RepeaterCreate ( op) => {
68+ // Upstream indexes the `@empty` view under the same repeater op.
69+ if let Some ( empty_view) = op. empty_view {
70+ elements. insert ( empty_view, false ) ;
71+ }
72+ ( op. xref , false )
73+ }
74+ _ => continue ,
75+ } ;
76+ elements. insert ( xref, is_iframe) ;
77+ }
78+ elements
79+ }
80+
81+ /// Apply upstream's legacy `ɵɵvalidateIframeAttribute` fallback: when a
82+ /// `Property` / `Attribute` / `DomProperty` op got no sanitizer from its
83+ /// security context, a security-sensitive iframe attribute gets the runtime
84+ /// validator. Removed upstream once the schema's `attributeNoBinding` iframe
85+ /// keys covered the same attributes (19.2.17 / 20.3.15 / 21.0.2).
86+ fn resolve_iframe_sanitizer (
87+ assume_iframe : bool ,
88+ elements : & FxHashMap < XrefId , bool > ,
89+ target : XrefId ,
90+ name : & str ,
91+ sanitizer : & mut Option < Ident < ' _ > > ,
92+ ) {
93+ if sanitizer. is_some ( ) {
94+ return ;
95+ }
96+ // For host bindings and `DomProperty` ops the element is not known at
97+ // compile time, so upstream assumes it may be an iframe; the emitted
98+ // validator checks the real tag at runtime.
99+ let is_iframe = if assume_iframe {
100+ true
101+ } else {
102+ * elements
103+ . get ( & target)
104+ . unwrap_or_else ( || panic ! ( "Property should have an element-like owner" ) )
105+ } ;
106+ if is_iframe && is_iframe_security_sensitive_attr ( name) {
107+ * sanitizer = Some ( Ident :: from ( Identifiers :: VALIDATE_IFRAME_ATTRIBUTE ) ) ;
108+ }
109+ }
110+
48111/// Resolves security sanitizers for property bindings.
49112///
50113/// This phase:
51114/// 1. For ExtractedAttribute ops (constant attributes), sets the trusted value function
52115/// 2. For Property, Attribute, and DomProperty ops, sets the sanitizer function
53116pub fn resolve_sanitizers ( job : & mut ComponentCompilationJob < ' _ > ) {
117+ let iframe_validation = uses_iframe_attr_validation ( job. angular_version ) ;
54118 // Collect view xrefs to avoid borrow issues
55119 let view_xrefs: Vec < _ > = job. all_views ( ) . map ( |v| v. xref ) . collect ( ) ;
56120
@@ -65,23 +129,52 @@ pub fn resolve_sanitizers(job: &mut ComponentCompilationJob<'_>) {
65129 }
66130 }
67131
132+ let elements = iframe_validation. then ( || create_op_xref_map ( view. create . iter ( ) ) ) ;
133+
68134 // Process update ops - set sanitizers for property/attribute bindings
69135 for op in view. update . iter_mut ( ) {
70136 match op {
71137 UpdateOp :: Property ( prop) => {
72138 if let Some ( fn_name) = get_sanitizer_fn ( prop. security_context ) {
73139 prop. sanitizer = Some ( Ident :: from ( fn_name) ) ;
74140 }
141+ if let Some ( elements) = & elements {
142+ resolve_iframe_sanitizer (
143+ false ,
144+ elements,
145+ prop. target ,
146+ prop. name . as_str ( ) ,
147+ & mut prop. sanitizer ,
148+ ) ;
149+ }
75150 }
76151 UpdateOp :: Attribute ( attr) => {
77152 if let Some ( fn_name) = get_sanitizer_fn ( attr. security_context ) {
78153 attr. sanitizer = Some ( Ident :: from ( fn_name) ) ;
79154 }
155+ if let Some ( elements) = & elements {
156+ resolve_iframe_sanitizer (
157+ false ,
158+ elements,
159+ attr. target ,
160+ attr. name . as_str ( ) ,
161+ & mut attr. sanitizer ,
162+ ) ;
163+ }
80164 }
81165 UpdateOp :: DomProperty ( dom_prop) => {
82166 if let Some ( fn_name) = get_sanitizer_fn ( dom_prop. security_context ) {
83167 dom_prop. sanitizer = Some ( Ident :: from ( fn_name) ) ;
84168 }
169+ if let Some ( elements) = & elements {
170+ resolve_iframe_sanitizer (
171+ true ,
172+ elements,
173+ dom_prop. target ,
174+ dom_prop. name . as_str ( ) ,
175+ & mut dom_prop. sanitizer ,
176+ ) ;
177+ }
85178 }
86179 _ => { }
87180 }
@@ -94,6 +187,8 @@ pub fn resolve_sanitizers(job: &mut ComponentCompilationJob<'_>) {
94187///
95188/// Host version - only processes the root unit (no embedded views).
96189pub fn resolve_sanitizers_for_host ( job : & mut HostBindingCompilationJob < ' _ > ) {
190+ let iframe_validation = uses_iframe_attr_validation ( job. angular_version ) ;
191+
97192 // Process create ops - set trusted value functions for extracted attributes
98193 for op in job. root . create . iter_mut ( ) {
99194 if let CreateOp :: ExtractedAttribute ( attr) = op {
@@ -105,23 +200,31 @@ pub fn resolve_sanitizers_for_host(job: &mut HostBindingCompilationJob<'_>) {
105200
106201 // Process update ops - set sanitizers for property/attribute bindings
107202 for op in job. root . update . iter_mut ( ) {
108- match op {
203+ let ( name , sanitizer ) = match op {
109204 UpdateOp :: Property ( prop) => {
110205 if let Some ( fn_name) = get_sanitizer_fn ( prop. security_context ) {
111206 prop. sanitizer = Some ( Ident :: from ( fn_name) ) ;
112207 }
208+ ( prop. name . as_str ( ) , & mut prop. sanitizer )
113209 }
114210 UpdateOp :: Attribute ( attr) => {
115211 if let Some ( fn_name) = get_sanitizer_fn ( attr. security_context ) {
116212 attr. sanitizer = Some ( Ident :: from ( fn_name) ) ;
117213 }
214+ ( attr. name . as_str ( ) , & mut attr. sanitizer )
118215 }
119216 UpdateOp :: DomProperty ( dom_prop) => {
120217 if let Some ( fn_name) = get_sanitizer_fn ( dom_prop. security_context ) {
121218 dom_prop. sanitizer = Some ( Ident :: from ( fn_name) ) ;
122219 }
220+ ( dom_prop. name . as_str ( ) , & mut dom_prop. sanitizer )
123221 }
124- _ => { }
222+ _ => continue ,
223+ } ;
224+ // A host job cannot know its host element at compile time, so upstream
225+ // assumes iframe and defers the tag check to the runtime validator.
226+ if iframe_validation && sanitizer. is_none ( ) && is_iframe_security_sensitive_attr ( name) {
227+ * sanitizer = Some ( Ident :: from ( Identifiers :: VALIDATE_IFRAME_ATTRIBUTE ) ) ;
125228 }
126229 }
127230}
0 commit comments