Skip to content

Commit d6be1be

Browse files
Test (#1690)
* fix: sanitize dangerouslySetInnerHTML with DOMPurify to prevent XSS * feat: introduce several new plays, common UI components, and a sanitizeHTML utility. --------- Co-authored-by: Priyankar Pal <88102392+priyankarpal@users.noreply.github.com>
1 parent 63a5a90 commit d6be1be

11 files changed

Lines changed: 38 additions & 16 deletions

File tree

.husky/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/usr/bin/env sh
22
. "$(dirname -- "$0")/_/husky.sh"
33

4-
yarn pre-commit
4+
npx lint-staged

src/common/Testimonial/TestimonialCard.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
22
import { format } from 'date-fns';
33
import * as allLocales from 'date-fns/locale';
44
import { email2Slug } from 'common/services/string';
5+
import sanitizeHTML from 'common/utils/sanitizeHTML';
56

67
const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, email }) => {
78
const [formattedDate] = useState(() => {
@@ -59,7 +60,7 @@ const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, e
5960
>
6061
<p
6162
className="leading-relaxed text-gray-700"
62-
dangerouslySetInnerHTML={{ __html: replaceWithBr() }}
63+
dangerouslySetInnerHTML={{ __html: sanitizeHTML(replaceWithBr()) }}
6364
/>
6465
</blockquote>
6566
</div>

src/common/badges-dashboard/BadgeDetails.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Badge from './Badge';
2+
import sanitizeHTML from 'common/utils/sanitizeHTML';
23
import './badge.css';
34

45
const BadgeDetails = ({ badge, onClose }) => {
@@ -9,7 +10,7 @@ const BadgeDetails = ({ badge, onClose }) => {
910
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:underline">${name}</a>`;
1011
});
1112

12-
return <span dangerouslySetInnerHTML={{ __html: descriptionWithLinks }} />;
13+
return <span dangerouslySetInnerHTML={{ __html: sanitizeHTML(descriptionWithLinks) }} />;
1314
};
1415

1516
return (

src/common/utils/sanitizeHTML.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import DOMPurify from 'dompurify';
2+
3+
/**
4+
* Sanitizes an HTML string using DOMPurify to prevent XSS attacks.
5+
* Use this utility whenever you need to render dynamic HTML via dangerouslySetInnerHTML.
6+
*
7+
* @param {string} html - The raw HTML string to sanitize.
8+
* @returns {string} - A sanitized HTML string safe to use with dangerouslySetInnerHTML.
9+
*/
10+
const sanitizeHTML = (html) => DOMPurify.sanitize(html ?? '');
11+
12+
export default sanitizeHTML;

src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ function SelectionSortVisualizer() {
2323
const handleSort = async () => {
2424
const arrCopy = [...arr];
2525
const outputElements = document.getElementById('output-visualizer');
26+
// Safe: clears the container to empty string (no user data injected).
27+
// All subsequent DOM mutations use createElement/createTextNode (XSS-safe).
2628
outputElements.innerHTML = '';
2729

2830
for (let i = 0; i < arrCopy.length - 1; i++) {

src/plays/devblog/Pages/Article.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from 'axios';
22
import { useState, useEffect } from 'react';
33
import { useParams } from 'react-router-dom';
4+
import sanitizeHTML from 'common/utils/sanitizeHTML';
45
import Loading from '../components/Loading';
56

67
const Article = () => {
@@ -50,7 +51,7 @@ const Article = () => {
5051

5152
<div
5253
className="mt-10 devBlog-article"
53-
dangerouslySetInnerHTML={{ __html: article.body_html }}
54+
dangerouslySetInnerHTML={{ __html: sanitizeHTML(article.body_html) }}
5455
/>
5556
</div>
5657
) : (

src/plays/fun-quiz/EndScreen.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// vendors
22
import { Fragment, useState } from 'react';
3+
import sanitizeHTML from 'common/utils/sanitizeHTML';
34

45
// css
56
import './FrontScreen.scss';
@@ -16,17 +17,17 @@ const EndScreen = ({ quizSummary, redirectHome }) => {
1617
<div className="question-number">Question: {currentQuestion?.qNo}</div>
1718
<li
1819
dangerouslySetInnerHTML={{
19-
__html: `${currentQuestion?.question}`
20+
__html: sanitizeHTML(`${currentQuestion?.question}`)
2021
}}
2122
/>
2223
<span
2324
dangerouslySetInnerHTML={{
24-
__html: `<br/><b>Ans</b>: ${currentQuestion?.correct_answer}<br/>`
25+
__html: sanitizeHTML(`<br/><b>Ans</b>: ${currentQuestion?.correct_answer}<br/>`)
2526
}}
2627
/>
2728
<span
2829
dangerouslySetInnerHTML={{
29-
__html: `<b>Your Answer</b>: ${currentQuestion?.your_answer}`
30+
__html: sanitizeHTML(`<b>Your Answer</b>: ${currentQuestion?.your_answer}`)
3031
}}
3132
/>
3233
</div>

src/plays/fun-quiz/QuizScreen.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useState, useCallback, useRef } from 'react';
2+
import sanitizeHTML from 'common/utils/sanitizeHTML';
23

34
import './QuizScreen.scss';
45

@@ -149,15 +150,15 @@ function QuizScreen({ category, getQuizSummary }) {
149150
<div className={`timer ${timer <= 5 && 'caution'}`}>{timer}</div>
150151
<div className="question-info">Question: {questionNumber + 1}</div>
151152
<div className="question">
152-
<h1 dangerouslySetInnerHTML={{ __html: currentQuestion?.question }} />
153+
<h1 dangerouslySetInnerHTML={{ __html: sanitizeHTML(currentQuestion?.question) }} />
153154
</div>
154155
<div className="options">
155156
{currentQuestion?.options?.map((option, index) => {
156157
return (
157158
<div className="single-opt" key={index}>
158159
<div
159160
className={itemClassDisplayController(option)}
160-
dangerouslySetInnerHTML={{ __html: option }}
161+
dangerouslySetInnerHTML={{ __html: sanitizeHTML(option) }}
161162
onClick={handleAnswerClick(option)}
162163
/>
163164
</div>

src/plays/markdown-editor/Output.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
2+
import sanitizeHTML from 'common/utils/sanitizeHTML';
23

34
const Output = ({ md, text, mdPreviewBox }) => {
45
return (
56
<div
67
className="md-editor output-div"
7-
dangerouslySetInnerHTML={{ __html: md.render(text) }}
8+
dangerouslySetInnerHTML={{ __html: sanitizeHTML(md.render(text)) }}
89
id={mdPreviewBox}
910
/>
1011
);

src/plays/text-to-speech/TextToSpeech.jsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,7 @@ function TextToSpeech(props) {
158158
<div className="tts-output-box">
159159
{convertedText ? (
160160
<>
161-
<p
162-
className="tts-output-text"
163-
dangerouslySetInnerHTML={{ __html: convertedText }}
164-
/>
161+
<p className="tts-output-text">{convertedText}</p>
165162

166163
<button className="tts-speaker-btn" onClick={handleSpeak}>
167164
{isSpeaking ? <FaStop size={28} /> : <FaVolumeUp size={28} />}

0 commit comments

Comments
 (0)