@@ -4,13 +4,17 @@ import { printIonError } from '@utils/logging';
44 * Sanitize an untrusted HTML string.
55 *
66 * Parses the string into a detached DOM, removes blocked tags, strips
7- * attributes outside the `allowedAttributes` list (refer `sanitizeElement`),
8- * and scrubs script-scheme URLs. Returns the sanitized HTML string.
7+ * attributes outside the narrow `domStringAllowedAttributes` list (refer
8+ * `sanitizeElement`), and scrubs script-scheme URLs. Returns the sanitized
9+ * HTML string.
910 *
1011 * Use this when you have an HTML string from an unknown source and need to
1112 * render it via `innerHTML`. Use `sanitizeDOMTree` instead when you already
1213 * have a DOM tree and want to sanitize it in place without a string round
13- * trip; both apply the same attribute policy.
14+ * trip. The two apply the same dangerous-vector scrubbing but different
15+ * attribute allowlists: this path stays narrow so existing consumers
16+ * (toast, loading, alert message, etc.) are unaffected, while
17+ * `sanitizeDOMTree` uses the wider rich-content allowlist.
1418 *
1519 * @param untrustedString - The HTML string to sanitize. Pass an
1620 * `IonicSafeString` to bypass sanitization, or `undefined` to short-circuit.
@@ -70,7 +74,7 @@ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | un
7074
7175 /* eslint-disable-next-line */
7276 for ( let childIndex = 0 ; childIndex < childElements . length ; childIndex ++ ) {
73- sanitizeElement ( childElements [ childIndex ] ) ;
77+ sanitizeElement ( childElements [ childIndex ] , domStringAllowedAttributes ) ;
7478 }
7579 }
7680 } ) ;
@@ -85,7 +89,7 @@ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | un
8589
8690 /* eslint-disable-next-line */
8791 for ( let childIndex = 0 ; childIndex < dfChildren . length ; childIndex ++ ) {
88- sanitizeElement ( dfChildren [ childIndex ] ) ;
92+ sanitizeElement ( dfChildren [ childIndex ] , domStringAllowedAttributes ) ;
8993 }
9094
9195 // Append document fragment to div
@@ -106,8 +110,8 @@ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | un
106110 * Sanitize an entire trusted DOM tree in place.
107111 *
108112 * Removes blocked tags (`script`, `iframe`, etc.) from the subtree and
109- * then sanitizes attributes on every remaining element using the same
110- * allowlist policy as `sanitizeDOMString` (refer `sanitizeElement`).
113+ * then sanitizes attributes on every remaining element using the wider
114+ * `richContentAllowedAttributes` allowlist (refer `sanitizeElement`).
111115 * Component presentational attributes (`size`, `color`, `shape`, inline
112116 * SVG, `aria-*`, `data-*`) are preserved; `style`, event handlers (`on*`),
113117 * form/navigation-hijack attributes, script-scheme URLs, and non-image
@@ -132,7 +136,7 @@ export const sanitizeDOMTree = (root: HTMLElement) => {
132136 }
133137 } ) ;
134138
135- sanitizeElement ( root ) ;
139+ sanitizeElement ( root , richContentAllowedAttributes , richContentAllowedAttributePrefixes ) ;
136140} ;
137141
138142/**
@@ -141,7 +145,7 @@ export const sanitizeDOMTree = (root: HTMLElement) => {
141145 * clean those up as well
142146 */
143147// TODO(FW-2832): type (using Element triggers other type errors as well)
144- const sanitizeElement = ( element : any ) => {
148+ const sanitizeElement = ( element : any , allowedAttributes : string [ ] , allowedAttributePrefixes : string [ ] = [ ] ) => {
145149 // IE uses childNodes, so ignore nodes that are not elements
146150 if ( element . nodeType && element . nodeType !== 1 ) {
147151 return ;
@@ -178,7 +182,7 @@ const sanitizeElement = (element: any) => {
178182 * (`formaction`, `action`, `target`), namespaced attributes like
179183 * `xlink:href`, and anything else not explicitly known to be safe.
180184 */
181- if ( ! isAttributeAllowed ( lowerName ) ) {
185+ if ( ! isAttributeAllowed ( lowerName , allowedAttributes , allowedAttributePrefixes ) ) {
182186 element . removeAttribute ( attributeName ) ;
183187 continue ;
184188 }
@@ -229,7 +233,7 @@ const sanitizeElement = (element: any) => {
229233
230234 /* eslint-disable-next-line */
231235 for ( let i = 0 ; i < childElements . length ; i ++ ) {
232- sanitizeElement ( childElements [ i ] ) ;
236+ sanitizeElement ( childElements [ i ] , allowedAttributes , allowedAttributePrefixes ) ;
233237 }
234238} ;
235239
@@ -292,21 +296,33 @@ export const reflectPropertiesToAttributes = (root: Element): void => {
292296} ;
293297
294298/**
295- * Attribute names that are always safe to keep. Covers global HTML
296- * attributes, the Ionic component presentational props that cloned rich
297- * content (e.g. `ion-select-option` markup) relies on, and the inert SVG
298- * presentation attributes used by inline icons.
299+ * Attribute allowlist for `sanitizeDOMString`. Intentionally narrow: this
300+ * path sanitizes developer HTML strings rendered via `innerHTML` by
301+ * `ion-toast`, `ion-loading`, the `ion-alert` message,
302+ * `ion-refresher-content`, and `ion-infinite-scroll-content`, so the list
303+ * is kept to the minimum those have always needed. Broadening it here would
304+ * change sanitization output for all of those consumers, so the wider
305+ * rich-content allowlist below is deliberately scoped to `sanitizeDOMTree`
306+ * instead.
307+ */
308+ const domStringAllowedAttributes = [ 'class' , 'id' , 'href' , 'src' , 'name' , 'slot' ] ;
309+
310+ /**
311+ * Attribute allowlist for `sanitizeDOMTree` (the select rich-content path).
312+ * Covers global HTML attributes, the Ionic component presentational props
313+ * that cloned rich content (e.g. `ion-select-option` markup) relies on, and
314+ * the inert SVG presentation attributes used by inline icons.
299315 *
300316 * `aria-*` and `data-*` are allowed separately by prefix (refer
301- * `allowedAttributePrefixes `) since they are inert and not worth
317+ * `richContentAllowedAttributePrefixes `) since they are inert and not worth
302318 * enumerating. URL-bearing names (`href`, `src`) are allowed here, but
303319 * their values are still scrubbed for script schemes in `sanitizeElement`.
304320 *
305321 * Notably absent: `style`, event handlers (`on*`), and the
306322 * form/navigation-hijack attributes (`formaction`, `action`, `target`),
307323 * which are therefore stripped.
308324 */
309- const allowedAttributes = [
325+ const richContentAllowedAttributes = [
310326 // Global / structural
311327 'class' ,
312328 'id' ,
@@ -371,17 +387,21 @@ const allowedAttributes = [
371387] ;
372388
373389/**
374- * Attribute-name prefixes that are always safe to keep. `aria-*` and
375- * ` data-*` attributes cannot execute script or load resources, so they are
376- * allowed wholesale rather than enumerated by name.
390+ * Attribute-name prefixes that are always safe to keep in the rich-content
391+ * path. `aria-*` and ` data-*` attributes cannot execute script or load
392+ * resources, so they are allowed wholesale rather than enumerated by name.
377393 */
378- const allowedAttributePrefixes = [ 'aria-' , 'data-' ] ;
394+ const richContentAllowedAttributePrefixes = [ 'aria-' , 'data-' ] ;
379395
380396/**
381397 * Whether an attribute name (already lowercased) is safe to keep, by exact
382398 * match or by an allowed prefix.
383399 */
384- const isAttributeAllowed = ( lowerName : string ) : boolean => {
400+ const isAttributeAllowed = (
401+ lowerName : string ,
402+ allowedAttributes : string [ ] ,
403+ allowedAttributePrefixes : string [ ]
404+ ) : boolean => {
385405 if ( allowedAttributes . includes ( lowerName ) ) {
386406 return true ;
387407 }
0 commit comments