Skip to content

Commit 6d5cf70

Browse files
Merge pull request #4371 from OneCommunityGlobal/DurgaVenkataPraveen-ImplementingXSSProtection
Durga venkata praveen implementing xss protection
2 parents 8c0fbd3 + d18a1cf commit 6d5cf70

4 files changed

Lines changed: 207 additions & 58 deletions

File tree

src/components/TeamMemberTasks/ReviewButton.jsx

Lines changed: 115 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from 'reactstrap';
1515
import { useDispatch, useSelector } from 'react-redux';
1616
import { toast } from 'react-toastify';
17+
import dompurify from 'dompurify';
1718
import styles from './style.module.css';
1819
import style from './reviewButton.module.css';
1920
import { 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,})(?:https?:\/\/)?[\w.-]+\.[a-zA-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 = /^https?:\/\//;
169+
const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-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' }}>

src/components/Timer/TimerPopout.jsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,43 @@ import { createRoot } from 'react-dom/client';
44
import { useRef, useEffect } from 'react';
55
import cs from 'classnames';
66
import { Provider } from 'react-redux';
7+
import DOMPurify from 'dompurify';
78
import styles from './Timer.module.css';
89
import './Countdown.module.css';
910
import { store } from '../../store';
1011

1112
function TimerPopout({ authUser, darkMode, TimerComponent }) {
1213
const popupRef = useRef(null);
14+
const rootRef = useRef(null);
15+
16+
// Sanitize CSS content to prevent CSS injection attacks
17+
const sanitizeCSS = cssText => {
18+
if (!cssText || typeof cssText !== 'string') return '';
19+
20+
// Remove potentially dangerous CSS properties and values
21+
const dangerousPatterns = [
22+
/javascript:/gi,
23+
/data:/gi,
24+
/vbscript:/gi,
25+
/expression\s*\(/gi,
26+
/@import/gi,
27+
/behavior\s*:/gi,
28+
/-moz-binding/gi,
29+
];
30+
31+
let sanitized = cssText;
32+
dangerousPatterns.forEach(pattern => {
33+
sanitized = sanitized.replace(pattern, '');
34+
});
35+
36+
return DOMPurify.sanitize(sanitized, { ALLOWED_TAGS: [] });
37+
};
1338

1439
useEffect(() => {
1540
return () => {
41+
if (rootRef.current) {
42+
rootRef.current.unmount();
43+
}
1644
if (popupRef.current && !popupRef.current.closed) {
1745
popupRef.current.close();
1846
}
@@ -102,7 +130,7 @@ function TimerPopout({ authUser, darkMode, TimerComponent }) {
102130
Array.from(window.document.styleSheets).forEach(styleSheet => {
103131
try {
104132
const cssRules = Array.from(styleSheet.cssRules)
105-
.map(rule => rule.cssText)
133+
.map(rule => sanitizeCSS(rule.cssText))
106134
.join('');
107135
const style = popup.document.createElement('style');
108136
style.textContent = cssRules;

0 commit comments

Comments
 (0)