11import React , { useState , useEffect } from 'react' ;
2+ import DOMPurify from 'dompurify' ;
23import PropTypes from 'prop-types' ;
34import { useDispatch , useSelector , connect } from 'react-redux' ;
45import { Button , Modal , ModalBody , ModalFooter , ModalHeader , Col , Row } from 'reactstrap' ;
@@ -22,9 +23,29 @@ const RoleInfoModal = ({ info, auth, roleName}) => {
2223 const [ infoContentModal , setInfoContentModal ] = useState ( '' ) ;
2324 const dispatch = useDispatch ( ) ;
2425
26+ // XSS Protection: Sanitize HTML content
27+ const sanitizeHTML = ( htmlContent ) => {
28+ if ( ! htmlContent || typeof htmlContent !== 'string' ) return '' ;
29+
30+ return DOMPurify . sanitize ( htmlContent , {
31+ ALLOWED_TAGS : [ 'p' , 'br' , 'strong' , 'em' , 'u' , 'ul' , 'ol' , 'li' , 'h1' , 'h2' , 'h3' , 'h4' , 'h5' , 'h6' , 'a' , 'span' , 'div' ] ,
32+ ALLOWED_ATTR : [ 'href' , 'class' ] ,
33+ ALLOW_DATA_ATTR : false ,
34+ ALLOWED_URI_REGEXP : / ^ h t t p s ? : \/ \/ / , // Only allow http/https URLs
35+ } ) ;
36+ } ;
37+
38+ // XSS Protection: Sanitize text content
39+ const sanitizeText = ( textContent ) => {
40+ if ( ! textContent || typeof textContent !== 'string' ) return '' ;
41+ return DOMPurify . sanitize ( textContent , { ALLOWED_TAGS : [ ] , ALLOWED_ATTR : [ ] } ) ;
42+ } ;
43+
2544 useEffect ( ( ) => {
26- setInfoContentModal ( infoContent ) ;
27- } , [ infoContent ] ) ;
45+ // Sanitize content when setting initial state
46+ setInfoContentModal ( sanitizeHTML ( infoContent ) ) ;
47+ // eslint-disable-next-line react-hooks/exhaustive-deps
48+ } , [ infoContent ] ) ; // sanitizeHTML is stable, no need to include in deps
2849
2950 const handleSaveSuccess = async ( ) => {
3051 toast . success ( '✔ The info was saved successfully!' , {
@@ -53,7 +74,9 @@ const RoleInfoModal = ({ info, auth, roleName}) => {
5374 } ;
5475
5576 const handleInputChange = ( content ) => {
56- setInfoContentModal ( content ) ;
77+ // Sanitize content from rich text editor
78+ const sanitizedContent = sanitizeHTML ( content ) ;
79+ setInfoContentModal ( sanitizedContent ) ;
5780 } ;
5881
5982 const handleSave = async ( e ) => {
@@ -63,14 +86,16 @@ const RoleInfoModal = ({ info, auth, roleName}) => {
6386 e . preventDefault ( ) ;
6487 }
6588
66- const updateInfo = { infoContent : infoContentModal } ;
89+ // Sanitize content before saving
90+ const sanitizedContent = sanitizeHTML ( infoContentModal ) ;
91+ const updateInfo = { infoContent : sanitizedContent } ;
6792 let saveResult ;
6893
6994 // If info doesn't exist in database, create new record
7095 if ( ! info || ! info . _id ) {
7196 const newInfo = {
72- infoName : roleName || 'UnknownRoleInfo' ,
73- infoContent : infoContentModal ,
97+ infoName : sanitizeText ( roleName ) || 'UnknownRoleInfo' ,
98+ infoContent : sanitizedContent ,
7499 visibility : '0'
75100 } ;
76101 saveResult = await dispatch ( addInfoCollection ( newInfo ) ) ;
@@ -79,7 +104,7 @@ const RoleInfoModal = ({ info, auth, roleName}) => {
79104 saveResult = await dispatch ( updateInfoCollection ( info . _id , updateInfo ) ) ;
80105 }
81106
82- setInfoContentModal ( infoContentModal ) ;
107+ setInfoContentModal ( sanitizedContent ) ;
83108
84109 if ( saveResult === 200 || saveResult === 201 ) {
85110 await handleSaveSuccess ( ) ;
@@ -110,7 +135,7 @@ const RoleInfoModal = ({ info, auth, roleName}) => {
110135 < div
111136 className = { `${ styles [ 'role-info-content' ] } ${ darkMode ? styles [ 'dark-mode' ] : '' } ` }
112137 style = { { paddingLeft : '20px' } }
113- dangerouslySetInnerHTML = { { __html : infoContentModal } }
138+ dangerouslySetInnerHTML = { { __html : sanitizeHTML ( infoContentModal ) } }
114139 onClick = { ( ) => setIsEditing ( true ) }
115140 /> }
116141 </ ModalBody >
0 commit comments