@@ -14,6 +14,7 @@ import {
1414} from 'reactstrap' ;
1515import { useDispatch , useSelector } from 'react-redux' ;
1616import { toast } from 'react-toastify' ;
17+ import dompurify from 'dompurify' ;
1718import styles from './style.module.css' ;
1819import style from './reviewButton.module.css' ;
1920import { boxStyle , boxStyleDark } from '~/styles' ;
@@ -49,6 +50,33 @@ function ReviewButton({ user, task, updateTask }) {
4950 errorMessage : '' ,
5051 } ) ;
5152
53+ // XSS Protection sanitizer
54+ const sanitizer = dompurify . sanitize ;
55+
56+ // Utility function to sanitize URLs
57+ const sanitizeUrl = url => {
58+ if ( ! url ) return '' ;
59+ return sanitizer ( url . trim ( ) , { ALLOWED_TAGS : [ ] , ALLOWED_ATTR : [ ] } ) ;
60+ } ;
61+
62+ // Utility function to sanitize text content
63+ const sanitizeText = text => {
64+ if ( ! text ) return '' ;
65+ return sanitizer ( text , { ALLOWED_TAGS : [ ] , ALLOWED_ATTR : [ ] } ) ;
66+ } ;
67+
68+ // Safe link handler to prevent XSS in href attributes
69+ const handleSafeLink = url => {
70+ // Sanitize the URL and validate it's safe to use as href
71+ const sanitizedUrl = sanitizeUrl ( url ) ;
72+ const validationResult = validateAllowedDomainTypes ( sanitizedUrl ) ;
73+
74+ if ( validationResult . isValid && validURL ( sanitizedUrl ) ) {
75+ return sanitizedUrl ;
76+ }
77+ return '#' ; // Fallback to safe href
78+ } ;
79+
5280 const toggleModal = ( ) => {
5381 setModal ( ! modal ) ;
5482 if ( ! modal ) {
@@ -78,7 +106,8 @@ function ReviewButton({ user, task, updateTask }) {
78106 if ( ! editLinkState . isOpen ) {
79107 // When opening the modal, find the link associated with this user
80108 const userLink = task . relatedWorkLinks ?. [ task . relatedWorkLinks . length - 1 ] || '' ;
81- setEditLinkState ( prev => ( { ...prev , link : userLink , error : null } ) ) ;
109+ const sanitizedUserLink = sanitizeUrl ( userLink ) ;
110+ setEditLinkState ( prev => ( { ...prev , link : sanitizedUserLink , error : null } ) ) ;
82111 }
83112 } ;
84113
@@ -129,17 +158,48 @@ function ReviewButton({ user, task, updateTask }) {
129158
130159 const validURL = url => {
131160 try {
132- if ( url === '' ) return false ;
133-
134- const pattern = / ^ (? = .{ 20 , } ) (?: h t t p s ? : \/ \/ ) ? [ \w . - ] + \. [ a - z A - Z ] { 2 , } (?: \/ \S * ) ? $ / ;
135- return pattern . test ( url ) ;
161+ if ( ! url || url . trim ( ) === '' ) return false ;
162+
163+ // Check minimum length requirement
164+ if ( url . length < 20 ) return false ;
165+
166+ // Secure URL validation pattern that prevents catastrophic backtracking
167+ // Split validation into parts to avoid nested quantifiers
168+ const protocolPattern = / ^ h t t p s ? : \/ \/ / ;
169+ const domainPattern = / ^ [ a - z A - Z 0 - 9 ] ( [ a - z A - Z 0 - 9 - ] * [ a - z A - Z 0 - 9 ] ) ? ( \. [ a - z A - Z 0 - 9 ] ( [ a - z A - Z 0 - 9 - ] * [ a - z A - Z 0 - 9 ] ) ? ) * \. [ a - z A - Z ] { 2 , } $ / ;
170+ const pathPattern = / ^ [ \/ \w \- . _ ~ : ? # [ \] @ ! $ & ' ( ) * + , ; = % ] * $ / ;
171+
172+ // If URL doesn't start with http/https, add https:// for validation
173+ const urlToTest = url . startsWith ( 'http' ) ? url : `https://${ url } ` ;
174+
175+ // Test protocol
176+ if ( ! protocolPattern . test ( urlToTest ) ) return false ;
177+
178+ // Extract domain and path parts
179+ const urlWithoutProtocol = urlToTest . replace ( protocolPattern , '' ) ;
180+ const slashIndex = urlWithoutProtocol . indexOf ( '/' ) ;
181+ const domain =
182+ slashIndex === - 1 ? urlWithoutProtocol : urlWithoutProtocol . substring ( 0 , slashIndex ) ;
183+ const path = slashIndex === - 1 ? '' : urlWithoutProtocol . substring ( slashIndex ) ;
184+
185+ // Validate domain and path separately
186+ if ( ! domainPattern . test ( domain ) ) return false ;
187+ if ( path && ! pathPattern . test ( path ) ) return false ;
188+
189+ // Additional validation using URL constructor
190+ try {
191+ new URL ( urlToTest ) ;
192+ return true ;
193+ } catch ( e ) {
194+ return false ;
195+ }
136196 } catch ( err ) {
137197 return false ;
138198 }
139199 } ;
140200
141201 const handleLink = e => {
142- const url = e . target . value . trim ( ) ;
202+ const url = sanitizeUrl ( e . target . value ) ;
143203 setLink ( url ) ;
144204 if ( ! url ) {
145205 setEditLinkState ( prev => ( { ...prev , error : 'A valid URL is required for review' } ) ) ;
@@ -236,8 +296,12 @@ function ReviewButton({ user, task, updateTask }) {
236296 }
237297
238298 if ( newStatus === 'Submitted' && link ) {
239- if ( validURL ( link ) ) {
240- updatedTask = { ...updatedTask , relatedWorkLinks : [ ...taskRelatedWorkLinks , link ] } ;
299+ const sanitizedLink = sanitizeUrl ( link ) ;
300+ if ( validURL ( sanitizedLink ) ) {
301+ updatedTask = {
302+ ...updatedTask ,
303+ relatedWorkLinks : [ ...taskRelatedWorkLinks , sanitizedLink ] ,
304+ } ;
241305 setLink ( '' ) ;
242306 } else {
243307 setIsSubmitting ( false ) ;
@@ -252,15 +316,17 @@ function ReviewButton({ user, task, updateTask }) {
252316 const submitReviewRequest = event => {
253317 event . preventDefault ( ) ;
254318
255- if ( ! validURL ( link ) ) {
319+ const sanitizedLink = sanitizeUrl ( link ) ;
320+ if ( ! validURL ( sanitizedLink ) ) {
256321 setEditLinkState ( prev => ( {
257322 ...prev ,
258- error : 'Please enter a valid URL of at least 20 characters' ,
323+ error :
324+ 'Please enter a valid URL (must start with http:// or https:// and be at least 20 characters)' ,
259325 } ) ) ;
260326 return ;
261327 }
262328
263- const validationResult = validateAllowedDomainTypes ( link ) ;
329+ const validationResult = validateAllowedDomainTypes ( sanitizedLink ) ;
264330 if ( ! validationResult . isValid ) {
265331 toggleInvalidDomainModal ( validationResult . errorType ) ;
266332 return ;
@@ -271,10 +337,10 @@ function ReviewButton({ user, task, updateTask }) {
271337
272338 const sendReviewReq = ( ) => {
273339 const data = { } ;
274- data . myUserId = myUserId ;
275- data . name = user . name ;
276- data . taskName = task . taskName ;
277- httpService . post ( `${ ApiEndpoint } /tasks/reviewreq/${ myUserId } ` , data ) ;
340+ data . myUserId = sanitizeText ( myUserId ) ;
341+ data . name = sanitizeText ( user . name ) ;
342+ data . taskName = sanitizeText ( task . taskName ) ;
343+ httpService . post ( `${ ApiEndpoint } /tasks/reviewreq/${ sanitizeText ( myUserId ) } ` , data ) ;
278344 } ;
279345
280346 const handleFinalSubmit = ( ) => {
@@ -286,23 +352,26 @@ function ReviewButton({ user, task, updateTask }) {
286352
287353 const sendEditLinkNotification = ( ) => {
288354 const data = { } ;
289- data . myUserId = myUserId ;
290- data . name = user . name ;
291- data . taskName = task . taskName ;
355+ data . myUserId = sanitizeText ( myUserId ) ;
356+ data . name = sanitizeText ( user . name ) ;
357+ data . taskName = sanitizeText ( task . taskName ) ;
292358 data . isLinkUpdate = true ;
293- httpService . post ( `${ ApiEndpoint } /tasks/reviewreq/${ myUserId } ` , data ) ;
359+ httpService . post ( `${ ApiEndpoint } /tasks/reviewreq/${ sanitizeText ( myUserId ) } ` , data ) ;
294360 } ;
295361
296362 const handleEditLink = ( ) => {
297- if ( ! validURL ( editLinkState . link ) ) {
363+ const sanitizedLink = sanitizeUrl ( editLinkState . link ) ;
364+
365+ if ( ! validURL ( sanitizedLink ) ) {
298366 setEditLinkState ( prev => ( {
299367 ...prev ,
300- error : 'Please enter a valid URL of at least 20 characters' ,
368+ error :
369+ 'Please enter a valid URL (must start with http:// or https:// and be at least 20 characters)' ,
301370 } ) ) ;
302371 return ;
303372 }
304373
305- const validationResult = validateAllowedDomainTypes ( editLinkState . link ) ;
374+ const validationResult = validateAllowedDomainTypes ( sanitizedLink ) ;
306375 if ( ! validationResult . isValid ) {
307376 toggleInvalidDomainModal ( validationResult . errorType ) ;
308377 return ;
@@ -316,10 +385,10 @@ function ReviewButton({ user, task, updateTask }) {
316385
317386 // If there are related work links, replace the last one (assuming it's the one for this user)
318387 if ( Array . isArray ( updatedTask . relatedWorkLinks ) && updatedTask . relatedWorkLinks . length > 0 ) {
319- updatedTask . relatedWorkLinks [ updatedTask . relatedWorkLinks . length - 1 ] = editLinkState . link ;
388+ updatedTask . relatedWorkLinks [ updatedTask . relatedWorkLinks . length - 1 ] = sanitizedLink ;
320389 } else {
321390 // If no related work links exist yet, add this one
322- updatedTask . relatedWorkLinks = [ editLinkState . link ] ;
391+ updatedTask . relatedWorkLinks = [ sanitizedLink ] ;
323392 }
324393
325394 // Call the update function from props
@@ -372,10 +441,11 @@ function ReviewButton({ user, task, updateTask }) {
372441 } ;
373442
374443 const handleEditLinkChange = e => {
375- // Safely extract the value first
376- const newValue = e && e . target && e . target . value !== undefined ? e . target . value : '' ;
377- // Then use the extracted value in the state update
378- setEditLinkState ( prev => ( { ...prev , link : newValue } ) ) ;
444+ // Safely extract and sanitize the value first
445+ const rawValue = e && e . target && e . target . value !== undefined ? e . target . value : '' ;
446+ const sanitizedValue = sanitizeUrl ( rawValue ) ;
447+ // Then use the sanitized value in the state update
448+ setEditLinkState ( prev => ( { ...prev , link : sanitizedValue } ) ) ;
379449 } ;
380450
381451 const buttonFormat = ( ) => {
@@ -427,8 +497,8 @@ function ReviewButton({ user, task, updateTask }) {
427497 // eslint-disable-next-line no-shadow
428498 task . relatedWorkLinks . map ( link => (
429499 < DropdownItem
430- key = { link }
431- href = { link }
500+ key = { sanitizeText ( link ) }
501+ href = { handleSafeLink ( link ) }
432502 target = "_blank"
433503 className = { `${ darkMode ? 'text-light' : '' } ${ style [ 'dark-mode-btn' ] } ` }
434504 >
@@ -467,8 +537,8 @@ function ReviewButton({ user, task, updateTask }) {
467537 { task . relatedWorkLinks &&
468538 task . relatedWorkLinks . map ( dropLink => (
469539 < DropdownItem
470- key = { dropLink }
471- href = { dropLink }
540+ key = { sanitizeText ( dropLink ) }
541+ href = { handleSafeLink ( dropLink ) }
472542 target = "_blank"
473543 className = { `${ darkMode ? 'text-light' : '' } ${ style [ 'dark-mode-btn' ] } ` }
474544 >
@@ -477,7 +547,7 @@ function ReviewButton({ user, task, updateTask }) {
477547 ) ) }
478548 < DropdownItem
479549 onClick = { toggleEditLinkModal }
480- className = { darkMode ? 'text-light dark-mode-btn' : '' }
550+ className = { ` ${ darkMode ? 'text-light' : '' } ${ style [ ' dark-mode-btn'] } ` }
481551 >
482552 < FontAwesomeIcon icon = { faPencilAlt } /> Edit Link
483553 </ DropdownItem >
@@ -498,7 +568,7 @@ function ReviewButton({ user, task, updateTask }) {
498568 setSelectedAction ( 'More Work Needed' ) ;
499569 toggleVerify ( ) ;
500570 } }
501- className = { darkMode ? 'text-light dark-mode-btn' : '' }
571+ className = { ` ${ darkMode ? 'text-light' : '' } ${ style [ ' dark-mode-btn'] } ` }
502572 >
503573 More work needed, reset this button
504574 </ DropdownItem >
@@ -564,9 +634,7 @@ function ReviewButton({ user, task, updateTask }) {
564634 < ModalBody className = { darkMode ? 'bg-yinmn-blue' : '' } >
565635 You are about to submit the following link for review:
566636 < div className = "mt-2" style = { { wordWrap : 'break-word' , wordBreak : 'break-all' } } >
567- < a href = { link } target = "_blank" rel = "noopener noreferrer" >
568- { link }
569- </ a >
637+ < span > { sanitizeText ( link ) } </ span >
570638 </ div >
571639 Please confirm if this is the correct link.
572640 </ ModalBody >
@@ -597,21 +665,24 @@ function ReviewButton({ user, task, updateTask }) {
597665 < ModalBody className = { darkMode ? 'bg-yinmn-blue' : '' } >
598666 Please add link to related work:
599667 < Input type = "text" required value = { link } onChange = { handleLink } />
600- { editLinkState . error && < div className = "text-danger" > { editLinkState . error } </ div > }
668+ { editLinkState . error && (
669+ < div className = "text-danger" > { sanitizeText ( editLinkState . error ) } </ div >
670+ ) }
601671 </ ModalBody >
602672 < ModalFooter className = { darkMode ? 'bg-yinmn-blue' : '' } >
603673 < Button
604674 onClick = { e => {
605675 e . preventDefault ( ) ;
606- if ( ! link || ! validURL ( link ) ) {
676+ const sanitizedLink = sanitizeUrl ( link ) ;
677+ if ( ! sanitizedLink || ! validURL ( sanitizedLink ) ) {
607678 setEditLinkState ( prev => ( {
608679 ...prev ,
609680 error : "Please enter a valid URL starting with 'https://'." ,
610681 } ) ) ;
611682 return ;
612683 }
613684
614- const validationResult = validateAllowedDomainTypes ( link ) ;
685+ const validationResult = validateAllowedDomainTypes ( sanitizedLink ) ;
615686 if ( ! validationResult . isValid ) {
616687 toggleInvalidDomainModal ( validationResult . errorType ) ;
617688 return ;
@@ -647,7 +718,9 @@ function ReviewButton({ user, task, updateTask }) {
647718 < ModalBody className = { darkMode ? 'bg-yinmn-blue' : '' } >
648719 < p > Update the link to your submitted work:</ p >
649720 < Input type = "text" required value = { editLinkState . link } onChange = { handleEditLinkChange } />
650- { editLinkState . error && < div className = "text-danger" > { editLinkState . error } </ div > }
721+ { editLinkState . error && (
722+ < div className = "text-danger" > { sanitizeText ( editLinkState . error ) } </ div >
723+ ) }
651724 </ ModalBody >
652725 < ModalFooter className = { darkMode ? 'bg-yinmn-blue' : '' } >
653726 < Button
@@ -688,7 +761,7 @@ function ReviewButton({ user, task, updateTask }) {
688761 ⚠️
689762 </ span >
690763 </ div >
691- < p > { invalidDomainModal . errorMessage } </ p >
764+ < p > { sanitizeText ( invalidDomainModal . errorMessage ) } </ p >
692765 < div className = "mt-3" >
693766 < strong > Acceptable link types:</ strong >
694767 < ul className = "mt-2" style = { { paddingLeft : '25px' } } >
0 commit comments