aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>2024-10-03 10:09:00 +0200
committersonartech <sonartech@sonarsource.com>2024-10-08 20:02:47 +0000
commitc9ecf7fa2f9af18f474a2aea2c7c3cefd3a6e80f (patch)
tree40b9f0694c1bd3baaee84ade9fa4059b851c7ae2 /server/sonar-web
parent12664adb48e6b95e521b0e05d81f916af2a88bc4 (diff)
downloadsonarqube-c9ecf7fa2f9af18f474a2aea2c7c3cefd3a6e80f.tar.gz
sonarqube-c9ecf7fa2f9af18f474a2aea2c7c3cefd3a6e80f.zip
SONAR-23249 Fix SSF-656 & SSF-657
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/.eslintrc22
-rw-r--r--server/sonar-web/design-system/.eslintrc22
-rw-r--r--server/sonar-web/design-system/package.json1
-rw-r--r--server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx33
-rw-r--r--server/sonar-web/design-system/src/components/Text.tsx13
-rw-r--r--server/sonar-web/design-system/src/helpers/index.ts1
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/helpers/__tests__/sanitize-test.tsx294
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts1
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/helpers/sanitize.tsx124
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx22
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx44
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/Login.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/Action.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/Params.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/rules/RuleDescription.tsx14
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts160
-rw-r--r--server/sonar-web/src/main/js/helpers/code-difference.ts5
-rw-r--r--server/sonar-web/src/main/js/helpers/sanitize.ts57
-rw-r--r--server/sonar-web/yarn.lock1
30 files changed, 652 insertions, 355 deletions
diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc
index 49071ab4dbf..7da42df461d 100644
--- a/server/sonar-web/.eslintrc
+++ b/server/sonar-web/.eslintrc
@@ -35,6 +35,28 @@
],
1
],
+ "react/forbid-component-props": [
+ "error",
+ {
+ "forbid": [
+ {
+ "propName": "dangerouslySetInnerHTML",
+ "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks"
+ }
+ ]
+ }
+ ],
+ "react/forbid-dom-props": [
+ "error",
+ {
+ "forbid": [
+ {
+ "propName": "dangerouslySetInnerHTML",
+ "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks"
+ }
+ ]
+ }
+ ],
"react/forbid-elements": [
"warn",
{
diff --git a/server/sonar-web/design-system/.eslintrc b/server/sonar-web/design-system/.eslintrc
index b6218abddcd..01e7937531f 100644
--- a/server/sonar-web/design-system/.eslintrc
+++ b/server/sonar-web/design-system/.eslintrc
@@ -5,6 +5,28 @@
"rules": {
// Custom SonarCloud config that differs from eslint-config-sonarqube
"camelcase": "off",
+ "react/forbid-component-props": [
+ "error",
+ {
+ "forbid": [
+ {
+ "propName": "dangerouslySetInnerHTML",
+ "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks"
+ }
+ ]
+ }
+ ],
+ "react/forbid-dom-props": [
+ "error",
+ {
+ "forbid": [
+ {
+ "propName": "dangerouslySetInnerHTML",
+ "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks"
+ }
+ ]
+ }
+ ],
"react/jsx-sort-props": "error",
"react/jsx-pascal-case": [2, { "allowNamespace": true }],
"react/jsx-no-constructed-context-values": "error",
diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json
index 866433a8085..2e459a6c9ee 100644
--- a/server/sonar-web/design-system/package.json
+++ b/server/sonar-web/design-system/package.json
@@ -72,6 +72,7 @@
"d3-shape": "3.2.0",
"d3-zoom": "3.0.0",
"date-fns": "4.1.0",
+ "dompurify": "3.1.7",
"lodash": "4.17.21",
"react": "18.3.1",
"react-day-picker": "8.10.0",
diff --git a/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
index 57cd229e71f..693c3cdde83 100644
--- a/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
+++ b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import styled from '@emotion/styled';
import classNames from 'classnames';
import hljs, { HighlightResult } from 'highlight.js';
@@ -26,6 +27,7 @@ import abap from 'highlightjs-sap-abap';
import tw from 'twin.macro';
import { themeColor, themeContrast } from '../helpers/theme';
import { hljsIssueIndicatorPlugin, hljsUnderlinePlugin } from '../sonar-aligned';
+import { SafeHTMLInjection, SanitizeLevel } from '../sonar-aligned/helpers/sanitize';
hljs.registerLanguage('abap', abap);
hljs.registerLanguage('apex', apex);
@@ -47,6 +49,7 @@ interface Props {
escapeDom?: boolean;
htmlAsString: string;
language?: string;
+ sanitizeLevel?: SanitizeLevel;
wrap?: boolean | 'words';
}
@@ -60,8 +63,15 @@ const htmlDecode = (escapedCode: string) => {
return doc.documentElement.textContent ?? '';
};
-export function CodeSyntaxHighlighter(props: Props) {
- const { className, htmlAsString, language, wrap, escapeDom = true } = props;
+export function CodeSyntaxHighlighter(props: Readonly<Props>) {
+ const {
+ className,
+ escapeDom = true,
+ htmlAsString,
+ language,
+ sanitizeLevel = SanitizeLevel.FORBID_STYLE,
+ wrap,
+ } = props;
let highlightedHtmlAsString = htmlAsString;
htmlAsString.match(GLOBAL_REGEXP)?.forEach((codeBlock) => {
@@ -95,16 +105,15 @@ export function CodeSyntaxHighlighter(props: Props) {
});
return (
- <StyledSpan
- className={classNames(
- `hljs ${className ?? ''}`,
- { 'code-wrap': wrap },
- { 'wrap-words': wrap === 'words' },
- )}
- // Safe: value is escaped by highlight.js
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: highlightedHtmlAsString }}
- />
+ <SafeHTMLInjection htmlAsString={highlightedHtmlAsString} sanitizeLevel={sanitizeLevel}>
+ <StyledSpan
+ className={classNames(
+ `hljs ${className ?? ''}`,
+ { 'code-wrap': wrap },
+ { 'wrap-words': wrap === 'words' },
+ )}
+ />
+ </SafeHTMLInjection>
);
}
diff --git a/server/sonar-web/design-system/src/components/Text.tsx b/server/sonar-web/design-system/src/components/Text.tsx
index 08437dcce22..25df5241e02 100644
--- a/server/sonar-web/design-system/src/components/Text.tsx
+++ b/server/sonar-web/design-system/src/components/Text.tsx
@@ -17,10 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import styled from '@emotion/styled';
import { ElementType } from 'react';
import tw from 'twin.macro';
import { themeColor, themeContrast } from '../helpers/theme';
+import { SafeHTMLInjection } from '../sonar-aligned/helpers/sanitize';
interface TextBoldProps {
className?: string;
@@ -32,12 +34,9 @@ interface TextBoldProps {
*/
export function TextBold({ match, name, className }: TextBoldProps) {
return match ? (
- <StyledText
- className={className}
- // Safe: comes from the search engine, that injects bold tags into component names
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: match }}
- />
+ <SafeHTMLInjection htmlAsString={match}>
+ <StyledText className={className} />
+ </SafeHTMLInjection>
) : (
<StyledText className={className} title={name}>
{name}
@@ -47,7 +46,7 @@ export function TextBold({ match, name, className }: TextBoldProps) {
/** @deprecated Use Text (with `isSubdued` prop) from Echoes instead.
*/
-export function TextMuted({ text, className }: { className?: string; text: string }) {
+export function TextMuted({ text, className }: Readonly<{ className?: string; text: string }>) {
return (
<StyledMutedText className={className} title={text}>
{text}
diff --git a/server/sonar-web/design-system/src/helpers/index.ts b/server/sonar-web/design-system/src/helpers/index.ts
index 73dd1464b22..7af0536ce90 100644
--- a/server/sonar-web/design-system/src/helpers/index.ts
+++ b/server/sonar-web/design-system/src/helpers/index.ts
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
export * from './colors';
export * from './constants';
export * from './keyboard';
diff --git a/server/sonar-web/design-system/src/sonar-aligned/helpers/__tests__/sanitize-test.tsx b/server/sonar-web/design-system/src/sonar-aligned/helpers/__tests__/sanitize-test.tsx
new file mode 100644
index 00000000000..b8be9b99daf
--- /dev/null
+++ b/server/sonar-web/design-system/src/sonar-aligned/helpers/__tests__/sanitize-test.tsx
@@ -0,0 +1,294 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { render, screen } from '@testing-library/react';
+import {
+ SafeHTMLInjection,
+ sanitizeHTMLNoSVGNoMathML,
+ sanitizeHTMLRestricted,
+ sanitizeHTMLToPreventCSSInjection,
+ sanitizeHTMLUserInput,
+} from '../sanitize';
+
+/*
+ * Test code borrowed from OWASP's sanitizer tests
+ * https://github.com/OWASP/java-html-sanitizer/blob/master/src/test/resources/org/owasp/html/htmllexerinput1.html
+ */
+const tainted = `<?xml version="not-even-close"?>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<!-- a test input for HtmlLexer -->
+
+<html lang="en" xml:lang="en">
+<head>
+<title>Test File For HtmlLexer &amp; HtmlParser</title>
+<link rel=stylesheet type="text/css" src=foo/bar.css />
+<body
+ bgcolor=white
+ linkcolor = "blue"
+ onload="document.writeln(
+ &quot;&lt;p&gt;properly escaped code in a handler&lt;/p&gt;&quot;);"
+>
+
+<script type="text/javascript"><!--
+document.writeln("<p>Some initialization code in global context</p>");
+--></script>
+
+<script type="text/javascript">
+// hi there
+document.writeln("<p>More initialization</p>");
+</script>
+
+<div id=clickydiv onclick="handleClicky(event)"
+ ondblclick=this.onclick(event);return(false)>
+Clicky
+</div>
+
+<input id=foo>
+<gxp:attr name="onchange">alert("&lt;b&gt;hi&lt;/b&gt;");</gxp:attr>
+</input>
+
+<pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+
+<!-- some tokenization corner cases -->
+
+< notatag <atag/>
+
+</ notatag> </redundantlyclosed/>
+
+<messyattributes a=b=c d="e"f=g h =i j= k l = m checked n="o"/>
+
+< < < all in one text block > > >
+
+<xmp>Make sure that <!-- comments don't obscure the xmp close</xmp>
+<% # some php code here
+write("<pre>$horriblySyntacticConstruct1</pre>\n\n");
+%>
+<script type="text/javascript"><!--
+alert("hello world");
+// --></script>
+
+<script>/* </script> */alert('hi');</script>
+<script><!--/* </script> */alert('hi');--></script>
+
+<xmp style=color:blue><!--/* </xmp> */alert('hi');--></xmp>
+
+<style><!-- p { contentf: '</style>' } --></style>
+<style>Foo<!-- > </style> --></style>
+<textarea><!-- Zoicks </textarea>--></textarea>
+<!-- An escaping text span start may share its U+002D HYPHEN-MINUS characters
+ - with its corresponding escaping text span end. -->
+<script><!--></script>
+<script><!---></script>
+<script><!----></script>
+
+This is <b>bold</b> and this is <i>italic</i> and this is <u>underlined</u>.
+<br />
+A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>.
+An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>.
+An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p style="color:blue">p</p>.
+A <strong>strong</strong> and a <a href="foo" ping="pong" rel="noopener" target="__blank" >link</a>
+
+<a href="javascript:alert('hello')" target="_blank">this is wrong</a>
+
+<svg><text>SVG isn't always allowed</text></svg>
+
+<math xmlns="http://www.w3.org/1998/Math/MathML">
+ <infinity />
+</math>
+
+</body>
+</html>
+<![CDATA[ No such thing as a CDATA> section in HTML ]]>
+<script>a<b</script>
+<img src=foo.gif /><a href=><a href=/>
+<span title=malformed attribs' do=don't id=foo checked onclick="a<b">Bar</span>`;
+
+describe('sanitizeHTMLToPreventCSSInjection', () => {
+ it('should strip off style attributes', () => {
+ const clean = `
+ <div id="clickydiv">
+ Clicky
+ </div>
+ <input id="foo">
+ alert("&lt;b&gt;hi&lt;/b&gt;");
+ <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("<pre>$horriblySyntacticConstruct1</pre>
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ <textarea>&lt;!-- Zoicks </textarea>--&gt;
+ This is <b>bold</b> and this is <i>italic</i> and this is <u>underlined</u>.
+ <br>
+ A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>.
+ An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>.
+ An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p>p</p>.
+ A <strong>strong</strong> and a <a rel="noopener" href="foo">link</a>
+ <a>this is wrong</a>
+ <svg><text>SVG isn't always allowed</text></svg>
+ <math xmlns="http://www.w3.org/1998/Math/MathML">
+ </math>
+ section in HTML ]]&gt;
+ <img src="foo.gif"><a href=""></a><a href="/">
+ <span checked="" id="foo" title="malformed">Bar</span></a>`;
+
+ expect(sanitizeHTMLToPreventCSSInjection(tainted).trimEnd().replace(/\s+/g, ' ')).toBe(
+ clean.replace(/\s+/g, ' '),
+ );
+ });
+});
+
+describe('sanitizeHTMLNoSVGNoMathML', () => {
+ it('should not allow MathML and SVG', () => {
+ const clean = `
+ <div id="clickydiv">
+ Clicky
+ </div>
+ <input id="foo">
+ alert("&lt;b&gt;hi&lt;/b&gt;");
+ <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("<pre>$horriblySyntacticConstruct1</pre>
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ <textarea>&lt;!-- Zoicks </textarea>--&gt;
+ This is <b>bold</b> and this is <i>italic</i> and this is <u>underlined</u>.
+ <br>
+ A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>.
+ An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>.
+ An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p>p</p>.
+ A <strong>strong</strong> and a <a rel="noopener" href="foo">link</a>
+ <a>this is wrong</a>
+ section in HTML ]]&gt;
+ <img src="foo.gif"><a href=""></a><a href="/">
+ <span checked="" id="foo" title="malformed">Bar</span></a>`;
+
+ expect(sanitizeHTMLNoSVGNoMathML(tainted).trimEnd().replace(/\s+/g, ' ')).toBe(
+ clean.replace(/\s+/g, ' '),
+ );
+ });
+});
+
+describe('sanitizeHTMLUserInput', () => {
+ it('should preserve only specific formatting tags and attributes', () => {
+ const clean = `
+ Clicky
+ alert("&lt;b&gt;hi&lt;/b&gt;");
+ <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("<pre>$horriblySyntacticConstruct1</pre>
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ &lt;!-- Zoicks --&gt;
+ This is <b>bold</b> and this is <i>italic</i> and this is underlined.
+ <br>
+ A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>.
+ An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>.
+ An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p>p</p>.
+ A <strong>strong</strong> and a <a rel="noopener" href="foo">link</a>
+ <a>this is wrong</a>
+ section in HTML ]]&gt;
+ <a href=""></a><a href="/">
+ Bar</a>`;
+
+ expect(sanitizeHTMLUserInput(tainted).trimEnd().replace(/\s+/g, ' ')).toBe(
+ clean.replace(/\s+/g, ' '),
+ );
+ });
+});
+
+describe('sanitizeHTMLRestricted', () => {
+ it('should preserve only a very limited list of formatting tags and attributes', () => {
+ const clean = `
+ Clicky
+ alert("&lt;b&gt;hi&lt;/b&gt;");
+ &lt;div id=notarealtag onclick=notcode()&gt;
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("$horriblySyntacticConstruct1
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ &lt;!-- Zoicks --&gt;
+ This is <b>bold</b> and this is <i>italic</i> and this is underlined.
+ <br>
+ A quote and a <code>code</code> and a pre.
+ An h1 and an h2 and an h3 and an h4 and an h5 and an h6.
+ An <li>ol</li> and a <ul><li>ul</li></ul> and a <p>p</p>.
+ A <strong>strong</strong> and a <a href="foo">link</a>
+ <a>this is wrong</a>
+ section in HTML ]]&gt;
+ <a href=""></a><a href="/">
+ Bar</a>`;
+
+ expect(sanitizeHTMLRestricted(tainted).trimEnd().replace(/\s+/g, ' ')).toBe(
+ clean.replace(/\s+/g, ' '),
+ );
+ });
+});
+
+describe('SafeHTMLInjection', () => {
+ it('should default to a span and the SanitizeLevel.FORBID_STYLE level', () => {
+ const tainted = `
+ <head>
+ <link rel=stylesheet type="text/css" src=foo/bar.css />
+ <style>some style</style>
+ </head>
+
+ <body>
+ <p style="color:blue">a stylish paragraph</p>
+
+ <svg><text>SVG isn't always allowed</text></svg>
+
+ <math xmlns="http://www.w3.org/1998/Math/MathML">
+ <infinity />
+ </math>
+ </body>
+ `;
+
+ render(<SafeHTMLInjection htmlAsString={tainted} />);
+
+ expect(screen.getByText('a stylish paragraph')).toBeInTheDocument();
+ expect(screen.getByText("SVG isn't always allowed")).toBeInTheDocument();
+ });
+});
diff --git a/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts b/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts
index 863797b2d1f..e288f05e8ee 100644
--- a/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts
+++ b/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts
@@ -18,4 +18,5 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+export * from './sanitize';
export * from './tabs';
diff --git a/server/sonar-web/design-system/src/sonar-aligned/helpers/sanitize.tsx b/server/sonar-web/design-system/src/sonar-aligned/helpers/sanitize.tsx
new file mode 100644
index 00000000000..1426c567efd
--- /dev/null
+++ b/server/sonar-web/design-system/src/sonar-aligned/helpers/sanitize.tsx
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import dompurify from 'dompurify';
+
+import React from 'react';
+
+const { sanitize } = dompurify;
+
+export enum SanitizeLevel {
+ FORBID_STYLE, // minimum sanitation level to prevent CSS injections
+ FORBID_SVG_MATHML, // adds SVG and MathML exclusion
+ USER_INPUT, // adds restrictions on tags and attributes
+ RESTRICTED, // adds even more restrictions on tags and attributes
+}
+
+export const sanitizeFunctionByLevel = (sanitizeLevel: SanitizeLevel) =>
+ ({
+ [SanitizeLevel.FORBID_STYLE]: sanitizeHTMLToPreventCSSInjection,
+ [SanitizeLevel.FORBID_SVG_MATHML]: sanitizeHTMLNoSVGNoMathML,
+ [SanitizeLevel.USER_INPUT]: sanitizeHTMLUserInput,
+ [SanitizeLevel.RESTRICTED]: sanitizeHTMLRestricted,
+ })[sanitizeLevel];
+
+export const sanitizeHTMLToPreventCSSInjection = (htmlAsString: string) =>
+ sanitize(htmlAsString, {
+ FORBID_ATTR: ['style'],
+ FORBID_TAGS: ['style'],
+ });
+
+export function sanitizeHTMLNoSVGNoMathML(htmlAsString: string) {
+ return sanitize(htmlAsString, {
+ FORBID_ATTR: ['style'],
+ FORBID_TAGS: ['style'],
+ USE_PROFILES: { html: true },
+ });
+}
+
+export function sanitizeHTMLUserInput(htmlAsString: string) {
+ return sanitize(htmlAsString, {
+ ALLOWED_ATTR: ['href', 'rel'],
+ ALLOWED_TAGS: [
+ 'a',
+ 'b',
+ 'blockquote',
+ 'br',
+ 'code',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'i',
+ 'li',
+ 'ol',
+ 'p',
+ 'pre',
+ 'strong',
+ 'ul',
+ ],
+ });
+}
+
+export function sanitizeHTMLRestricted(htmlAsString: string) {
+ return sanitize(htmlAsString, {
+ ALLOWED_ATTR: ['href'],
+ ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'i', 'li', 'p', 'strong', 'ul'],
+ });
+}
+
+/**
+ * Safely injects HTML into an element with no risk of XSS attacks.
+ *
+ * @param children The React element to clone with the sanitized HTML (defaults to a `span`)
+ * @param htmlAsString The HTML string to sanitize and inject (required)
+ * @param sanitizeLevel The level of sanitation to apply (defaults to `SanitizeLevel.FORBID_STYLE`)
+ *
+ * @returns A React element with the sanitized HTML injected, and all other props preserved
+ *
+ * @example
+ * Here's a simple example with no children:
+ * ```
+ * <SafeHTMLInjection htmlAsString={taintedString} />
+ * ```
+ *
+ * @example
+ * Here's an example with a custom `sanitizeLevel` and a child `div`:
+ * ```
+ * <SafeHTMLInjection htmlAsString={taintedString} sanitizeLevel={SanitizeLevel.RESTRICTED}>
+ * // the HTML will be safely injected in the div below, with the className preserved:
+ * <div className="someClassThatWillBePreserved" />
+ * </SafeHTMLInjection>
+ * ```
+ */
+export const SafeHTMLInjection = ({
+ children,
+ htmlAsString,
+ sanitizeLevel = SanitizeLevel.FORBID_STYLE,
+}: Readonly<{
+ children?: React.ReactElement;
+ htmlAsString: string;
+ sanitizeLevel?: SanitizeLevel;
+}>) =>
+ React.cloneElement(children ?? <span />, {
+ dangerouslySetInnerHTML: { __html: sanitizeFunctionByLevel(sanitizeLevel)(htmlAsString) },
+ });
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx
index 3671ee2f0f8..c6134af333c 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx
@@ -17,13 +17,20 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { Button, ButtonVariety } from '@sonarsource/echoes-react';
-import { FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system';
+import {
+ FlagMessage,
+ HtmlFormatter,
+ Modal,
+ SafeHTMLInjection,
+ SanitizeLevel,
+ Spinner,
+} from 'design-system';
import * as React from 'react';
import { dismissAnalysisWarning, getTask } from '../../../api/ce';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import { translate } from '../../../helpers/l10n';
-import { sanitizeStringRestricted } from '../../../helpers/sanitize';
import { TaskWarning } from '../../../types/tasks';
import { CurrentUser } from '../../../types/users';
@@ -118,11 +125,9 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> {
<div className="sw-flex sw-items-center sw-mt-2">
<FlagMessage variant="warning">
<HtmlFormatter>
- <span
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
- }}
+ <SafeHTMLInjection
+ htmlAsString={message.trim().replace(/\n/g, '<br />')}
+ sanitizeLevel={SanitizeLevel.RESTRICTED}
/>
</HtmlFormatter>
</FlagMessage>
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
index 7aa212f1358..efa190f33f0 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx
@@ -27,6 +27,8 @@ import {
InputTextArea,
LabelValueSelectOption,
Note,
+ SafeHTMLInjection,
+ SanitizeLevel,
Switch,
} from 'design-system';
import * as React from 'react';
@@ -35,7 +37,6 @@ import { Profile } from '../../../api/quality-profiles';
import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
import DocumentationLink from '../../../components/common/DocumentationLink';
import { DocLink } from '../../../helpers/doc-links';
-import { sanitizeString } from '../../../helpers/sanitize';
import { useActivateRuleMutation } from '../../../queries/quality-profiles';
import { Feature } from '../../../types/features';
import { IssueSeverity } from '../../../types/issues';
@@ -265,11 +266,12 @@ export default function ActivationFormModal(props: Readonly<Props>) {
/>
)}
{param.htmlDesc !== undefined && (
- <Note
- as="div"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
- />
+ <SafeHTMLInjection
+ htmlAsString={param.htmlDesc}
+ sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
+ >
+ <Note as="div" />
+ </SafeHTMLInjection>
)}
</FormField>
))
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
index 4f7142ab3a9..2c6fb0cd8e6 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { HttpStatusCode } from 'axios';
import {
ButtonPrimary,
@@ -28,6 +29,8 @@ import {
LabelValueSelectOption,
LightLabel,
Modal,
+ SafeHTMLInjection,
+ SanitizeLevel,
} from 'design-system';
import * as React from 'react';
import { Status } from '~sonar-aligned/types/common';
@@ -36,7 +39,6 @@ import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsEx
import { RULE_STATUSES } from '../../../helpers/constants';
import { csvEscape } from '../../../helpers/csv';
import { translate } from '../../../helpers/l10n';
-import { sanitizeString } from '../../../helpers/sanitize';
import { latinize } from '../../../helpers/strings';
import { useCreateRuleMutation, useUpdateRuleMutation } from '../../../queries/rules';
import {
@@ -294,11 +296,14 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
value={actualValue}
/>
)}
+
{param.htmlDesc !== undefined && (
- <LightLabel
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
- />
+ <SafeHTMLInjection
+ htmlAsString={param.htmlDesc}
+ sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
+ >
+ <LightLabel />
+ </SafeHTMLInjection>
)}
</FormField>
);
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
index 0a9ac8ee0d4..5b1dffbbc25 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
@@ -23,13 +23,13 @@ import {
ButtonSecondary,
CodeSyntaxHighlighter,
InputTextArea,
+ SanitizeLevel,
} from 'design-system';
import * as React from 'react';
import FormattingTips from '../../../components/common/FormattingTips';
import RuleTabViewer from '../../../components/rules/RuleTabViewer';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { sanitizeUserInput } from '../../../helpers/sanitize';
import { useUpdateRuleMutation } from '../../../queries/rules';
import { RuleDetails } from '../../../types/types';
import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
@@ -61,8 +61,9 @@ export default function RuleDetailsDescription(props: Readonly<Props>) {
{ruleDetails.htmlNote !== undefined && (
<CodeSyntaxHighlighter
className="markdown sw-my-6"
- htmlAsString={sanitizeUserInput(ruleDetails.htmlNote)}
+ htmlAsString={ruleDetails.htmlNote}
language={ruleDetails.lang}
+ sanitizeLevel={SanitizeLevel.USER_INPUT}
/>
)}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx
index fc7a6486c66..316e72a51aa 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx
@@ -17,10 +17,18 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { CellComponent, Note, SubHeadingHighlight, Table, TableRow } from 'design-system';
+
+import {
+ CellComponent,
+ Note,
+ SafeHTMLInjection,
+ SanitizeLevel,
+ SubHeadingHighlight,
+ Table,
+ TableRow,
+} from 'design-system';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import { sanitizeString } from '../../../helpers/sanitize';
import { RuleParameter } from '../../../types/types';
interface Props {
@@ -38,10 +46,12 @@ export default function RuleDetailsParameters({ params }: Props) {
<CellComponent>
<div className="sw-flex sw-flex-col sw-gap-2">
{param.htmlDesc !== undefined && (
- <div
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }}
- />
+ <SafeHTMLInjection
+ htmlAsString={param.htmlDesc}
+ sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
+ >
+ <div />
+ </SafeHTMLInjection>
)}
{param.defaultValue !== undefined && (
<Note as="div">
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx
index df464f021d5..cc7b2e74d1b 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import styled from '@emotion/styled';
import {
DangerButtonPrimary,
@@ -26,6 +27,8 @@ import {
LightLabel,
Modal,
PencilIcon,
+ SafeHTMLInjection,
+ SanitizeLevel,
TrashIcon,
themeBorder,
} from 'design-system';
@@ -35,7 +38,6 @@ import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff';
import Avatar from '../../../components/ui/Avatar';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { sanitizeUserInput } from '../../../helpers/sanitize';
import { ReviewHistoryType } from '../../../types/security-hotspots';
import { Issue, IssueChangelog } from '../../../types/types';
import HotspotCommentModal from '../../security-hotspots/components/HotspotCommentModal';
@@ -47,7 +49,20 @@ export interface HotspotReviewHistoryProps {
onEditComment: (key: string, comment: string) => void;
}
-export default function IssueReviewHistory(props: HotspotReviewHistoryProps) {
+const getUpdatedChangelog = ({ changelog }: { changelog: IssueChangelog[] }) =>
+ changelog.map((changelogItem) => {
+ const diffHasIssueStatusChange = changelogItem.diffs.some((diff) => diff.key === 'issueStatus');
+
+ return {
+ ...changelogItem,
+ // If the diff is an issue status change, we remove deprecated status and resolution diffs
+ diffs: changelogItem.diffs.filter(
+ (diff) => !(diffHasIssueStatusChange && ['resolution', 'status'].includes(diff.key)),
+ ),
+ };
+ });
+
+export default function IssueReviewHistory(props: Readonly<HotspotReviewHistoryProps>) {
const { issue } = props;
const [changeLog, setChangeLog] = React.useState<IssueChangelog[]>([]);
const history = useGetIssueReviewHistory(issue, changeLog);
@@ -57,19 +72,8 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) {
React.useEffect(() => {
getIssueChangelog(issue.key).then(
({ changelog }) => {
- const updatedChangelog = changelog.map((changelogItem) => {
- const diffHasIssueStatusChange = changelogItem.diffs.some(
- (diff) => diff.key === 'issueStatus',
- );
-
- return {
- ...changelogItem,
- // If the diff is an issue status change, we remove deprecated status and resolution diffs
- diffs: changelogItem.diffs.filter(
- (diff) => !(diffHasIssueStatusChange && ['resolution', 'status'].includes(diff.key)),
- ),
- };
- });
+ const updatedChangelog = getUpdatedChangelog({ changelog });
+
setChangeLog(updatedChangelog);
},
() => {},
@@ -85,6 +89,7 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) {
<div className="sw-typo-semibold sw-mb-1">
<DateTimeFormatter date={date} />
</div>
+
<LightLabel as="div" className="sw-flex sw-gap-2">
{user.name && (
<div className="sw-flex sw-items-center sw-gap-1">
@@ -112,11 +117,9 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) {
{type === ReviewHistoryType.Comment && key && html && markdown && (
<div className="sw-mt-2 sw-flex sw-justify-between">
- <CommentBox
- className="sw-pl-2 sw-ml-2 sw-typo-default"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeUserInput(html) }}
- />
+ <SafeHTMLInjection htmlAsString={html} sanitizeLevel={SanitizeLevel.USER_INPUT}>
+ <CommentBox className="sw-pl-2 sw-ml-2 sw-typo-default" />
+ </SafeHTMLInjection>
{updatable && (
<div className="sw-flex sw-gap-6">
@@ -127,6 +130,7 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) {
size="small"
stopPropagation={false}
/>
+
<DestructiveIcon
Icon={TrashIcon}
aria-label={translate('issue.comment.delete')}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx
index 5a4daac6b9e..3390089477d 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx
@@ -17,12 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { Button, ButtonVariety } from '@sonarsource/echoes-react';
-import { FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system';
+import {
+ FlagMessage,
+ HtmlFormatter,
+ Modal,
+ SafeHTMLInjection,
+ SanitizeLevel,
+ Spinner,
+} from 'design-system';
import * as React from 'react';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import { translate } from '../../../helpers/l10n';
-import { sanitizeStringRestricted } from '../../../helpers/sanitize';
import { useDismissBranchWarningMutation } from '../../../queries/branch';
import { TaskWarning } from '../../../types/tasks';
import { Component } from '../../../types/types';
@@ -51,11 +58,9 @@ export function AnalysisWarningsModal(props: Props) {
<div className="sw-flex sw-items-center sw-mt-2">
<FlagMessage variant="warning">
<HtmlFormatter>
- <span
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')),
- }}
+ <SafeHTMLInjection
+ htmlAsString={message.trim().replace(/\n/g, '<br />')}
+ sanitizeLevel={SanitizeLevel.RESTRICTED}
/>
</HtmlFormatter>
</FlagMessage>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
index 5b0cf3fbd3f..75a88ada80d 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import styled from '@emotion/styled';
import {
DangerButtonPrimary,
@@ -26,6 +27,8 @@ import {
LightLabel,
Modal,
PencilIcon,
+ SafeHTMLInjection,
+ SanitizeLevel,
TrashIcon,
themeBorder,
} from 'design-system';
@@ -34,7 +37,6 @@ import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff';
import Avatar from '../../../components/ui/Avatar';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { sanitizeUserInput } from '../../../helpers/sanitize';
import { Hotspot, ReviewHistoryType } from '../../../types/security-hotspots';
import { getHotspotReviewHistory } from '../utils';
import HotspotCommentModal from './HotspotCommentModal';
@@ -45,7 +47,7 @@ export interface HotspotReviewHistoryProps {
onEditComment: (key: string, comment: string) => void;
}
-export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) {
+export default function HotspotReviewHistory(props: Readonly<HotspotReviewHistoryProps>) {
const { hotspot } = props;
const history = getHotspotReviewHistory(hotspot);
const [editCommentKey, setEditCommentKey] = React.useState('');
@@ -86,11 +88,9 @@ export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) {
{type === ReviewHistoryType.Comment && key && html && markdown && (
<div className="sw-mt-2 sw-flex sw-justify-between">
- <CommentBox
- className="sw-pl-2 sw-ml-2 sw-typo-default"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeUserInput(html) }}
- />
+ <SafeHTMLInjection htmlAsString={html} sanitizeLevel={SanitizeLevel.USER_INPUT}>
+ <CommentBox className="sw-pl-2 sw-ml-2 sw-typo-default" />
+ </SafeHTMLInjection>
{updatable && (
<div className="sw-flex sw-gap-6">
diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
index 21ea7d31278..1e30ae1f29e 100644
--- a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
+++ b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx
@@ -24,6 +24,8 @@ import {
Card,
FlagMessage,
PageContentFontWrapper,
+ SafeHTMLInjection,
+ SanitizeLevel,
Title,
themeBorder,
themeColor,
@@ -33,7 +35,6 @@ import { Helmet } from 'react-helmet-async';
import { Image } from '~sonar-aligned/components/common/Image';
import { Location } from '~sonar-aligned/types/router';
import { translate } from '../../../helpers/l10n';
-import { sanitizeUserInput } from '../../../helpers/sanitize';
import { getReturnUrl } from '../../../helpers/urls';
import { IdentityProvider } from '../../../types/types';
import LoginForm from './LoginForm';
@@ -69,11 +70,9 @@ export default function Login(props: Readonly<LoginProps>) {
)}
{message !== undefined && message.length > 0 && (
- <StyledMessage
- className="markdown sw-rounded-2 sw-p-4 sw-mb-6"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeUserInput(message) }}
- />
+ <SafeHTMLInjection htmlAsString={message} sanitizeLevel={SanitizeLevel.USER_INPUT}>
+ <StyledMessage className="markdown sw-rounded-2 sw-p-4 sw-mb-6" />
+ </SafeHTMLInjection>
)}
{identityProviders.length > 0 && (
diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx
index b2635d566e9..89604821359 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx
@@ -17,11 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { Text, Tooltip } from '@sonarsource/echoes-react';
-import { SubHeading } from 'design-system';
+import { SafeHTMLInjection, SanitizeLevel, SubHeading } from 'design-system';
import * as React from 'react';
import { translateWithParameters } from '../../../helpers/l10n';
-import { sanitizeStringRestricted } from '../../../helpers/sanitize';
import { ExtendedSettingDefinition } from '../../../types/settings';
import { getPropertyDescription, getPropertyName } from '../utils';
@@ -40,11 +40,9 @@ export default function DefinitionDescription({ definition }: Readonly<Props>) {
</SubHeading>
{description && (
- <div
- className="markdown sw-mt-1"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeStringRestricted(description) }}
- />
+ <SafeHTMLInjection htmlAsString={description} sanitizeLevel={SanitizeLevel.RESTRICTED}>
+ <div className="markdown sw-mt-1" />
+ </SafeHTMLInjection>
)}
<Tooltip content={translateWithParameters('settings.key_x', definition.key)}>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
index bea02959d5f..40c4b3a8362 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
@@ -17,12 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { BasicSeparator, Note, SubTitle } from 'design-system';
+
+import { BasicSeparator, Note, SafeHTMLInjection, SanitizeLevel, SubTitle } from 'design-system';
import { groupBy, sortBy } from 'lodash';
import * as React from 'react';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { Location } from '~sonar-aligned/types/router';
-import { sanitizeStringRestricted } from '../../../helpers/sanitize';
import { SettingDefinitionAndValue } from '../../../types/settings';
import { Component } from '../../../types/types';
import { SUB_CATEGORY_EXCLUSIONS } from '../constants';
@@ -93,15 +93,16 @@ class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefiniti
{subCategory.name}
</SubTitle>
)}
+
{subCategory.description != null && (
- <Note
- className="markdown"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: sanitizeStringRestricted(subCategory.description),
- }}
- />
+ <SafeHTMLInjection
+ htmlAsString={subCategory.description}
+ sanitizeLevel={SanitizeLevel.RESTRICTED}
+ >
+ <Note className="markdown" />
+ </SafeHTMLInjection>
)}
+
<BasicSeparator className="sw-mt-6" />
<DefinitionsList
component={component}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx
index d2372b36a5f..d57d717c4fb 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx
@@ -24,13 +24,14 @@ import {
HtmlFormatter,
InputTextArea,
PencilIcon,
+ SafeHTMLInjection,
+ SanitizeLevel,
themeBorder,
themeColor,
} from 'design-system';
import * as React from 'react';
import FormattingTipsWithLink from '../../../../components/common/FormattingTipsWithLink';
import { translate } from '../../../../helpers/l10n';
-import { sanitizeUserInput } from '../../../../helpers/sanitize';
import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';
function InputForFormattedText(
@@ -64,10 +65,12 @@ function InputForFormattedText(
) : (
<>
<HtmlFormatter>
- <FormattedPreviewBox
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeUserInput(formattedValue ?? '') }}
- />
+ <SafeHTMLInjection
+ htmlAsString={formattedValue ?? ''}
+ sanitizeLevel={SanitizeLevel.USER_INPUT}
+ >
+ <FormattedPreviewBox />
+ </SafeHTMLInjection>
</HtmlFormatter>
<ButtonSecondary className="sw-mt-2" onClick={props.onEditing} icon={<PencilIcon />}>
diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx
index 8666c9bdbc2..9beb4beb9af 100644
--- a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx
+++ b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx
@@ -17,7 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Badge, Card, LinkBox, LinkIcon, SubHeading, Tabs } from 'design-system';
+
+import { Badge, Card, LinkBox, LinkIcon, SafeHTMLInjection, SubHeading, Tabs } from 'design-system';
import * as React from 'react';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -95,11 +96,9 @@ export default function Action(props: Props) {
{action.deprecatedSince && <DeprecatedBadge since={action.deprecatedSince} />}
</header>
- <div
- className="sw-mt-4 markdown"
- // Safe: comes from the backend
- dangerouslySetInnerHTML={{ __html: action.description }}
- />
+ <SafeHTMLInjection htmlAsString={action.description}>
+ <div className="sw-mt-4 markdown" />
+ </SafeHTMLInjection>
<div className="sw-mt-4">
<Tabs options={tabOptions} onChange={(opt) => setTab(opt)} value={tab} />
diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx
index 711dc6c8c4a..76ed0840db7 100644
--- a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx
+++ b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx
@@ -17,7 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { SubTitle } from 'design-system';
+
+import { SafeHTMLInjection, SubTitle } from 'design-system';
import { isEmpty } from 'lodash';
import * as React from 'react';
import { WebApi } from '../../../types/types';
@@ -45,11 +46,9 @@ export default function Domain({ domain, query }: Props) {
</header>
{!isEmpty(domain.description) && (
- <div
- className="sw-mt-3 markdown"
- // Safe: comes from the backend
- dangerouslySetInnerHTML={{ __html: domain.description }}
- />
+ <SafeHTMLInjection htmlAsString={domain.description}>
+ <div className="sw-mt-3 markdown" />
+ </SafeHTMLInjection>
)}
<div className="sw-mt-4 sw-flex sw-flex-col sw-gap-4">
diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx
index 865fc766b09..b88f080e86e 100644
--- a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx
+++ b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx
@@ -17,7 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ContentCell, DarkLabel, HtmlFormatter, Note, Table, TableRow } from 'design-system';
+
+import {
+ ContentCell,
+ DarkLabel,
+ HtmlFormatter,
+ Note,
+ SafeHTMLInjection,
+ Table,
+ TableRow,
+} from 'design-system';
import * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { WebApi } from '../../../types/types';
@@ -108,11 +117,9 @@ export default class Params extends React.PureComponent<Props> {
{this.renderKey(param)}
<ContentCell>
- <div
- className="markdown"
- // Safe: comes from the backend
- dangerouslySetInnerHTML={{ __html: param.description }}
- />
+ <SafeHTMLInjection htmlAsString={param.description}>
+ <div className="markdown" />
+ </SafeHTMLInjection>
</ContentCell>
<ContentCell>
diff --git a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
index ca1341902e4..a5cf16b7a96 100644
--- a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
@@ -22,6 +22,7 @@ import {
CodeSyntaxHighlighter,
FlagMessage,
HtmlFormatter,
+ SanitizeLevel,
ToggleButton,
themeBorder,
themeColor,
@@ -30,7 +31,6 @@ import * as React from 'react';
import { RuleDescriptionSection, RuleDescriptionSections } from '../../apps/coding-rules/rule';
import applyCodeDifferences from '../../helpers/code-difference';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { sanitizeString } from '../../helpers/sanitize';
import { isDefined } from '../../helpers/types';
import { Cve as CveDetailsType } from '../../types/cves';
import { CveDetails } from './CveDetails';
@@ -147,8 +147,9 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
{isDefined(introductionSection) && (
<CodeSyntaxHighlighter
className="rule-desc"
- htmlAsString={sanitizeString(introductionSection)}
+ htmlAsString={introductionSection}
language={language}
+ sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
/>
)}
{defaultContext && (
@@ -180,8 +181,9 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
<OtherContextOption />
) : (
<CodeSyntaxHighlighter
- htmlAsString={sanitizeString(selectedContext.content)}
+ htmlAsString={selectedContext.content}
language={language}
+ sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
/>
)}
@@ -200,14 +202,16 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
{isDefined(introductionSection) && (
<CodeSyntaxHighlighter
className="rule-desc"
- htmlAsString={sanitizeString(introductionSection)}
+ htmlAsString={introductionSection}
language={language}
+ sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
/>
)}
<CodeSyntaxHighlighter
- htmlAsString={sanitizeString(sections[0].content)}
+ htmlAsString={sections[0].content}
language={language}
+ sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML}
/>
{cve && <CveDetails cve={cve} />}
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx b/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx
index fe90bf7bfa6..f65317b396d 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx
+++ b/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { render } from '@testing-library/react';
+import { SafeHTMLInjection } from 'design-system/lib';
import React from 'react';
import applyCodeDifferences from '../code-difference';
@@ -146,12 +147,8 @@ public void endpoint(HttpServletRequest request, HttpServletResponse response) t
function renderDom(codeSnippet: string) {
return render(
- <div
- className="markdown"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{
- __html: codeSnippet,
- }}
- />,
+ <SafeHTMLInjection htmlAsString={codeSnippet}>
+ <div className="markdown" />
+ </SafeHTMLInjection>,
);
}
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts
deleted file mode 100644
index f219b154746..00000000000
--- a/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { sanitizeString, sanitizeStringRestricted } from '../sanitize';
-
-describe('sanitizeStringRestricted', () => {
- it('should preserve only specific formatting tags', () => {
- expect(
- sanitizeStringRestricted(`
- Hi <a href="http://example.com" target="_blank">this</a> is <i>in italics</i> and <ul>
- <li> lists </li>
- <li> are allowed</li>
- </ul>
- <p>
- as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
- and <code>code.is.accepted too</code>
- </p>
- `),
- ).toBe(`
- Hi <a target="_blank" href="http://example.com">this</a> is <i>in italics</i> and <ul>
- <li> lists </li>
- <li> are allowed</li>
- </ul>
- <p>
- as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
- and <code>code.is.accepted too</code>
- </p>
- `);
- });
-
- /*
- * Test code borrowed from OWASP's sanitizer tests
- * https://github.com/OWASP/java-html-sanitizer/blob/master/src/test/resources/org/owasp/html/htmllexerinput1.html
- */
- it('should strip everything else', () => {
- const clean = sanitizeStringRestricted(`<?xml version="not-even-close"?>
-
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
-
- <!-- a test input for HtmlLexer -->
-
- <html>
- <head>
- <title>Test File For HtmlLexer &amp; HtmlParser</title>
- <link rel=stylesheet type="text/css" src=foo/bar.css />
- <body
- bgcolor=white
- linkcolor = "blue"
- onload="document.writeln(
- &quot;&lt;p&gt;properly escaped code in a handler&lt;/p&gt;&quot;);"
- >
-
- <script type="text/javascript"><!--
- document.writeln("<p>Some initialization code in global context</p>");
- --></script>
-
- <script type="text/javascript">
- // hi there
- document.writeln("<p>More initialization</p>");
- </script>
-
- <div id=clickydiv onclick="handleClicky(event)"
- ondblclick=this.onclick(event);return(false)>
- Clicky
- </div>
-
- <input id=foo>
- <gxp:attr name="onchange">alert("&lt;b&gt;hi&lt;/b&gt;");</gxp:attr>
- </input>
-
- <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
-
- <!-- some tokenization corner cases -->
-
- < notatag <atag/>
-
- </ notatag> </redundantlyclosed/>
-
- <messyattributes a=b=c d="e"f=g h =i j= k l = m checked n="o"/>
-
- < < < all in one text block > > >
-
- <xmp>Make sure that <!-- comments don't obscure the xmp close</xmp>
- <% # some php code here
- write("<pre>$horriblySyntacticConstruct1</pre>\n\n");
- %>
- <script type="text/javascript"><!--
- alert("hello world");
- // --></script>
-
- <script>/* </script> */alert('hi');</script>
- <script><!--/* </script> */alert('hi');--></script>
-
- <xmp style=color:blue><!--/* </xmp> */alert('hi');--></xmp>
-
- <style><!-- p { contentf: '</style>' } --></style>
- <style>Foo<!-- > </style> --></style>
- <textarea><!-- Zoicks </textarea>--></textarea>
- <!-- An escaping text span start may share its U+002D HYPHEN-MINUS characters
- - with its corresponding escaping text span end. -->
- <script><!--></script>
- <script><!---></script>
- <script><!----></script>
- </body>
- </html>
- <![CDATA[ No such thing as a CDATA> section in HTML ]]>
- <script>a<b</script>
- <img src=foo.gif /><a href=><a href=/>
- <span title=malformed attribs' do=don't id=foo checked onclick="a<b">Bar</span>`);
-
- expect(clean.replace(/\s+/g, '')).toBe(
- `Clickyalert("&lt;b&gt;hi&lt;/b&gt;");&lt;divid=notarealtagonclick=notcode()&gt;&lt;notatag&lt;&lt;&lt;allinonetextblock&gt;&gt;&gt;&lt;%#somephpcodeherewrite("$horriblySyntacticConstruct1");%&gt;*/alert('hi');*/alert('hi');--&gt;*/alert('hi');--&gt;'}--&gt;--&gt;&lt;!--Zoicks--&gt;sectioninHTML]]&gt;<ahref=""></a><ahref="/">Bar</a>`,
- );
- });
-});
-
-describe('sanitizeString', () => {
- it('should not allow MathML and SVG', () => {
- const tainted = `
- Hi <a href="javascript:alert('hello')" target="_blank">this</a> is <i>in italics</i> and <ul>
- <li> lists </li>
- <li> are allowed</li>
- </ul>
- <p class="some-class">
- as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
- and <code>code.is.accepted too</code>
- </p>
- <svg><text>SVG isn't allowed</text></svg>
- <math xmlns="http://www.w3.org/1998/Math/MathML">
- <infinity />
- </math>`;
- const clean = `
- Hi <a>this</a> is <i>in italics</i> and <ul>
- <li> lists </li>
- <li> are allowed</li>
- </ul>
- <p class="some-class">
- as well. This is <b>Amazing</b> and this <strong>bold</strong> <br>
- and <code>code.is.accepted too</code>
- </p>`;
-
- expect(sanitizeString(tainted).trimRight()).toBe(clean);
- });
-});
diff --git a/server/sonar-web/src/main/js/helpers/code-difference.ts b/server/sonar-web/src/main/js/helpers/code-difference.ts
index ff7653546b2..2caaae10460 100644
--- a/server/sonar-web/src/main/js/helpers/code-difference.ts
+++ b/server/sonar-web/src/main/js/helpers/code-difference.ts
@@ -17,9 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { sanitizeHTMLNoSVGNoMathML } from 'design-system';
import { diffLines } from 'diff';
import { groupBy, keyBy } from 'lodash';
-import { sanitizeString } from './sanitize';
const NUMBER_OF_EXAMPLES = 2;
@@ -85,7 +86,7 @@ function differentiateCode(compliant: string, nonCompliant: string) {
nonCompliantCode += `<div class='code-removed'>${value}</div>`;
}
});
- return [sanitizeString(nonCompliantCode), sanitizeString(compliantCode)];
+ return [sanitizeHTMLNoSVGNoMathML(nonCompliantCode), sanitizeHTMLNoSVGNoMathML(compliantCode)];
}
function replaceInDom(current: Element, code: string) {
diff --git a/server/sonar-web/src/main/js/helpers/sanitize.ts b/server/sonar-web/src/main/js/helpers/sanitize.ts
deleted file mode 100644
index acdae5799ca..00000000000
--- a/server/sonar-web/src/main/js/helpers/sanitize.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { sanitize } from 'dompurify';
-
-export function sanitizeStringRestricted(html: string) {
- return sanitize(html, {
- ALLOWED_TAGS: ['b', 'br', 'code', 'i', 'li', 'p', 'strong', 'ul', 'a'],
- ALLOWED_ATTR: ['target', 'href'],
- });
-}
-
-export function sanitizeString(html: string) {
- return sanitize(html, { USE_PROFILES: { html: true } });
-}
-
-export function sanitizeUserInput(html: string) {
- return sanitize(html, {
- ALLOWED_TAGS: [
- 'b',
- 'br',
- 'code',
- 'i',
- 'li',
- 'p',
- 'strong',
- 'ul',
- 'ol',
- 'a',
- 'h1',
- 'h2',
- 'h3',
- 'h4',
- 'h5',
- 'h6',
- 'blockquote',
- 'pre',
- ],
- ALLOWED_ATTR: ['target', 'href', 'rel'],
- });
-}
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index b8a763a5a3d..62f81b59ef4 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -8723,6 +8723,7 @@ __metadata:
d3-shape: 3.2.0
d3-zoom: 3.0.0
date-fns: 4.1.0
+ dompurify: 3.1.7
lodash: 4.17.21
react: 18.3.1
react-day-picker: 8.10.0