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 src/common/badges-dashboard/BadgeDetails.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Badge from './Badge';
import sanitizeHTML from 'common/utils/sanitizeHTML';
import './badge.css';
import sanitizeHTML from 'common/utils/sanitizeHTML';

const BadgeDetails = ({ badge, onClose }) => {
const makeClickableLinks = (badge) => {
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'
}
};
Copy link
Member

Choose a reason for hiding this comment

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

hey @Suvam-paul145 looks like ai generated, can you fix this i mean we don't need to make it complex, it's just a simple css stuff so make it simple please.


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
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
2 changes: 1 addition & 1 deletion src/plays/devblog/Pages/Article.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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';
import sanitizeHTML from 'common/utils/sanitizeHTML';

const Article = () => {
const [article, setArticle] = useState({});
Expand Down
1 change: 1 addition & 0 deletions src/plays/fun-quiz/EndScreen.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// vendors
import { Fragment, useState } from 'react';

import sanitizeHTML from 'common/utils/sanitizeHTML';

// css
Expand Down
2 changes: 1 addition & 1 deletion src/plays/fun-quiz/QuizScreen.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import sanitizeHTML from 'common/utils/sanitizeHTML';

import sanitizeHTML from 'common/utils/sanitizeHTML';
import './QuizScreen.scss';

// assets
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
6 changes: 5 additions & 1 deletion src/plays/text-to-speech/TextToSpeech.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { FaVolumeUp, FaStop } from 'react-icons/fa';
import PlayHeader from 'common/playlists/PlayHeader';
import sanitizeHTML from 'common/utils/sanitizeHTML';
import './styles.css';

function TextToSpeech(props) {
Expand Down Expand Up @@ -158,7 +159,10 @@ function TextToSpeech(props) {
<div className="tts-output-box">
{convertedText ? (
<>
<p className="tts-output-text">{convertedText}</p>
<p
className="tts-output-text"
dangerouslySetInnerHTML={{ __html: sanitizeHTML(convertedText) }}
/>
Comment on lines 162 to 165
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
4 changes: 2 additions & 2 deletions src/plays/tube2tunes/Tube2tunes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ function Tube2tunes(props) {
.catch((err) => {
setError(true);
setLoading(false);
// eslint-disable-next-line no-console
console.error('Error: ', err);
// Optional: log error for debugging
console.error('Error fetching YouTube audio:', err);
});

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