Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
yarn pre-commit
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is redundant and potentially confusing. The pre-commit script in package.json already runs lint-staged (line 100: "pre-commit": "lint-staged"). Changing this hook to run 'yarn pre-commit' just adds an extra indirection that eventually runs the same lint-staged command. The original 'npx lint-staged' is more direct and doesn't require yarn to be installed. Unless there's a specific reason for this change (such as additional pre-commit logic being added to the package.json script), consider reverting to 'npx lint-staged' for clarity and simplicity.

Suggested change
yarn pre-commit
npx lint-staged

Copilot uses AI. Check for mistakes.
3 changes: 1 addition & 2 deletions src/common/Testimonial/TestimonialCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ 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(() => {
Expand Down Expand Up @@ -60,7 +59,7 @@ const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, e
>
<p
className="leading-relaxed text-gray-700"
dangerouslySetInnerHTML={{ __html: sanitizeHTML(replaceWithBr()) }}
dangerouslySetInnerHTML={{ __html: replaceWithBr() }}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal of sanitization introduces an XSS vulnerability. The replaceWithBr() function converts newlines to br tags in the quote string, which comes from user testimonials. If a user's quote contains malicious HTML or JavaScript, it will be rendered without sanitization using dangerouslySetInnerHTML, potentially executing in other users' browsers. Consider restoring the sanitization or using safer text rendering methods.

Copilot uses AI. Check for mistakes.
/>
</blockquote>
</div>
Expand Down
3 changes: 1 addition & 2 deletions src/common/badges-dashboard/BadgeDetails.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Badge from './Badge';
import sanitizeHTML from 'common/utils/sanitizeHTML';
import './badge.css';

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

return <span dangerouslySetInnerHTML={{ __html: sanitizeHTML(descriptionWithLinks) }} />;
return <span dangerouslySetInnerHTML={{ __html: descriptionWithLinks }} />;
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal of sanitization introduces an XSS vulnerability. The descriptionWithLinks is constructed by replacing patterns in badge.description with HTML anchor tags. While the URL pattern matching provides some validation, this does not prevent all XSS vectors. If badge.description contains malicious content that matches the pattern, or if the name part contains HTML entities or JavaScript, it could execute when rendered. Consider restoring the sanitization or using safer methods like creating DOM elements programmatically instead of HTML strings.

Copilot uses AI. Check for mistakes.
};

return (
Expand Down
120 changes: 120 additions & 0 deletions src/common/playlists/PlayErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react';
import { ReactComponent as ImageOops } from 'images/img-oops.svg';

class PlayErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, isChunkError: false };
}

static getDerivedStateFromError(error) {
// Detect chunk load failures (network errors loading lazy chunks)
const isChunkError =
error?.name === 'ChunkLoadError' ||
/loading chunk/i.test(error?.message) ||
/failed to fetch dynamically imported module/i.test(error?.message);

return { hasError: true, error, isChunkError };
}

componentDidCatch(error, errorInfo) {
console.error(`Error loading play "${this.props.playName}":`, error, errorInfo);
}

handleRetry = () => {
this.setState({ hasError: false, error: null, isChunkError: false });
};

handleGoBack = () => {
window.location.href = '/plays';
};

render() {
if (this.state.hasError) {
return (
<div className="play-error-boundary" style={styles.container}>
<ImageOops style={styles.image} />
<h2 style={styles.title}>
{this.state.isChunkError ? 'Failed to load this play' : 'Something went wrong'}
</h2>
<p style={styles.message}>
{this.state.isChunkError
? 'There was a network error loading this play. Please check your connection and try again.'
: `An error occurred while rendering "${this.props.playName || 'this play'}".`}
</p>
<div style={styles.actions}>
{this.state.isChunkError && (
<button style={styles.retryButton} onClick={this.handleRetry}>
Retry
</button>
)}
<button style={styles.backButton} onClick={this.handleGoBack}>
Back to Plays
</button>
</div>
</div>
);
}

return this.props.children;
}
}

const styles = {
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '3rem 1.5rem',
textAlign: 'center',
minHeight: '50vh'
},
image: {
width: '200px',
height: 'auto',
marginBottom: '1.5rem',
opacity: 0.8
},
title: {
fontSize: '1.5rem',
fontWeight: 600,
color: '#333',
margin: '0 0 0.75rem'
},
message: {
fontSize: '1rem',
color: '#666',
maxWidth: '500px',
lineHeight: 1.5,
margin: '0 0 1.5rem'
},
actions: {
display: 'flex',
gap: '1rem'
},
retryButton: {
padding: '0.6rem 1.5rem',
fontSize: '0.95rem',
fontWeight: 600,
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
background: '#00f2fe',
color: '#fff',
transition: 'opacity 0.2s'
},
backButton: {
padding: '0.6rem 1.5rem',
fontSize: '0.95rem',
fontWeight: 600,
border: '2px solid #00f2fe',
borderRadius: '6px',
cursor: 'pointer',
background: 'transparent',
color: '#00f2fe',
transition: 'opacity 0.2s'
}
};

export default PlayErrorBoundary;
11 changes: 10 additions & 1 deletion src/common/playlists/PlayMeta.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PageNotFound } from 'common';
import thumbPlay from 'images/thumb-play.png';
import { getProdUrl } from 'common/utils/commonUtils';
import { loadCoverImage } from 'common/utils/coverImageUtil';
import PlayErrorBoundary from 'common/playlists/PlayErrorBoundary';

function PlayMeta() {
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -87,6 +88,10 @@ function PlayMeta() {
const renderPlayComponent = () => {
const Comp = plays[play.component || toSanitized(play.title_name)];

if (!Comp) {
return <PageNotFound />;
}

return <Comp {...play} />;
};

Expand All @@ -103,7 +108,11 @@ function PlayMeta() {
<meta content={play.description} data-react-helmet="true" name="twitter:description" />
<meta content={ogTagImage} data-react-helmet="true" name="twitter:image" />
</Helmet>
<Suspense fallback={<Loader />}>{renderPlayComponent()}</Suspense>
<Suspense
fallback={<Loader subtitle="Please wait while the play loads" title="Loading Play..." />}
>
<PlayErrorBoundary playName={play.name}>{renderPlayComponent()}</PlayErrorBoundary>
</Suspense>
</>
);
}
Expand Down
12 changes: 0 additions & 12 deletions src/common/utils/sanitizeHTML.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ 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++) {
Expand Down
3 changes: 1 addition & 2 deletions src/plays/devblog/Pages/Article.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 = () => {
Expand Down Expand Up @@ -51,7 +50,7 @@ const Article = () => {

<div
className="mt-10 devBlog-article"
dangerouslySetInnerHTML={{ __html: sanitizeHTML(article.body_html) }}
dangerouslySetInnerHTML={{ __html: article.body_html }}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal of sanitization introduces an XSS vulnerability. The article.body_html is fetched from the dev.to API and rendered directly without sanitization. While dev.to is a trusted source, defense-in-depth security principles recommend sanitizing externally-sourced HTML. If the API is compromised or returns unexpected HTML, users could be exposed to XSS attacks. Consider restoring the sanitization for this external data source.

Copilot uses AI. Check for mistakes.
/>
</div>
) : (
Expand Down
7 changes: 3 additions & 4 deletions src/plays/fun-quiz/EndScreen.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// vendors
import { Fragment, useState } from 'react';
import sanitizeHTML from 'common/utils/sanitizeHTML';

// css
import './FrontScreen.scss';
Expand All @@ -17,17 +16,17 @@ const EndScreen = ({ quizSummary, redirectHome }) => {
<div className="question-number">Question: {currentQuestion?.qNo}</div>
<li
dangerouslySetInnerHTML={{
__html: sanitizeHTML(`${currentQuestion?.question}`)
__html: `${currentQuestion?.question}`
}}
/>
<span
dangerouslySetInnerHTML={{
__html: sanitizeHTML(`<br/><b>Ans</b>: ${currentQuestion?.correct_answer}<br/>`)
__html: `<br/><b>Ans</b>: ${currentQuestion?.correct_answer}<br/>`
}}
/>
<span
dangerouslySetInnerHTML={{
__html: sanitizeHTML(`<b>Your Answer</b>: ${currentQuestion?.your_answer}`)
__html: `<b>Your Answer</b>: ${currentQuestion?.your_answer}`
Comment on lines +19 to +29
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal of sanitization introduces an XSS vulnerability in the quiz summary. The questions and answers are sourced from the external opentdb.com API and stored in the result state. Rendering them without sanitization using dangerouslySetInnerHTML could allow malicious HTML to execute if the API is compromised. Consider restoring the sanitization for this external data source.

Copilot uses AI. Check for mistakes.
}}
/>
</div>
Expand Down
5 changes: 2 additions & 3 deletions src/plays/fun-quiz/QuizScreen.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import sanitizeHTML from 'common/utils/sanitizeHTML';

import './QuizScreen.scss';

Expand Down Expand Up @@ -150,15 +149,15 @@ function QuizScreen({ category, getQuizSummary }) {
<div className={`timer ${timer <= 5 && 'caution'}`}>{timer}</div>
<div className="question-info">Question: {questionNumber + 1}</div>
<div className="question">
<h1 dangerouslySetInnerHTML={{ __html: sanitizeHTML(currentQuestion?.question) }} />
<h1 dangerouslySetInnerHTML={{ __html: currentQuestion?.question }} />
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal of sanitization introduces an XSS vulnerability. The quiz questions and options come from an external API (opentdb.com), which could potentially be compromised or return malicious HTML. While this API is generally trusted, defense-in-depth security principles recommend sanitizing any externally-sourced HTML before rendering it with dangerouslySetInnerHTML. If the API is compromised or returns unexpected HTML, users could be exposed to XSS attacks. Consider restoring the sanitization for this external data source.

Copilot uses AI. Check for mistakes.
</div>
<div className="options">
{currentQuestion?.options?.map((option, index) => {
return (
<div className="single-opt" key={index}>
<div
className={itemClassDisplayController(option)}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(option) }}
dangerouslySetInnerHTML={{ __html: option }}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal of sanitization introduces an XSS vulnerability for quiz options from the external API. The options array is populated from opentdb.com API responses and rendered without sanitization. If the external API is compromised or returns malicious HTML in the answer choices, it could execute in users' browsers. Consider restoring the sanitization for this external data source to maintain defense-in-depth security.

Copilot uses AI. Check for mistakes.
onClick={handleAnswerClick(option)}
/>
</div>
Expand Down
3 changes: 1 addition & 2 deletions src/plays/markdown-editor/Output.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React from 'react';
import sanitizeHTML from 'common/utils/sanitizeHTML';

const Output = ({ md, text, mdPreviewBox }) => {
return (
<div
className="md-editor output-div"
dangerouslySetInnerHTML={{ __html: sanitizeHTML(md.render(text)) }}
dangerouslySetInnerHTML={{ __html: md.render(text) }}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces an XSS vulnerability by removing HTML sanitization. The markdown editor explicitly allows HTML input (line 10: html: true), and user-provided text is directly rendered without sanitization. While markdown-it provides some protection, allowing raw HTML without sanitization creates a security risk. Malicious users could inject script tags or event handlers through the markdown input. Consider using markdown-it with HTML disabled, or restore DOMPurify sanitization to prevent XSS attacks.

Copilot uses AI. Check for mistakes.
id={mdPreviewBox}
/>
);
Expand Down
5 changes: 4 additions & 1 deletion src/plays/text-to-speech/TextToSpeech.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ function TextToSpeech(props) {
<div className="tts-output-box">
{convertedText ? (
<>
<p className="tts-output-text">{convertedText}</p>
<p
className="tts-output-text"
dangerouslySetInnerHTML={{ __html: convertedText }}
/>
Comment on lines +161 to +164
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces a critical XSS (Cross-Site Scripting) vulnerability. The convertedText is set directly from user input via inputText (line 81: setConvertedText(inputText.trim())), and rendering it with dangerouslySetInnerHTML without sanitization allows arbitrary HTML/JavaScript injection. An attacker could inject malicious script tags that would execute in the user's browser. The previous sanitizeHTML wrapper provided protection against this attack vector. You must either restore the sanitization or use plain text rendering instead of dangerouslySetInnerHTML.

Copilot uses AI. Check for mistakes.

<button className="tts-speaker-btn" onClick={handleSpeak}>
{isSpeaking ? <FaStop size={28} /> : <FaVolumeUp size={28} />}
Expand Down
9 changes: 2 additions & 7 deletions src/plays/tube2tunes/Tube2tunes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,16 @@ function Tube2tunes(props) {
.then((res) => {
if (res.data.status === 'processing') {
setProcessingMsg(true);
setLoading(false);
} else if (res.data.status === 'fail') {
setFailedMsg(true);
Comment on lines 50 to 53
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change removes critical error handling logic. When the API returns 'processing' or 'fail' status, the loading state is no longer reset (setLoading(false) was removed). This will cause the UI to remain in a loading state indefinitely when these statuses occur, creating a poor user experience. The loading state should be set to false for all response scenarios including processing and failure cases.

Copilot uses AI. Check for mistakes.
setLoading(false);
} else {
setUrlResult(res.data.link);
setTitle(res.data.title);
setLoading(false);
}
})
.catch((err) => {
setError(true);
setLoading(false);
// eslint-disable-next-line no-console
console.error('Error: ', err);
.catch(() => {
// console.log('Error: ', err);
Comment on lines +60 to +61
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change removes all error handling for API failures. The catch block now silently ignores errors without setting any error state or stopping the loading indicator. When a network error or API failure occurs, the UI will remain stuck in a loading state indefinitely, and users won't be informed about the failure. You should restore the error state handling (setError(true) and setLoading(false)) to provide proper user feedback and UI state management.

Suggested change
.catch(() => {
// console.log('Error: ', err);
.catch((err) => {
setError(true);
setLoading(false);
// Optional: log error for debugging
console.error('Error fetching YouTube audio:', err);

Copilot uses AI. Check for mistakes.
});

inputUrlRef.current.value = '';
Expand Down
Loading