Risques sécurité dangerouslySetInnerHTML()

Fixes #192
This commit is contained in:
JubaAzul 2025-01-15 09:07:56 -05:00
parent a7e65d4095
commit f43da4c8ba
8 changed files with 47 additions and 8 deletions

View file

@ -2,10 +2,11 @@ import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon'; import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon';
import DOMPurify from 'dompurify';
describe('AnswerIcon', () => { describe('AnswerIcon', () => {
test('renders correct icon when correct is true', () => { test('renders correct icon when correct is true', () => {
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: true }) }} />); const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: true })) }} />);
const svgElement = container.querySelector('svg'); const svgElement = container.querySelector('svg');
expect(svgElement).toBeInTheDocument(); expect(svgElement).toBeInTheDocument();
@ -20,7 +21,7 @@ describe('AnswerIcon', () => {
}); });
test('renders incorrect icon when correct is false', () => { test('renders incorrect icon when correct is false', () => {
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: false }) }} />); const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: false })) }} />);
const svgElement = container.querySelector('svg'); const svgElement = container.querySelector('svg');
expect(svgElement).toBeInTheDocument(); expect(svgElement).toBeInTheDocument();

View file

@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
import Template, { ErrorTemplate } from './templates'; import Template, { ErrorTemplate } from './templates';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import './styles.css'; import './styles.css';
import DOMPurify from 'dompurify';
interface GIFTTemplatePreviewProps { interface GIFTTemplatePreviewProps {
questions: string[]; questions: string[];
@ -73,7 +74,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div> <div className="error">{error}</div>
) : isPreviewReady ? ( ) : isPreviewReady ? (
<div data-testid="preview-container"> <div data-testid="preview-container">
<div dangerouslySetInnerHTML={{ __html: items }}></div> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
</div> </div>
) : ( ) : (
<div className="loading">Chargement de la prévisualisation...</div> <div className="loading">Chargement de la prévisualisation...</div>

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import textType, { formatLatex } from '../../GiftTemplate/templates/TextType'; import textType, { formatLatex } from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
// import Latex from 'react-latex'; // import Latex from 'react-latex';
type Choices = { type Choices = {
@ -39,7 +40,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
{choices.map((choice, i) => { {choices.map((choice, i) => {
@ -56,7 +57,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
(choice.isCorrect ? '✅' : '❌')} (choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div> <div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}> <div className={`answer-text ${selected}`}>
<div dangerouslySetInnerHTML={{ __html: formatLatex(choice.text.text) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(formatLatex(choice.text.text)) }} />
</div> </div>
</Button> </Button>
{choice.feedback && showAnswer && ( {choice.feedback && showAnswer && (

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
type CorrectAnswer = { type CorrectAnswer = {
numberHigh?: number; numberHigh?: number;
@ -34,7 +35,7 @@ const NumericalQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div> <div>
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
type Choices = { type Choices = {
feedback: { format: string; text: string } | null; feedback: { format: string; text: string } | null;
@ -28,7 +29,7 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
interface Props { interface Props {
questionContent: TextFormat; questionContent: TextFormat;
@ -27,7 +28,7 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({ text: questionContent }) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: questionContent })) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
<Button <Button

28
package-lock.json generated Normal file
View file

@ -0,0 +1,28 @@
{
"name": "EvalueTonSavoir",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dompurify": "^3.2.3"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/dompurify": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"dompurify": "^3.2.3"
}
}