@@ -3,43 +3,25 @@ import DOMPurify from 'dompurify';
33// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
44export function getSecureDOMPurifyConfig ( ) {
55 return {
6- // Block dangerous elements that can cause XSS and CSS injection
7- FORBID_TAGS : [
8- 'svg' , 'defs' , 'use' , 'g' , 'symbol' , 'marker' , 'pattern' , 'mask' , 'clipPath' ,
9- 'linearGradient' , 'radialGradient' , 'stop' , 'animate' , 'animateTransform' ,
10- 'animateMotion' , 'set' , 'switch' , 'foreignObject' , 'script' , 'style' , 'link' ,
11- 'meta' , 'iframe' , 'object' , 'embed' , 'applet' , 'form' , 'input' , 'textarea' ,
12- 'select' , 'option' , 'button' , 'label' , 'fieldset' , 'legend' , 'frameset' ,
13- 'frame' , 'noframes' , 'base' , 'basefont' , 'isindex' , 'dir' , 'menu' , 'menuitem'
14- ] ,
15- // Block dangerous attributes that can cause XSS and CSS injection
16- FORBID_ATTR : [
17- 'xlink:href' , 'href' , 'onload' , 'onerror' , 'onclick' , 'onmouseover' ,
18- 'onfocus' , 'onblur' , 'onchange' , 'onsubmit' , 'onreset' , 'onselect' ,
19- 'onunload' , 'onresize' , 'onscroll' , 'onkeydown' , 'onkeyup' , 'onkeypress' ,
20- 'onmousedown' , 'onmouseup' , 'onmouseover' , 'onmouseout' , 'onmousemove' ,
21- 'ondblclick' , 'oncontextmenu' , 'onwheel' , 'ontouchstart' , 'ontouchend' ,
22- 'ontouchmove' , 'ontouchcancel' , 'onabort' , 'oncanplay' , 'oncanplaythrough' ,
23- 'ondurationchange' , 'onemptied' , 'onended' , 'onerror' , 'onloadeddata' ,
24- 'onloadedmetadata' , 'onloadstart' , 'onpause' , 'onplay' , 'onplaying' ,
25- 'onprogress' , 'onratechange' , 'onseeked' , 'onseeking' , 'onstalled' ,
26- 'onsuspend' , 'ontimeupdate' , 'onvolumechange' , 'onwaiting' , 'onbeforeunload' ,
27- 'onhashchange' , 'onpagehide' , 'onpageshow' , 'onpopstate' , 'onstorage' ,
28- 'onunload' , 'style' , 'class' , 'id' , 'data-*' , 'aria-*'
29- ] ,
30- // Allow only safe image formats and protocols
6+ // Allow common markdown elements including anchor tags
7+ ALLOWED_TAGS : [ 'a' , 'p' , 'br' , 'strong' , 'em' , 'u' , 'h1' , 'h2' , 'h3' , 'h4' , 'h5' , 'h6' , 'ul' , 'ol' , 'li' , 'blockquote' , 'pre' , 'code' , 'img' , 'table' , 'thead' , 'tbody' , 'tr' , 'th' , 'td' , 'hr' , 'div' , 'span' ] ,
8+ // Allow safe attributes including href for anchor tags
9+ ALLOWED_ATTR : [ 'href' , 'title' , 'alt' , 'src' , 'width' , 'height' , 'target' , 'rel' ] ,
10+ // Allow safe protocols for links
3111 ALLOWED_URI_REGEXP : / ^ (?: (?: (?: f | h t ) t p s ? | m a i l t o | t e l | c a l l t o | c i d | x m p p ) : | [ ^ a - z ] | [ a - z + . \- ] + (?: [ ^ a - z + . \- : ] | $ ) ) / i,
32- // Remove dangerous protocols
12+ // Allow unknown protocols but be cautious
3313 ALLOW_UNKNOWN_PROTOCOLS : false ,
34- // Sanitize URLs to prevent malicious content loading
14+ // Sanitize DOM for security
3515 SANITIZE_DOM : true ,
36- // Remove dangerous elements completely
37- KEEP_CONTENT : false ,
38- // Additional security measures
39- ADD_ATTR : [ ] ,
16+ // Keep content but sanitize it
17+ KEEP_CONTENT : true ,
18+ // Block dangerous elements that can cause XSS
19+ FORBID_TAGS : [ 'script' , 'style' , 'iframe' , 'object' , 'embed' , 'applet' , 'svg' , 'defs' , 'use' , 'g' , 'symbol' , 'marker' , 'pattern' , 'mask' , 'clipPath' , 'linearGradient' , 'radialGradient' , 'stop' , 'animate' , 'animateTransform' , 'animateMotion' , 'set' , 'switch' , 'foreignObject' , 'link' , 'meta' , 'form' , 'input' , 'textarea' , 'select' , 'option' , 'button' , 'label' , 'fieldset' , 'legend' , 'frameset' , 'frame' , 'noframes' , 'base' , 'basefont' , 'isindex' , 'dir' , 'menu' , 'menuitem' ] ,
20+ // Block dangerous attributes but allow safe href
21+ FORBID_ATTR : [ 'xlink:href' , 'onload' , 'onerror' , 'onclick' , 'onmouseover' , 'onfocus' , 'onblur' , 'onchange' , 'onsubmit' , 'onreset' , 'onselect' , 'onunload' , 'onresize' , 'onscroll' , 'onkeydown' , 'onkeyup' , 'onkeypress' , 'onmousedown' , 'onmouseup' , 'onmouseover' , 'onmouseout' , 'onmousemove' , 'ondblclick' , 'oncontextmenu' , 'onwheel' , 'ontouchstart' , 'ontouchend' , 'ontouchmove' , 'ontouchcancel' , 'onabort' , 'oncanplay' , 'oncanplaythrough' , 'ondurationchange' , 'onemptied' , 'onended' , 'onerror' , 'onloadeddata' , 'onloadedmetadata' , 'onloadstart' , 'onpause' , 'onplay' , 'onplaying' , 'onprogress' , 'onratechange' , 'onseeked' , 'onseeking' , 'onstalled' , 'onsuspend' , 'ontimeupdate' , 'onvolumechange' , 'onwaiting' , 'onbeforeunload' , 'onhashchange' , 'onpagehide' , 'onpageshow' , 'onpopstate' , 'onstorage' , 'onunload' , 'style' , 'class' , 'id' , 'data-*' , 'aria-*' ] ,
4022 // Block data URIs that could contain malicious content
4123 ALLOW_DATA_ATTR : false ,
42- // Custom hook to further sanitize content
24+ // Custom hooks for additional security
4325 HOOKS : {
4426 uponSanitizeElement : function ( node , data ) {
4527 // Block any remaining dangerous elements
@@ -51,14 +33,37 @@ export function getSecureDOMPurifyConfig() {
5133 return false ;
5234 }
5335
54- // Block img tags with SVG data URIs
36+ // Block img tags with SVG data URIs that could contain malicious JavaScript
5537 if ( node . tagName && node . tagName . toLowerCase ( ) === 'img' ) {
5638 const src = node . getAttribute ( 'src' ) ;
57- if ( src && ( src . startsWith ( 'data:image/svg' ) || src . endsWith ( '.svg' ) ) ) {
58- if ( process . env . DEBUG === 'true' ) {
59- console . warn ( 'Blocked potentially malicious SVG image:' , src ) ;
39+ if ( src ) {
40+ // Block all SVG data URIs to prevent XSS via embedded JavaScript
41+ if ( src . startsWith ( 'data:image/svg' ) || src . endsWith ( '.svg' ) ) {
42+ if ( process . env . DEBUG === 'true' ) {
43+ console . warn ( 'Blocked potentially malicious SVG image:' , src ) ;
44+ }
45+ return false ;
46+ }
47+
48+ // Additional check for base64 encoded SVG with script tags
49+ if ( src . startsWith ( 'data:image/svg+xml;base64,' ) ) {
50+ try {
51+ const base64Content = src . split ( ',' ) [ 1 ] ;
52+ const decodedContent = atob ( base64Content ) ;
53+ if ( decodedContent . includes ( '<script' ) || decodedContent . includes ( 'javascript:' ) ) {
54+ if ( process . env . DEBUG === 'true' ) {
55+ console . warn ( 'Blocked SVG with embedded JavaScript:' , src . substring ( 0 , 100 ) + '...' ) ;
56+ }
57+ return false ;
58+ }
59+ } catch ( e ) {
60+ // If decoding fails, block it as a safety measure
61+ if ( process . env . DEBUG === 'true' ) {
62+ console . warn ( 'Blocked malformed SVG data URI:' , src ) ;
63+ }
64+ return false ;
65+ }
6066 }
61- return false ;
6267 }
6368 }
6469
@@ -100,6 +105,19 @@ export function getSecureDOMPurifyConfig() {
100105 return false ;
101106 }
102107
108+ // Allow href attribute for anchor tags only
109+ if ( data . attrName === 'href' ) {
110+ // Only allow href on anchor tags
111+ if ( node . tagName && node . tagName . toLowerCase ( ) === 'a' ) {
112+ return true ;
113+ } else {
114+ if ( process . env . DEBUG === 'true' ) {
115+ console . warn ( 'Blocked href attribute on non-anchor element:' , node . tagName ) ;
116+ }
117+ return false ;
118+ }
119+ }
120+
103121 return true ;
104122 }
105123 }
0 commit comments