From 4f3b770c4ee9274a0a5299e02ce34d842e678f87 Mon Sep 17 00:00:00 2001 From: Suvam-paul145 Date: Tue, 24 Feb 2026 20:31:21 +0530 Subject: [PATCH 1/3] fix: sanitize dangerouslySetInnerHTML with DOMPurify to prevent XSS --- src/common/Testimonial/TestimonialCard.jsx | 3 ++- src/common/badges-dashboard/BadgeDetails.jsx | 3 ++- src/common/utils/sanitizeHTML.js | 12 ++++++++++++ .../SelectionSortVisualizer.js | 2 ++ src/plays/devblog/Pages/Article.jsx | 3 ++- src/plays/fun-quiz/EndScreen.jsx | 7 ++++--- src/plays/fun-quiz/QuizScreen.jsx | 5 +++-- src/plays/markdown-editor/Output.jsx | 3 ++- src/plays/text-to-speech/TextToSpeech.jsx | 5 +---- src/plays/tube2tunes/Tube2tunes.jsx | 9 +++++++-- 10 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 src/common/utils/sanitizeHTML.js diff --git a/src/common/Testimonial/TestimonialCard.jsx b/src/common/Testimonial/TestimonialCard.jsx index c2e0c29ce0..9bf94226a0 100644 --- a/src/common/Testimonial/TestimonialCard.jsx +++ b/src/common/Testimonial/TestimonialCard.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { format } from 'date-fns'; import * as allLocales from 'date-fns/locale'; import { email2Slug } from 'common/services/string'; +import sanitizeHTML from 'common/utils/sanitizeHTML'; const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, email }) => { const [formattedDate] = useState(() => { @@ -59,7 +60,7 @@ const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, e >

diff --git a/src/common/badges-dashboard/BadgeDetails.jsx b/src/common/badges-dashboard/BadgeDetails.jsx index 893bf60b19..ff78258d9f 100644 --- a/src/common/badges-dashboard/BadgeDetails.jsx +++ b/src/common/badges-dashboard/BadgeDetails.jsx @@ -1,4 +1,5 @@ import Badge from './Badge'; +import sanitizeHTML from 'common/utils/sanitizeHTML'; import './badge.css'; const BadgeDetails = ({ badge, onClose }) => { @@ -9,7 +10,7 @@ const BadgeDetails = ({ badge, onClose }) => { return `${name}`; }); - return ; + return ; }; return ( diff --git a/src/common/utils/sanitizeHTML.js b/src/common/utils/sanitizeHTML.js new file mode 100644 index 0000000000..b07766de01 --- /dev/null +++ b/src/common/utils/sanitizeHTML.js @@ -0,0 +1,12 @@ +import DOMPurify from 'dompurify'; + +/** + * Sanitizes an HTML string using DOMPurify to prevent XSS attacks. + * Use this utility whenever you need to render dynamic HTML via dangerouslySetInnerHTML. + * + * @param {string} html - The raw HTML string to sanitize. + * @returns {string} - A sanitized HTML string safe to use with dangerouslySetInnerHTML. + */ +const sanitizeHTML = (html) => DOMPurify.sanitize(html ?? ''); + +export default sanitizeHTML; diff --git a/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js b/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js index 951f4380cb..1eb8241875 100644 --- a/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js +++ b/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js @@ -23,6 +23,8 @@ function SelectionSortVisualizer() { const handleSort = async () => { const arrCopy = [...arr]; const outputElements = document.getElementById('output-visualizer'); + // Safe: clears the container to empty string (no user data injected). + // All subsequent DOM mutations use createElement/createTextNode (XSS-safe). outputElements.innerHTML = ''; for (let i = 0; i < arrCopy.length - 1; i++) { diff --git a/src/plays/devblog/Pages/Article.jsx b/src/plays/devblog/Pages/Article.jsx index 8f7c70e7da..ef2ea2d2b0 100644 --- a/src/plays/devblog/Pages/Article.jsx +++ b/src/plays/devblog/Pages/Article.jsx @@ -1,6 +1,7 @@ import axios from 'axios'; import { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import sanitizeHTML from 'common/utils/sanitizeHTML'; import Loading from '../components/Loading'; const Article = () => { @@ -50,7 +51,7 @@ const Article = () => {

) : ( diff --git a/src/plays/fun-quiz/EndScreen.jsx b/src/plays/fun-quiz/EndScreen.jsx index a8f337760f..5398009a8b 100644 --- a/src/plays/fun-quiz/EndScreen.jsx +++ b/src/plays/fun-quiz/EndScreen.jsx @@ -1,5 +1,6 @@ // vendors import { Fragment, useState } from 'react'; +import sanitizeHTML from 'common/utils/sanitizeHTML'; // css import './FrontScreen.scss'; @@ -16,17 +17,17 @@ const EndScreen = ({ quizSummary, redirectHome }) => {
Question: {currentQuestion?.qNo}
  • Ans: ${currentQuestion?.correct_answer}
    ` + __html: sanitizeHTML(`
    Ans: ${currentQuestion?.correct_answer}
    `) }} /> Your Answer: ${currentQuestion?.your_answer}` + __html: sanitizeHTML(`Your Answer: ${currentQuestion?.your_answer}`) }} /> diff --git a/src/plays/fun-quiz/QuizScreen.jsx b/src/plays/fun-quiz/QuizScreen.jsx index cc150234f8..571946985a 100644 --- a/src/plays/fun-quiz/QuizScreen.jsx +++ b/src/plays/fun-quiz/QuizScreen.jsx @@ -1,4 +1,5 @@ import { useEffect, useState, useCallback, useRef } from 'react'; +import sanitizeHTML from 'common/utils/sanitizeHTML'; import './QuizScreen.scss'; @@ -149,7 +150,7 @@ function QuizScreen({ category, getQuizSummary }) {
    {timer}
    Question: {questionNumber + 1}
    -

    +

    {currentQuestion?.options?.map((option, index) => { @@ -157,7 +158,7 @@ function QuizScreen({ category, getQuizSummary }) {
    diff --git a/src/plays/markdown-editor/Output.jsx b/src/plays/markdown-editor/Output.jsx index 9228271430..8f7ca65314 100644 --- a/src/plays/markdown-editor/Output.jsx +++ b/src/plays/markdown-editor/Output.jsx @@ -1,10 +1,11 @@ import React from 'react'; +import sanitizeHTML from 'common/utils/sanitizeHTML'; const Output = ({ md, text, mdPreviewBox }) => { return (
    ); diff --git a/src/plays/text-to-speech/TextToSpeech.jsx b/src/plays/text-to-speech/TextToSpeech.jsx index 3286a6d0f9..e400bc4fde 100644 --- a/src/plays/text-to-speech/TextToSpeech.jsx +++ b/src/plays/text-to-speech/TextToSpeech.jsx @@ -158,10 +158,7 @@ function TextToSpeech(props) {
    {convertedText ? ( <> -

    +

    {convertedText}